『 C++ 』线程与原子操作:高效并发编程的利器

文章目录

  • 为什么使用C++线程
  • 一、`C++11`std::thread`类的简单介绍
    • 1.1 函数名与功能
    • 1.2`std::thread`类的简单介绍
    • 1.3 线程函数参数
  • 二、 线程同步与锁
    • 2.1 线程同步与锁
    • 2.2死锁演示
  • 三、原子操作
    • 3.1 原子操作与线程安全
    • 3.2 原子操作的优势
    • 3.3 CAS操作与自旋锁
    • 3.4 原子操作与普通操作的汇编对比
  • 四、共享资源的线程安全问题
    • 4.1`std::shared_ptr`的线程安全问题
    • 4.2 使用原子类型保护共享资源
  • 五、单例模式的线程安全问题
    • 5.1 懒汉模式的单例模式
    • 5.2 静态变量初始化的线程安全性

  在现代多核处理器时代,并发编程是提升程序性能的关键手段。C++11引入了对线程的原生支持,简化了多线程编程的复杂性。本文将结合代码示例,探讨C++线程的使用场景、原子操作的优势,以及如何解决线程安全问题。

为什么使用C++线程



  在C++11之前,多线程编程依赖于平台相关的API,如Windows的CreateThread和Linux的pthread_create,这限制了代码的跨平台能力。C++11引入了std::thread类,提供了统一的线程管理接口,简化了线程的创建、同步和销毁。

一、C++11std::thread`类的简单介绍

1.1 函数名与功能


函数名 功能
thread() 构造一个线程对象,没有关联任何线程函数,即没有启动任何线程
thread(fn, args1, args2, ...) 构造一个线程对象,并关联线程函数fnargs1args2,…为线程函数的参数
get_id() 获取线程id
joinable() 线程是否还在执行,joinable代表的是一个正在执行中的线程
join() 该函数调用后会阻塞住线程,当该线程结束后,主线程继续执行
detach() 在创建线程对象后马上调用,用于把被创建线程与线程对象分离开,分离的线程变为后台线程,创建的线程的“死活”就与主线程无关

1.2std::thread类的简单介绍

  在C++11之前,涉及到多线程问题,都是和平台相关的,比如Windows和Linux下各有自己的接口,这使得代码的可移植性比较差。C++11中最重要的特性就是对线程进行支持了,使得C++在并行编程时不需要依赖第三方库,而且在原子操作中还引入了原子类的概念。要使用标准库中的线程,必须包含头文件。C++11中线程类。

 注意:

• 线程是操作系统中的一个概念,线程对象可以关联一个线程,用来控制线程以及获取线程的状态。

• 当创建一个线程对象后,没有提供线程函数,该对象实际没有对应任何线程。

get_id()的返回值类型为id类型,id类型实际为std::thread命名空间下封装的一个类,该类中包含了一个结构体:

   #include 
   int main()
   {
       std::thread t1;
       std::cout << t1.get_id() << std::endl;
       return 0;
   }

• 当创建一个线程对象后,并且给线程关联线程函数,该线程就被启动,与主线程一起运行。线程函数一般情况下可按照以下三种方式提供:

• 函数指针

• lambda表达式

• 函数对象

std::thread类是防拷贝的,不允许拷贝构造以及赋值,但是可以移动构造和移动赋值,即将一个线程对象关联线程的状态转移给其他线程对象,转移期间不影响线程的执行。

• 可以通过joinable()函数判断线程是否是有效的,如果是以下任意情况,则线程无效:

• 采用无参构造函数构造的线程对象

• 线程对象的状态已经转移给其他线程对象

• 线程已经调用join或者detach结束

 示例代码:

#include 
using namespace std;
#include 

void ThreadFunc(int a)
{
    cout << "Thread1" << a << endl;
}

class TF
{
public:
    void operator()()
    {
        cout << "Thread3" << endl;
    }
};

