C++线程
目录
C与C++的线程用法区别
1. 线程创建与管理
2. 线程同步
3. 传递对象
4. 异常处理
5. 线程的生命周期管理
6. 跨平台性
总结
线程库
thread类的简单介绍
基本代码实现
函数指针
Lambda
线程id
线程函数参数
线程和容器
移动赋值
移动构造(move)
小知识点move
lambda局部的可执行对象
锁 (不支持拷贝)
try_lock
死锁
lock_guard
unique_lock
条件变量
wait和notify_one
支持两个线程交替打印,一个打印奇数,一个打印偶数
原子性操作库(atomic)
单例模式的(懒汉模式具有线程安全问题)
编辑懒汉单例模式最简单(C++11之前不是线程安全的,C++11之后的是安全的)
C与C++的线程用法区别
C和C++的线程用法区别可以从多个角度进行比较,包括线程创建与管理、线程同步、传递对象、异常处理等方面。以下是C与C++线程用法的全面总结:
1. 线程创建与管理
-
C:
- C语言本身不支持线程,线程编程依赖外部库,如 POSIX Threads (pthreads) 或 Windows API。
- 使用
pthread_create()
创建线程,使用pthread_join()
或pthread_detach()
来管理线程。 - 线程管理较为底层,编程时需要显式处理线程创建、同步、销毁等操作。
示例(C语言,使用 pthread):
#include <pthread.h> #include <stdio.h>void* thread_function(void* arg) {printf("Thread is running.\n");return NULL; }int main() {pthread_t thread;pthread_create(&thread, NULL, thread_function, NULL);pthread_join(thread, NULL); // 等待线程结束return 0; }
-
C++:
- C++11及以后版本引入了对线程的原生支持,提供了
std::thread
类来管理线程。 - 线程创建更简单,线程对象会自动管理线程的生命周期。
- 支持线程同步与线程池等高级功能。
示例(C++语言,使用 std::thread):
#include <iostream> #include <thread>void thread_function() {std::cout << "Thread is running." << std::endl; }int main() {std::thread t(thread_function); // 创建线程t.join(); // 等待线程结束return 0; }
- C++11及以后版本引入了对线程的原生支持,提供了
2. 线程同步
-
C:
- 在C中,线程同步通常使用
pthread_mutex_t
,pthread_cond_t
等机制。 - 使用互斥锁(mutex)、条件变量等实现线程之间的协调,避免数据竞争。
示例(C语言,使用 pthread 互斥锁):
#include <pthread.h> #include <stdio.h>pthread_mutex_t lock;void* thread_function(void* arg) {pthread_mutex_lock(&lock); // 加锁printf("Thread is running.\n");pthread_mutex_unlock(&lock); // 解锁return NULL; }int main() {pthread_t thread;pthread_mutex_init(&lock, NULL);pthread_create(&thread, NULL, thread_function, NULL);pthread_join(thread, NULL);pthread_mutex_destroy(&lock);return 0; }
- 在C中,线程同步通常使用
-
C++:
- C++11引入了更高层次的线程同步机制,包括
std::mutex
,std::lock_guard
,std::condition_variable
和std::atomic
。 std::mutex
用于互斥锁,std::lock_guard
简化了锁的使用,自动管理锁的加锁与释放。std::condition_variable
实现线程之间的通知和等待机制。
示例(C++语言,使用 std::mutex):
#include <iostream> #include <thread> #include <mutex>std::mutex mtx;void thread_function() {std::lock_guard<std::mutex> guard(mtx); // 自动加锁std::cout << "Thread is running." << std::endl; }int main() {std::thread t(thread_function);t.join();return 0; }
- C++11引入了更高层次的线程同步机制,包括
3. 传递对象
-
C:
- 在C中,线程函数通常接受
void*
类型的参数,通过指针传递数据或对象。 - 需要手动管理指针的生命周期,确保线程结束后数据不会被意外修改。
示例(C语言,传递结构体指针):
#include <pthread.h> #include <stdio.h>typedef struct {int a;int b; } MyStruct;void* thread_function(void* arg) {MyStruct* data = (MyStruct*)arg;printf("a = %d, b = %d\n", data->a, data->b);return NULL; }int main() {pthread_t thread;MyStruct obj = {10, 20};pthread_create(&thread, NULL, thread_function, (void*)&obj);pthread_join(thread, NULL);return 0; }
- 在C中,线程函数通常接受
-
C++:
- C++支持通过值传递、引用传递以及使用智能指针(如
std::shared_ptr
和std::unique_ptr
)来传递对象。 - 值传递会复制对象副本,而引用传递则允许多个线程共享同一个对象。
- 智能指针可以自动管理内存,避免内存泄漏。
示例(C++语言,使用
std::shared_ptr
):#include <iostream> #include <thread> #include <memory>class MyClass { public:int a, b;MyClass(int x, int y) : a(x), b(y) {}void print() { std::cout << "a = " << a << ", b = " << b << std::endl; } };void thread_function(std::shared_ptr<MyClass> obj) {obj->print(); }int main() {std::shared_ptr<MyClass> obj = std::make_shared<MyClass>(10, 20);std::thread t(thread_function, obj); // 传递智能指针t.join();return 0; }
- C++支持通过值传递、引用传递以及使用智能指针(如
4. 异常处理
-
C:
- C语言没有内建的异常处理机制。错误处理通常通过返回值、全局变量或
setjmp
和longjmp
来实现。 - 在多线程中,异常往往无法传播到其他线程,开发者必须在每个线程内部处理异常。
- C语言没有内建的异常处理机制。错误处理通常通过返回值、全局变量或
-
C++:
- C++11支持异常处理,可以在线程函数内部使用
try-catch
语句来捕获和处理异常。 std::thread
类中的线程异常不会自动传播,需要显式地捕获异常。
示例(C++语言,使用异常处理):
#include <iostream> #include <thread>void thread_function() {try {throw std::runtime_error("Thread error");} catch (const std::exception& e) {std::cout << "Caught exception: " << e.what() << std::endl;} }int main() {std::thread t(thread_function);t.join();return 0; }
- C++11支持异常处理,可以在线程函数内部使用
5. 线程的生命周期管理
-
C:
- C语言的线程生命周期需要手动管理,线程的结束需要使用
pthread_join()
或pthread_detach()
来显式等待线程完成或分离线程。
- C语言的线程生命周期需要手动管理,线程的结束需要使用
-
C++:
- C++中的
std::thread
在其对象超出作用域时会自动销毁。如果线程对象没有显式地调用join()
或detach()
,则会抛出异常(std::terminate()
)。
示例(C++语言,自动销毁线程):
#include <iostream> #include <thread>void thread_function() {std::cout << "Thread is running." << std::endl; }int main() {std::thread t(thread_function);t.join(); // 等待线程结束return 0; // t超出作用域后自动销毁 }
- C++中的
6. 跨平台性
-
C:
- C中的线程库通常依赖于平台特定的API(如POSIX或Windows API),这意味着跨平台开发需要考虑不同平台的线程库和同步机制。
-
C++:
- C++11及以后版本提供了标准化的线程库
std::thread
和同步机制,可以在多种平台上使用(如Windows、Linux、macOS等),具有较好的跨平台性。
- C++11及以后版本提供了标准化的线程库
总结
特性 | C语言 | C++语言 |
---|---|---|
线程库 | 依赖外部库(pthreads、Windows API) | 原生支持(std::thread ) |
线程创建 | 使用 pthread_create | 使用 std::thread |
线程同步 | 使用 pthread_mutex , pthread_cond 等 | 使用 std::mutex , std::lock_guard , std::atomic 等 |
传递对象 | 通过指针传递(手动管理内存) | 通过值、引用、智能指针传递(更灵活、安全) |
异常处理 | 没有内建异常处理机制 | 支持 try-catch 语句,异常可以在线程中捕获 |
线程生命周期管理 | 手动管理,使用 pthread_join 或 pthread_detach | 自动销毁,未 join() 或 detach() 会调用 std::terminate |
跨平台性 | 需要依赖平台特定的线程库 | 跨平台支持更好,使用标准线程库 |
C++提供了更高级的线程管理机制,使得多线程编程更加简洁、安全和高效,而C则需要更多手动管理。
线程库
thread类的简单介绍
在C++11之前,涉及到多线程问题,都是和平台相关的,比如windows和linux下各有自己的接口,这使得代码的可移植性比较差。C++11中最重要的特性就是对线程进行支持了,使得C++在 并行编程时不需要依赖第三方库,而且在原子操作中还引入了原子类的概念。要使用标准库中的线程,必须包含< thread >头文件。thread文档
函数名 | 功能 |
---|---|
thread() | 构造一个线程对象,没有关联任何线程函数,即没有启动任何线程 |
thread(fn, args1, args2, ...) | 构造一个线程对象,并关联线程函数fn,args1,args2,...为线程函数的 参数 |
get_id() | 获取线程id |
jionable() | 线程是否还在执行,joinable代表的是一个正在执行中的线程。 |
jion() | 该函数调用后会阻塞住线程,当该线程结束后,主线程继续执行 |
detach() | 在创建线程对象后马上调用,用于把被创建线程与线程对象分离开,分离 的线程变为后台线程,创建的线程的"死活"就与主线程无关 |
Example
// thread example
#include <iostream> // std::cout
#include <thread> // std::threadvoid foo()
{// do stuff...
}void bar(int x)
{// do stuff...
}int main()
{std::thread first (foo); // spawn new thread that calls foo()std::thread second (bar,0); // spawn new thread that calls bar(0)std::cout << "main, foo and bar now execute concurrently...\n";// synchronize threads:first.join(); // pauses until first finishessecond.join(); // pauses until second finishesstd::cout << "foo and bar completed.\n";return 0;
注意:
1. 线程是操作系统中的一个概念,线程对象可以关联一个线程,用来控制线程以及获取线程的状态。
2. 当创建一个线程对象后,没有提供线程函数,该对象实际没有对应任何线程。
#include <thread>int main()
{std::thread t1;cout << t1.get_id() << endl;return 0;
}
get_id()的返回值类型为id类型,id类型实际为std::thread命名空间下封装的一个类,该类中 包含了一个结构体:
3. 当创建一个线程对象后,并且给线程关联线程函数,该线程就被启动,与主线程一起运行。 线程函数一般情况下可按照以下三种方式提供:(C与C++的区别,C++传的对象更多了 )
- 函数指针
- lambda表达式
- 函数对象
4. thread类是防拷贝的(不支持拷贝赋值和拷贝构造,支持移动复制和移动构造),不允许拷贝构造以及赋值,但是可以移动构造和移动赋值,即将一个线程对象关联线程的状态转移给其他线程对象,转移期间不意向线程的执行。
5. 可以通过joinable()函数判断线程是否是有效的,如果是以下任意情况,则线程无效
- 采用无参构造函数构造的线程对象
- 线程对象的状态已经转移给其他线程对象
- 线程已经调用jion或者detach结束
基本代码实现
函数指针
void func(int x)
{for (int i = 0; i < x; i++){cout << i << endl;}
}int main() {thread t1(func, 3); // 调用 int 版本//不要忘了等待t1.join();return 0;
}
这里要注意传重载函数时编译器无法确定,如果遇到这种情况,通常会出现编译错误。解决办法就是最好选择用Lambda 。
Lambda
void func(int x)
{for (int i = 0; i < x; i++){cout << i << endl;}
}
void func(int x, const string &s)
{for (int i = 0; i < x; i++){cout << i << endl << s << endl;}
}int main() {thread t1([]() {func(1);});thread t2([]() {func(2,"ljw");});//不要忘了等待t1.join();t2.join();return 0;
}
线程id
线程id
std::thread::get_id(获取线程id)
std::thread::get_id
int main() {thread t1([]() {func(1);});cout << "t1->" << t1.get_id() << endl;thread t2([]() {func(2,"ljw");});cout << "t2->" << t2.get_id() << endl;//不要忘了等待t1.join();t2.join();return 0;
}
std::this_thread::get_id(在线程里面获取自己的线程)
std::this_thread::get_id
void func(int x)
{for (int i = 0; i < x; i++){cout << i << endl;cout << this_thread::get_id() << endl;}
}
void func(int x, const string &s)
{for (int i = 0; i < x; i++){cout << i << endl << s << endl;cout << this_thread::get_id() << endl;}
}
线程函数参数
线程函数的参数是以值拷贝的方式拷贝到线程栈空间中的,因此:即使线程参数为引用类型,在 线程中修改后也不能修改外部实参,因为其实际引用的是线程栈中的拷贝,而不是外部实参。(用&接收,最好就要把ref加上)
错误样例
在线程函数中对x修改,不会影响外部实参,因为:线程函数参数虽然是引用方式,但其实际引用的是线程栈中的拷贝
void func(int& x)
{x++;
}int main()
{int x = 1;thread t1(func, x);cout << x;//不要忘了等待t1.join();return 0;
}
解决方案1,如果想要通过形参改变外部实参时,必须借助std::ref()函数(用&接收,最好就要把ref加上)
void func(int& x)
{x++;
}
int main()
{int x = 1;thread t1(func, ref(x));//cout << x;//这里要注意要在join以后再打印,要不然main这个线程可能会运行的快于funct1.join();cout << x;return 0;
}
解决方案2,地址的拷贝
void func(int* x)
{*x += 1;
}
int main()
{int x = 1;thread t1(func, &x);//cout << x;//这里要注意要在join以后再打印,要不然main这个线程可能会运行的快于funct1.join();cout << x;return 0;
}
注意:如果是类成员函数作为线程参数时,必须将this作为线程函数参数。
线程和容器
int main()
{size_t n;cin >> n;//创建n个线程执行Printvector<thread> vthd(n);return 0;
}
给n个值,第二个值,不给上图这里就是thread,相当于创建了n个线程对象,但没有执行
不支持拷贝赋值和拷贝构造,支持移动赋值和移动构造
移动赋值
std::thread::operator=
下面用多线程代码展示移动赋值
思路:
- 先创建几个空线程对象放在vector
- 再创建匿名线程对象移动复制给thd,相当于放入vector
void func(int x)
{for (int i = 0; i < x; i++){cout << i << endl;}
}
int main()
{size_t n;cin >> n;//创建n个线程执行funcvector<thread> vthd(n);for (auto& thd : vthd){//移动赋值thd = thread(func, 2);}for (auto& thd : vthd){thd.join();}return 0;
}
移动构造(move)
thread t1(func, 3);thread t2(move(t1));
小知识点move
在C++中,std::move
是一个用于将对象从左值转化为右值的标准库函数。它不会移动对象本身,而是通过转换对象的类型来启用移动语义。
主要用途:
-
启用移动语义:
std::move
使得对象能够通过移动而不是复制的方式传递给函数或赋值给其他对象。它本质上是一个类型转换工具,转化一个对象为右值引用。 -
提高性能: 使用移动语义可以避免不必要的复制,特别是当对象非常大或昂贵时,比如在容器操作中,
std::vector
、std::string
等类的数据转移。
语法:
std::move(x)
-
这里
x
是一个左值(普通变量或对象)。 -
std::move(x)
会将x
转换为一个右值引用(T&&
),即允许资源被“移动”而不是被复制。
示例代码:
#include <iostream>
#include <vector>
#include <utility> // for std::moveclass MyClass {
public:MyClass() {std::cout << "MyClass Constructor\n";}MyClass(const MyClass& other) {std::cout << "Copy Constructor\n";}MyClass(MyClass&& other) noexcept {std::cout << "Move Constructor\n";}
};int main() {MyClass obj1;MyClass obj2 = std::move(obj1); // Move constructor calledstd::vector<int> v1 = {1, 2, 3, 4};std::vector<int> v2 = std::move(v1); // Move the contents of v1 into v2std::cout << "v1 size: " << v1.size() << "\n"; // v1 is now empty, valid but unspecified statestd::cout << "v2 size: " << v2.size() << "\n"; // v2 contains the data
}
注意事项:
-
资源管理: 使用
std::move
后,原对象进入“已移交”状态,不能再被访问或操作。调用std::move
不会对对象做任何改变,它只是通过类型转换来触发移动语义。 -
函数返回值优化:
std::move
常用于返回对象时启用返回值优化(RVO)或者移动语义,使得返回值的复制被替换为移动。 -
与
std::forward
的关系:std::forward
用于在完美转发中保持对象的左值或右值属性,而std::move
总是将对象转化为右值。
总结:
std::move
是一个用于启用移动语义的工具,它允许对象的资源从一个位置“移动”到另一个位置而不是复制,从而提高程序的性能,特别是对于大型对象或容器。
lambda局部的可执行对象
int main()
{size_t n1 = 10000;size_t n2 = 10000;int x = 0;cout << n1 << "+" << n2 << endl;thread t1([&](){for (int i = 0; i < n1; i++){x++;} });thread t2([&](){for (int i = 0; i < n2; i++){x++;}});t1.join();t2.join();cout << x;return 0;
}
上述代码有线程安全的问题 ,有可能两个线程会同时++x,解决办法用锁
锁 (不支持拷贝)
mutex的种类
在C++11中,Mutex总共包了四个互斥量的种类:
1. std::mutex C++11提供的最基本的互斥量,该类的对象之间不能拷贝,也不能进行移动。mutex最常用的三个函数:
函数名 | 函数功能 |
---|---|
lock() | 上锁:锁住互斥量 |
unlock() | 解锁:释放对互斥量的所有权 |
try_lock() | 尝试锁住互斥量,如果互斥量被其他线程占有,则当前线程也不会被阻塞 |
注意,线程函数调用lock()时,可能会发生以下三种情况:
- 如果该互斥量当前没有被锁住,则调用线程将该互斥量锁住,直到调用 unlock之前, 该线程一直拥有该锁
- 如果当前互斥量被其他线程锁住,则当前的调用线程被阻塞住
- 如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)
线程函数调用try_lock()时,可能会发生以下三种情况:
- 如果当前互斥量没有被其他线程占有,则该线程锁住互斥量,直到该线程调用 unlock释放互斥量
- 如果当前互斥量被其他线程锁住,则当前调用线程返回 false,而并不会被阻塞掉
- 如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)
2. std::recursive_mutex
其允许同一个线程对互斥量多次上锁(即递归上锁),来获得对互斥量对象的多层所有权, 释放互斥量时需要调用与该锁层次深度相同次数的 unlock(),除此之外,
std::recursive_mutex 的特性和 std::mutex 大致相同。
3. std::timed_mutex
比 std::mutex 多了两个成员函数,try_lock_for(),try_lock_until() 。
- try_lock_for()
接受一个时间范围,表示在这一段时间范围之内线程如果没有获得锁则被阻塞住(与 std::mutex 的 try_lock() 不同,try_lock 如果被调用时没有获得锁则直接返回 false),如果在此期间其他线程释放了锁,则该线程可以获得对互斥量的锁,如果超 时(即在指定时间内还是没有获得锁),则返回 false。
- try_lock_until()
接受一个时间点作为参数,在指定时间点未到来之前线程如果没有获得锁则被阻塞住, 如果在此期间其他线程释放了锁,则该线程可以获得对互斥量的锁,如果超时(即在指 定时间内还是没有获得锁),则返回 false。
4. std::recursive_timed_mutex
mutex | 普通锁 |
recursive_mutex | 递归互斥锁(递归时就死锁了,实质是判断是不是当前线程,是当前线程就lock,不是就不lock) |
timed_mutex | 时间互斥锁 |
recursive_timed_mutex | 递归时间互斥锁 |
mutex文档
也是一个类
int main()
{size_t n1 = 10000;size_t n2 = 10000;int x = 0;mutex mtx;cout << n1 << "+" << n2 << endl;thread t1([&](){for (int i = 0; i < n1; i++){mtx.lock();x++;mtx.unlock();} });thread t2([&](){for (int i = 0; i < n2; i++){mtx.lock();x++;mtx.unlock();}});t1.join();t2.join();cout << x;return 0;
}
不用lambda线程时传递注意用引用接收时并且要改变传过来的值时,要加ref
void func(size_t n, int& x,mutex& mtx)
{for (int i = 0; i < 1000; i++){mtx.lock();x++;mtx.unlock();}
}int main()
{int x = 0;mutex mtx;thread t1(func,10, ref(x), ref(mtx));thread t2(func, 100,ref(x), ref(mtx));t1.join();t2.join();cout << x;return 0;
}
左值引用这里都要加ref,100和" "里的都是右值(thread里的参数会传给thread的构造函数,然后因为其底层原因,这些所有的值传递过去的时候,都会变成类似的右值)
try_lock
std::mutex::try_lock文档
try_lock是不阻塞,lock是阻塞
各种锁文档
mutex | 普通锁 |
recursive_mutex | 递归互斥锁(递归时就死锁了,实质是判断是不是当前线程,是当前线程就lock,不是就不lock) |
timed_mutex | 时间互斥锁 |
recursive_timed_mutex | 递归时间互斥锁 |
0.3秒运行一次
死锁
如果抛异常了直接退出程序了但锁还没有解除,就形成了死锁
int main()
{mutex mtx;mtx.lock();//抛异常了mtx.unlock();return 0;
}
解决办法:lock_guard
lock_guard文档
把锁交给类对象进行管理,进行资源的释放等等,避免抛异常出问题(死锁)
模拟一个lock_guard
这里用模板是因为锁也有很多种锁,注意成员变量要用&
template<class Lock>
class LockGuard
{
public:LockGuard(Lock& lk):_lk(lk){_lk.lock();}~LockGuard(){_lk.unlock();}
private:Lock& _lk;
};
会产生死锁代码实例
void func()
{srand(time(0));if (rand() % 3){throw exception("异常");}else{cout << "func" << endl;}
}
int main()
{mutex mtx;size_t n1 = 5;size_t n2 = 5;int x = 0;thread t1([&](){try{for (int i = 0; i < n1; i++){mtx.lock();//可能抛异常func();x++;mtx.unlock();}}catch (const exception& e){cout << e.what() << endl;}});return 0;
}
lock_guard
解决办法就是用std::lock_guard
lock_guard - C++ Reference (cplusplus.com)
unique_lock
unique_lock - C++ Reference (cplusplus.com)
两者区别,lock_guard只支持构造和析构 unique_lock支持手动解锁
代码示例
// unique_lock example
#include <iostream> // std::cout
#include <thread> // std::thread
#include <mutex> // std::mutex, std::unique_lockstd::mutex mtx; // mutex for critical sectionvoid print_block (int n, char c) {// critical section (exclusive access to std::cout signaled by lifetime of lck):std::unique_lock<std::mutex> lck (mtx);for (int i=0; i<n; ++i) { std::cout << c; }std::cout << '\n';
}int main ()
{std::thread th1 (print_block,50,'*');std::thread th2 (print_block,50,'$');th1.join();th2.join();return 0;
}
条件变量
std::condition_variable
condition_variable - C++ Reference (cplusplus.com)
常用:
wait | condition_variable::wait - C++ Reference (cplusplus.com) |
notify_one(唤醒一个) | condition_variable::notify_one - C++ Reference (cplusplus.com) |
notify_all(唤醒所有) | condition_variable::notify_all - C++ Reference (cplusplus.com) |
wait和notify_one
这里注意要用unique_lock,所以只能用unqiue_lock,wait的时候会unlock
wait后会自动释放锁
被唤醒的时候,被唤醒线程马上又会拿到lock
wait的第二个参数的用法
在 C++ 中,std::condition_variable::wait
函数的第二个参数用于指定一个谓词条件,用来判断是否需要等待或是继续执行。这是为了避免虚假唤醒(spurious wakeups),即线程在没有被显式通知的情况下被唤醒。
wait
函数的基本用法如下:
std::mutex mtx;
std::condition_variable cv;
bool ready = false;void thread_func()
{std::unique_lock<std::mutex> lock(mtx);// 使用wait的第二个参数作为条件,只有条件为false时才等待cv.wait(lock, []{ return ready; });// 当ready为true时,才会继续执行// 继续执行后面的代码
}void notify_func()
{std::this_thread::sleep_for(std::chrono::seconds(1));std::lock_guard<std::mutex> lock(mtx);ready = true;cv.notify_all();
}
在上面的例子中:
-
wait(lock, pred)
:lock
:一个std::unique_lock<std::mutex>
对象,表示线程要持有的锁。pred
:一个谓词条件,返回一个布尔值的函数或者可调用对象。在这个例子中是[] { return ready; }
,它在ready
为true
时返回true
,否则返回false
。
-
cv.wait(lock, pred)
会让线程在锁定条件变量的同时进入等待状态,直到谓词pred
返回true
,线程才会被唤醒继续执行。
这种方法避免了虚假唤醒的情况,因为它在每次被唤醒后都会检查条件是否满足。如果不满足,线程会继续等待。
支持两个线程交替打印,一个打印奇数,一个打印偶数
两个线程t1和t2,两个线程一个线程打印一次,交替执行
如何保证t1先运行
分析一下这个代码,t2可能会一直等待,因为如果t1跑的特别快时,唤醒的时候,t2还没有开始等待,那么当t1跑完的时候,t2就会在一直等待,没人唤醒,从而让程序崩溃
正确写法
没有要唤醒的就什么也不做
int main()
{int x = 0;mutex mtx;condition_variable cv;int flag = false;//保证t1进程先运行thread t1([&](){for (int i=0;i<5;i++){unique_lock<mutex> lck(mtx);//如果是false,++x,且保证t1先跳过循环while (flag) {cv.wait(lck);//wait时会释放锁unlock};x++;cout << "t1->" << x << endl;flag = true;cv.notify_one();//如果没有等待notify_one那么就什么也不做}});thread t2([&](){for (int i = 0; i < 5; i++){unique_lock<mutex> lck(mtx);//保证t2后执行while (!flag) {cv.wait(lck);};x++;cout << "t2->" << x << endl;flag = false;cv.notify_one();}});t1.join();t2.join();return 0;
}
解释:首先他们会先竞争锁,如果t1拿到锁了(只有一个线程能拿到锁)那么t2就会在unique_lock这停下来,极端条件下,然后t1走了一个循环后,再拿到了锁,但flag是true要进行等待了,等待的时候会释放锁,然后t2就会拿到锁。
如果是t2先拿到了锁,会直接进入wait,然后t1执行,后面的就跟t1先拿到锁的情况一样了
wait会解锁
思路
第一种,1在锁上面,2执行完了解锁,就会唤醒你
第二种,不在锁上面,获取到锁了,但在wait上面,wait上面会解锁,然后t2执行,再notify t1,然后把锁交给t1
第三种,t1的时间片到了,在}不动了, 然后t2执行完后一定会wait,然后释放锁,t1有时间片后,就可以拿到锁
原子性操作库(atomic)
atomic - C++ Reference (cplusplus.com)
多线程最主要的问题是共享数据带来的问题(即线程安全)。如果共享数据都是只读的,那么没问 题,因为只读操作不会影响到数据,更不会涉及对数据的修改,所以所有线程都会获得同样的数 据。但是,当一个或多个线程要修改共享数据时,就会产生很多潜在的麻烦。比如:
这种情况用加锁,会很消耗资源,临界时间时间段不适合用互斥锁,线程会频繁阻塞
要用自旋锁(库里没有)用原子的,一般都是内置类型的
int main()
{size_t n1 = 10000;size_t n2 = 10000;int x = 0;mutex mtx;cout << n1 << "+" << n2 << endl;thread t1([&](){for (int i = 0; i < n1; i++){mtx.lock();x++;mtx.unlock();} });thread t2([&](){for (int i = 0; i < n2; i++){mtx.lock();x++;mtx.unlock();}});t1.join();t2.join();cout << x;return 0;
}
原子类型
int main()
{size_t n1 = 10000;size_t n2 = 10000;mutex mtx;atomic<bool> flag = true;atomic<int> x = 0;cout << n1 << "+" << n2 << endl;thread t1([&](){for (int i = 0; i < n1; i++){x++;flag = false;} });thread t2([&](){for (int i = 0; i < n2; i++){x++;flag = false;}});t1.join();t2.join();printf("%d\n", x);//错误打印方法cout << x;return 0;
}
这里要注意的是
printf("%d\n", x);//错误打印方法
shared_ptr多线程问题
shared_ptr多线程问题
++的时候用原子的
只想保护*copy1,用{ }局部域
单例模式的(懒汉模式具有线程安全问题)
同时调用这一步
懒汉单例模式最简单(C++11之前不是线程安全的,C++11之后的是安全的)
C++11把变成了原子操作