在现代多核处理器时代,并发编程是提升程序性能的关键手段。C++11引入了对线程的原生支持,简化了多线程编程的复杂性。本文将结合代码示例,探讨C++线程的使用场景、原子操作的优势,以及如何解决线程安全问题。
在C++11之前,多线程编程依赖于平台相关的API,如Windows的CreateThread
和Linux的pthread_create
,这限制了代码的跨平台能力。C++11引入了std::thread
类,提供了统一的线程管理接口,简化了线程的创建、同步和销毁。
C++11
std::thread`类的简单介绍函数名 | 功能 |
---|---|
thread() |
构造一个线程对象,没有关联任何线程函数,即没有启动任何线程 |
thread(fn, args1, args2, ...) |
构造一个线程对象,并关联线程函数fn ,args1 ,args2 ,…为线程函数的参数 |
get_id() |
获取线程id |
joinable() |
线程是否还在执行,joinable 代表的是一个正在执行中的线程 |
join() |
该函数调用后会阻塞住线程,当该线程结束后,主线程继续执行 |
detach() |
在创建线程对象后马上调用,用于把被创建线程与线程对象分离开,分离的线程变为后台线程,创建的线程的“死活”就与主线程无关 |
std::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;
}
面试题:并发与并行的区别?
内容有点多,先放下面的链接了
C++多线程编程中的参数传递技巧
在多线程环境中,同步是确保线程安全的关键。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;
}
死锁是多线程编程中常见的问题。以下是一个简单的死锁示例:
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;
}
代码死锁的原因是线程t1
中func()
函数可能抛出异常,导致mtx.lock()
之后未能执行mtx.unlock()
,从而使得线程t2
无法获取锁,进而导致死锁。
原子操作是解决线程安全问题的有效手段之一。C++11引入了std::atomic
类模板,提供了对任意类型变量的原子操作支持。
原子操作确保了变量的读写操作不会被中断,从而避免了竞态条件。与互斥锁相比,原子操作的开销更小,适用于临界区较短的场景。
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;
}
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;
}
原子操作在底层通常通过特定的CPU指令实现。例如,x++
操作在普通情况下可能被编译器拆分为多个指令,而在原子操作中,x++
会被编译为一条不可分割的指令,从而确保操作的原子性。
共享资源的线程安全问题一直是并发编程中的难点。std::shared_ptr
是一个典型的共享资源,其引用计数需要确保线程安全。
std::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)++
操作不是线程安全的。为了解决这个问题,可以使用互斥锁保护对共享资源的访问。
另一种解决方案是使用原子类型保护共享资源。例如,可以将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;
}
单例模式是一种常见的设计模式,但在多线程环境中,单例模式的线程安全问题需要特别关注。
懒汉模式的线程安全问题
在多线程环境中可能会导致多个实例被创建。为了解决这个问题,可以使用双重检查锁定(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;
}
C++11之后,静态变量初始化的线程安全性得到了显著改善。编译器会自动为静态局部变量的初始化提供互斥锁,确保其在多线程环境中的线程安全。这意味着在单例模式中,使用静态局部变量的方式是最简单且且最安全的实现方式。
这是因为静态局部变量的初始化在C++11之后,由编译器自动保证线程安全。编译器会在静态局部变量的初始化过程中插入互斥锁机制,确保在多线程环境下,即使多个线程同时访问该静态局部变量,也只会有一个线程能够初始化它,