int main()
{
    // 线程函数为函数指针
    thread t1(ThreadFunc, 10);
    // 线程函数为lambda表达式
    thread t2([]{cout << "Thread2" << endl; });
    // 线程函数为函数对象
    TF tf;
    thread t3(tf);
    t1.join();
    t2.join();
    t3.join();
    cout << "Main thread!" << endl;
    return 0;
}

 面试题:并发与并行的区别?

1.3 线程函数参数

内容有点多,先放下面的链接了

C++多线程编程中的参数传递技巧

二、 线程同步与锁

2.1 线程同步与锁

在多线程环境中,同步是确保线程安全的关键。C++提供了多种同步机制,如互斥锁(std::mutex)、条件变量(std::condition_variable)等。

lock_guard与unique_lock

std::lock_guardstd::unique_lock是C++11中引入的RAII风格的锁管理类。它们通过构造函数自动加锁,析构函数自动解锁,有效避免了死锁问题。std::unique_lockstd::lock_guard更灵活,支持条件变量和定时锁。

std::mutex mtx;
std::condition_variable cv;
bool flag = false;

void ThreadFunc() {
    std::unique_lock<std::mutex> lock(mtx);
    cv.wait(lock, [] { return flag; });
    std::cout << "ThreadFunc: Flag is true" << std::endl;
}

int main() {
    std::thread t(ThreadFunc);
    {
        std::lock_guard<std::mutex> lock(mtx);
        flag = true;
    }
    cv.notify_one();
    t.join();
    return 0;
}

2.2死锁演示

死锁是多线程编程中常见的问题。以下是一个简单的死锁示例:

void func()
{
	srand(time(0));
	if (rand() % 2 == 0)
	{
		throw exception("异常");
	}
	else
	{
		cout << "func()" << endl;
	}
}
//
//RAII
template<class Lock>
class LockGuard
{
public:
	LockGuard(Lock& lk)
		:_lk(lk)
	{
		_lk.lock();
	}

	~LockGuard()
	{
		_lk.unlock();
	}
private:
	Lock& _lk;
};

//lock_guard
//unique_lock
// a. 可以跟time_mutex配合使用
// b. 支持手动解锁, 再加锁
// RTTI 运行阶段类型识别

int main()
{
	mutex mtx;
	size_t n1 = 10000;
	size_t n2 = 10000;

	size_t x = 0;
	thread t1([n1, &x, &mtx]() {
		try {
			for (size_t i = 0; i < n1; i++)
			{
				mtx.lock();
				//LockGuard lg(mtx);
				/*lock_guard lg(mtx);*/

				++x;
				func();
				//mtx.lock();
			}
		}
		catch (const exception& e)
		{
			cout << e.what() << endl;
		}
	});

	thread t2([n2, &x, &mtx]() {
		for (size_t i = 0; i < n2; i++)
		{
			mtx.lock();
			++x;
			mtx.unlock();
			}
		});

	t1.join();
	t2.join();

	cout << x << endl;

	return 0;
}

代码死锁的原因是线程t1func()函数可能抛出异常,导致mtx.lock()之后未能执行mtx.unlock(),从而使得线程t2无法获取锁,进而导致死锁。

三、原子操作

3.1 原子操作与线程安全

原子操作是解决线程安全问题的有效手段之一。C++11引入了std::atomic类模板,提供了对任意类型变量的原子操作支持。

3.2 原子操作的优势

原子操作确保了变量的读写操作不会被中断,从而避免了竞态条件。与互斥锁相比,原子操作的开销更小,适用于临界区较短的场景。

std::atomic<int> x(0);

void Increment() {
    for (int i = 0; i < 1000; i++) {
        x++;
    }
}

int main() {
    std::thread t1(Increment);
    std::thread t2(Increment);
    t1.join();
    t2.join();
    std::cout << x << std::endl;
    return 0;
}

3.3 CAS操作与自旋锁

