『 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++11
std::thread`类的简单介绍
1.1 函数名与功能
函数名 | 功能 |
---|---|
thread() | 构造一个线程对象,没有关联任何线程函数,即没有启动任何线程 |
thread(fn, args1, args2, ...) | 构造一个线程对象,并关联线程函数fn ,args1 ,args2 ,…为线程函数的参数 |
get_id() | 获取线程id |
joinable() | 线程是否还在执行,joinable 代表的是一个正在执行中的线程 |
join() | 该函数调用后会阻塞住线程,当该线程结束后,主线程继续执行 |
detach() | 在创建线程对象后马上调用,用于把被创建线程与线程对象分离开,分离的线程变为后台线程,创建的线程的“死活”就与主线程无关 |
1.2std::thread
类的简单介绍
在C++11之前,涉及到多线程问题,都是和平台相关的,比如Windows和Linux下各有自己的接口,这使得代码的可移植性比较差。C++11中最重要的特性就是对线程进行支持了,使得C++在并行编程时不需要依赖第三方库,而且在原子操作中还引入了原子类的概念。要使用标准库中的线程,必须包含<thread>
头文件。C++11中线程类。
注意:
• 线程是操作系统中的一个概念,线程对象可以关联一个线程,用来控制线程以及获取线程的状态。
• 当创建一个线程对象后,没有提供线程函数,该对象实际没有对应任何线程。
• get_id()
的返回值类型为id
类型,id
类型实际为std::thread
命名空间下封装的一个类,该类中包含了一个结构体:
#include <thread>int main(){std::thread t1;std::cout << t1.get_id() << std::endl;return 0;}
• 当创建一个线程对象后,并且给线程关联线程函数,该线程就被启动,与主线程一起运行。线程函数一般情况下可按照以下三种方式提供:
• 函数指针
• lambda表达式
• 函数对象
• std::thread
类是防拷贝的,不允许拷贝构造以及赋值,但是可以移动构造和移动赋值,即将一个线程对象关联线程的状态转移给其他线程对象,转移期间不影响线程的执行。
• 可以通过joinable()
函数判断线程是否是有效的,如果是以下任意情况,则线程无效:
• 采用无参构造函数构造的线程对象
• 线程对象的状态已经转移给其他线程对象
• 线程已经调用join
或者detach
结束
示例代码:
#include <iostream>
using namespace std;
#include <thread>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_guard
和std::unique_lock
是C++11中引入的RAII风格的锁管理类。它们通过构造函数自动加锁,析构函数自动解锁,有效避免了死锁问题。std::unique_lock
比std::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<mutex> lg(mtx);/*lock_guard<mutex> 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;
}
代码死锁的原因是线程t1
中func()
函数可能抛出异常,导致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<int>
,从而确保引用计数的线程安全。
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之后,由编译器自动保证线程安全。编译器会在静态局部变量的初始化过程中插入互斥锁机制,确保在多线程环境下,即使多个线程同时访问该静态局部变量,也只会有一个线程能够初始化它,