线程安全
什么是线程安全
解释一:
线程安全是指代码在多线程访问某个类(方法或者对象)时,这个类始终能表现出正确的行为。换种说法,如果一个类或者对象能够在多线程环境下运行,其中内容不会因为多线程的并发访问而输出错误结果或状态,那么它就是线程安全的
解释二:
在多线程同时对临界区资源(共享资源), 最终这个临界区资源的最终操作结果的值是正确的,那么就是线程安全,反之就是线程不安全
线程安全的核心问题
- 原子性
这点上,跟数据库事务的原子性概念差不多,即一个操作(可能含有多个子操作)要么全部执行完毕(即生效),要么全部都不执行(都不生效)
关于这个内容有个很简单的转账问题:C有事需要30万,他的余额剩下10万。于是,他想向A借钱,但是A只能借10万,所幸B还能借他10万,他让A和B向他的银行账户转账。A在向C进行转账之前,读取C的余额为10万,加上他向C转账的10万,计算得出此时C的账户应该有20万,但还未来得及将结果写入,此时B的转账请求来了;B同样发现C的余额为10万,然后转入10万后并写入,此时A同样将计算的30万写入到C的余额。这种情况下,C的最终余额为20万,并非预期的30万
- 可见性
指当一个线程修改了对象的状态或者值的时候,其他线程能够同步进行看到,这称为可见性。
如果此时两个线程处于不同的CPU,那么在线程1改变了 i 的值还未刷新到主存,线程2也要改变 i 的值,此时这个变量 i 的值肯定还是之前的值,线程1对变量的修改,线程2并没有看到,这就是可见性问题
- 有序性
程序执行的顺序需要按照代码的先后顺序执行,在多线程编程时需要考虑这个问题。
示例:抢票
当多个线程同时共享同一个全局变量或静态变量(局部变量不会),并做写操作时,可能会发生数据冲突问题,也就是线程安全问题。但读操作并不会发生数据冲突问题
// 操作共享变量会有问题的抢票代码#include<stdio.h>#include<stdlib.h>#include<string.h>#include<unistd.h>#include<pthread.h>int ticket =100;void*route(void*arg){char*id =(char*)arg;while(1){if( ticket >0){usleep(1000);printf("%s sells ticket:%d\n", id, ticket);
ticket--;}else{break;}}}intmain(void){
pthread_t t1, t2, t3, t4;pthread_create(&t1,NULL, route,(void*)"thread 1");pthread_create(&t2,NULL, route,(void*)"thread 2");pthread_create(&t3,NULL, route,(void*)"thread 3");pthread_create(&t4,NULL, route,(void*)"thread 4");pthread_join(t1,NULL);pthread_join(t2,NULL);pthread_join(t3,NULL);pthread_join(t4,NULL);return0;}
结果:
从这里的输出结果来看,售票数量出现了负数,表示此时票数已经出现了问题,因为不可能两个人同时在一个位置吧?这就是线程不安全导致出现的问题了。
如何保证线程安全?
在C++中,可以使用一下几种方式来确保 线程安全
1.使用互斥量(mutex)来对临界资源(共享资源)进行保护。互斥量可以防止多个线程同时访问临界资源,从而避免数据竞争所导致的问题。
2.使用读写锁(reader-writer lock)来对共享资源进行保护。读写锁允许多个读线程同时访问共享资源,但是写线程必须独占资源。这样可以在保证线程安全的同时,尽可能的提高系统的并发性
3.使用条件变量(condition variable)来协调线程之间的协作。条件变量可以用来在线程之间传递信号,从而控制线程的执行流程。
4.使用信号量(POSIX),用于却表多个进程/线程或者不同部分的同一进程在访问共享资源的安全性。他们可以通过在不同的进程或线程之间共享来构建并发程序,从而避免竞争条件和死锁等并发编程问题。
1.使用互斥量(mutex)来保护共享资源:
#define_CRT_SECURE_NO_WARNINGS1#include<iostream>#include<stdlib.h>#include<string>#include<unistd.h>#include<thread>#include<mutex>int tickets =100;
std::mutex mutex;voidroute(){//这里也采用了RAII思想
std::lock_guard<std::mutex>lock(mutex);while(1){//共享资源if( tickets >0){
std::cout <<"get a ticket: "<< tickets--<< std::endl;usleep(1000);}else{break;}// 抢完票的后序动作usleep(1000);}}intmain(){
std::thread t1(route);
std::thread t2(route);
std::thread t3(route);
std::thread t4(route);
t1.join();
t2.join();
t3.join();
t4.join();return0;}
1.上述例子中,我们定义了一个全局互斥量****mutex 和一个共享资源 ticket。然后在
route
函数中,我们使用
std::lock_guard
对mutex进行加锁。这样就可以保证在同一时刻,只能有一个线程可以访问 ticket。
2.在 main 函数中。我们创建了四个线程 t1,t2,t3 和 t4,并让它们都执行route抢票函数操作。由于 在一个线程访问到共享资源之后,会用 mutex 对共享资源进行加锁,所以此时只有一个线程能够进行抢票(修改ticket的值),因此最终结果就是 ticket == 0。
2.使用读写锁(reader-writer lock)来保护共享资源:
#define_CRT_SECURE_NO_WARNINGS1#include<iostream>#include<stdlib.h>#include<string>#include<thread>#include<shared_mutex>#include<Windows.h>int tickets =100;
std::shared_mutex mutex;//全局读写锁voidroute(){
std::unique_lock<std::shared_mutex>lock(mutex);//上写锁while(1){//共享资源if(tickets >0){
std::cout <<"get a ticket: "<< tickets--<< std::endl;Sleep(10);}else{break;}// 抢完票的后序动作Sleep(10);}}voidreadRoute(){
std::shared_lock<std::shared_mutex>lock(mutex);//上读锁
std::cout <<"tickets is : "<< tickets << std::endl;}intmain(){
std::thread t1(route);
std::thread t2(route);
std::thread t3(readRoute);
std::thread t4(readRoute);
t1.join();
t2.join();
t3.join();
t4.join();return0;}
1.在这个例子中,我们定义类一个读写锁和一个共享资源 tickets。 然后在
route
函数中,我们使用
std::unique_lock
对 mutex 进行加写锁。这时可以保证在同一时刻,只能有一个写线程对 tickets 的值进行修改。
2.在readroute
函数中,我们使用
std::shared_lock
对 g_mutex 进行加读锁。这样可以保证在同意是可以,能够有多个读线程可以同时读取 g_counter 的值,但是写线程必须等待所有的读线程结束后才能执行;同理,如果写线程对共享资源进行操作,此时读线程也无法获取到共享资源
常见的读写锁操作
- 读锁定(Read Lock):请求对共享资源的读取权限。如果没有线程持有写锁,则允许多个读线程同时获得读锁。
- 读解锁(Read Unlock):释放读锁,当所有读锁都被释放后,写线程可以请求获取写锁。
- 写锁定(Write Lock):请求对共享资源的写权限。写锁请求会阻塞,直到没有任何读锁或写锁被其他线程持有。
- 写解锁(Write Unlock):释放写锁,使得其他读线程或写线程可以对资源进行访问。
3.使用条件变量(condition variable)使线程能够更好的协调工作:
#define_CRT_SECURE_NO_WARNINGS1#include<iostream>#include<stdlib.h>#include<string>#include<thread>#include<mutex>#include<Windows.h>#include<condition_variable>int tickets =100;bool flag =false;
std::mutex mutex;//全局锁
std::condition_variable cv;voidroute1(){
std::unique_lock<std::mutex>lock(mutex);while(1){//共享资源if(tickets >0){
std::cout <<"get a ticket: "<< tickets--<< std::endl;Sleep(10);}else{
flag =true;//将标志符置为true,并通知线程2
cv.notify_one();break;}// 抢完票的后序动作Sleep(10);}}voidroute2(){
std::unique_lock<std::mutex>lock(mutex);while(!flag){
cv.wait(lock);}
std::cout <<"thread 2 finished"<< std::endl;}intmain(){
std::thread t1(route1);
std::thread t2(route2);
t1.join();
t2.join();return0;}
- 这个例子中,我们定义了一个互斥量
mutex
和一个条件变量cv
。我们还定义了一个全局变量flag
,用于标记抢票是否完成。 2.在线程1,在执行完抢票之后,我们将 flag 置为 true,并使用cv_notify_one()
函数来通知线程2 3.在线程 2 中,我们使用 while (!g_flag) 循环检测 g_flag 的值。如果 flag 为 false,则使用cv.wait(lock)
函数等待通知,否则执行后续的操作。 4.当线程 1 通知线程 2 时,线程 2 将被唤醒,并继续往下执行。最终,线程 2 会输出 “thread 2 finished”。 5.通过这个例子,我们可以看到,使用条件变量可以在线程间协调协作,使得线程可以根据某些条件的改变而被唤醒或等待。
4.使用信号量来保证线程安全
#include<semaphore>#include<thread>#include<iostream>
std::semaphore sem(5);// 创建信号量,初始值为 5voidthread_func(){
sem.wait();// 等待信号量的值大于 0
std::cout <<"Thread "<< std::this_thread::get_id()<<" is accessing the resource."<< std::endl;// 访问共享资源
sem.post();// 释放信号量的值}intmain(){
std::thread threads[10];// 创建 10 个线程for(int i =0; i <10;++i){
threads[i]= std::thread(thread_func);}for(int i =0; i <10;++i){
threads[i].join();}return0;}
在这个示例中,我们创建了一个信号量,初始值为 5,表示共享资源可以被 5 个线程同时访问。每个线程在访问共享资源之前会等待信号量的值大于 0,如果值为 0,则会阻塞直到其他线程释放信号量的值。
版权归原作者 梦静泽 所有, 如有侵权,请联系我们删除。