std::atomic内部通常基于硬件的CAS(Compare-And-Swap)操作实现。CAS操作通过比较和交换机制确保操作的原子性。以下是一个CAS操作的示例:

std::atomic<int> x(0);

void CASIncrement() {
    for (int i = 0; i < 1000000; i++) {
        int old, newval;
        do {
            old = x.load();
            newval = old + 1;
        } while (!x.compare_exchange_weak(old, newval));
    }
}

int main() {
    std::thread t1(CASIncrement);
    std::thread t2(CASIncrement);
    t1.join();
    t2.join();
    std::cout << x << std::endl;
    return 0;
}

3.4 原子操作与普通操作的汇编对比

原子操作在底层通常通过特定的CPU指令实现。例如,x++操作在普通情况下可能被编译器拆分为多个指令,而在原子操作中,x++会被编译为一条不可分割的指令,从而确保操作的原子性。

四、共享资源的线程安全问题

共享资源的线程安全问题一直是并发编程中的难点。std::shared_ptr是一个典型的共享资源,其引用计数需要确保线程安全。

4.1std::shared_ptr的线程安全问题

std::shared_ptr本身是线程安全的,但其保护的资源可能不是线程安全的。例如,多个线程同时访问std::shared_ptr指向的对象时,可能会导致数据竞争。

std::shared_ptr<int> sp = std::make_shared<int>(0);

void IncrementShared() {
    for (int i = 0; i < 1000000; i++) {
        (*sp)++;
    }
}

int main() {
    std::thread t1(IncrementShared);
    std::thread t2(IncrementShared);
    t1.join();
    t2.join();
    std::cout << *sp << std::endl;
    return 0;
}

在这个例子中,std::shared_ptr的引用计数是线程安全的,但(*sp)++操作不是线程安全的。为了解决这个问题,可以使用互斥锁保护对共享资源的访问。

4.2 使用原子类型保护共享资源

另一种解决方案是使用原子类型保护共享资源。例如,可以将std::shared_ptr的引用计数改为std::atomic,从而确保引用计数的线程安全。

std::shared_ptr<int> sp = std::make_shared<int>(0);
std::atomic<int> count(0);

void IncrementShared() {
    for (int i = 0; i < 1000000; i++) {
        count.fetch_add(1);
    }
}

int main() {
    std::thread t1(IncrementShared);
    std::thread t2(IncrementShared);
    t1.join();
    t2.join();
    std::cout << count << std::endl;
    return 0;
}

五、单例模式的线程安全问题

  单例模式是一种常见的设计模式,但在多线程环境中,单例模式的线程安全问题需要特别关注。

  懒汉模式的线程安全问题

5.1 懒汉模式的单例模式

  在多线程环境中可能会导致多个实例被创建。为了解决这个问题,可以使用双重检查锁定(Double-Checked Locking)或std::call_once
  下面是一个使用静态局部变量实现线程安全单例模式的 C++ 示例。

class Singleton {
public:
    static Singleton& GetInstance() {
        static Singleton instance;
        return instance;
    }

private:
    Singleton() {}
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
};

int main() {
    Singleton s1 = Singleton::GetInstance();
    Singleton s2 = Singleton::GetInstance();
    assert(s1 == s2); // 验证单例
    return 0;
}

5.2 静态变量初始化的线程安全性

  C++11之后,静态变量初始化的线程安全性得到了显著改善。编译器会自动为静态局部变量的初始化提供互斥锁,确保其在多线程环境中的线程安全。这意味着在单例模式中,使用静态局部变量的方式是最简单且且最安全的实现方式。
  这是因为静态局部变量的初始化在C++11之后,由编译器自动保证线程安全。编译器会在静态局部变量的初始化过程中插入互斥锁机制,确保在多线程环境下,即使多个线程同时访问该静态局部变量,也只会有一个线程能够初始化它,

你可能感兴趣的:(C++,多线程,c++,开发语言)