【Linux】线程安全——补充|互斥、锁|同步、条件变量(上) https://developer.aliyun.com/article/1565759
🌙 Linux线程互斥
💫 互斥相关概念
临界资源:
多个执行流进行安全访问的共享资源就叫临界资源
临界区:
多个执行流进行访问临界资源的代码就是临界区
互斥:
任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用。
原子性:
不会被任何调度机制打断的操作,该操作只有两态,要么不做,要么做完,这就是原子性。
理解原子性:
一个资源进行的操作如果只用一条汇编语句就能完成,就是原子性的,反之不是原子的。对变量++或者–。在C、C++上,看起来只有一条语句,但是汇编之后至少是三条语句:
- 从内存读取数据到CPU寄存器中
- 在寄存器中让CPU进行对应的算逻运算
- 写回新的结果到内存中变量的位置
对一个资源访问的时候,要么不做,要么做完,不是原子性的情况:线程A被切换,没做完,有中间状态,不是原子性。实际上对变量做–的时候,对应三条汇编语句,未来会对应三条汇编语句!所以很明显,++、–不是原子性的,不是一条语句。
单纯的++或者++都不是原子的,有可能会有数据一致性的问题。
💫 互斥量mutex
概念:
大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个线程,其他线程无法获得这种变量。但有时候,很多变量需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互多个线程并发的操作共享变量,会带来问题:数据不一致问题。
要解决线程不安全的情况,保护共享资源:
- 代码必须有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。
- 如果多个线程同时要求执行临界区的代码,并且此时临界区没有线程在执行,那么只能允许一个线程进入该临界区。
- 如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。
- 实际上就是需要一把锁,Linux提供的这把锁就叫互斥量,如果一个线程持有锁,那么其他的线程就无法进来访问了。
常见的相关接口:
#include <pthread.h> // 初始化 int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr); // 销毁 int pthread_mutex_destroy(pthread_mutex_t *mutex); // 全局 pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; //成功返回0,失败返回错误码
即可以定义成局部的,也可以定义成全局的。pthread_mutex_t是锁的类型,如果我们定义的锁是全局的,就不要用pthread_mutex_int和pthread_mutex_destroy初始化和销毁了。
#include <pthread.h> //加锁 int pthread_mutex_lock(pthread_mutex_t *mutex); //如果加锁成功,直接持有锁,加锁不成功,此时立马出错返回(试着加锁,非阻塞获取方式) int pthread_mutex_trylock(pthread_mutex_t *mutex); //解锁 int pthread_mutex_unlock(pthread_mutex_t *mutex); // 成功返回0,失败返回错误码
💫 mutex的使用
全局锁的使用:
使用全局锁+4个线程的代码,定义全局锁并初始化PTHREAD_MUTEX_INITIALIZER,同时用pthread_create创建4个线程进行测试,由于此时锁是全局的,我们不需要把锁传给每个线程:
#include <iostream> #include <string> #include <vector> #include <unistd.h> #include <pthread.h> using std::cout; using std::endl; #include <iostream> #include <string> #include <vector> #include <unistd.h> #include <pthread.h> using std::cout; using std::endl; int tickets = 1000; pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; void* get_ticket(void* args) { std::string username = static_cast<const char *>(args); while(true) { pthread_mutex_lock(&lock); if(tickets>0) { usleep(11111); cout<<username<<"正在抢票 : "<<tickets<<endl; tickets--; pthread_mutex_unlock(&lock); } else { pthread_mutex_unlock(&lock); break; } } return nullptr; } int main() { pthread_t t1, t2, t3, t4; pthread_create(&t1, nullptr, get_ticket, (void *)"thread 1"); pthread_create(&t2, nullptr, get_ticket, (void *)"thread 2"); pthread_create(&t3, nullptr, get_ticket, (void *)"thread 3"); pthread_create(&t4, nullptr, get_ticket, (void *)"thread 4"); pthread_join(t1, nullptr); pthread_join(t2, nullptr); pthread_join(t3, nullptr); pthread_join(t4, nullptr); return 0; }
局部锁的使用:
局部锁+for循环创建线程的代码,此时的锁是局部的,为了把锁传递给每个线程,我们可以定义一个结构体ThreadData,存放着线程名与锁:
#include <iostream> #include <string> #include <vector> #include <unistd.h> #include <pthread.h> using std::cout; using std::endl; int tickets = 1000; class ThreadData { public: ThreadData(const std::string&threadname,pthread_mutex_t *mutex_p) :threadname_(threadname),mutex_p_(mutex_p) {} ~ThreadData(){} public: std::string threadname_; pthread_mutex_t *mutex_p_; }; void* get_ticket(void* args) { ThreadData*td = static_cast<ThreadData *>(args); while(true) { pthread_mutex_lock(td->mutex_p_); if(tickets>0) { usleep(11111); cout<<td->threadname_<<"正在抢票 : "<<tickets<<endl; tickets--; pthread_mutex_unlock(td->mutex_p_); } else { pthread_mutex_unlock(td->mutex_p_); break;//注意这里有break } } return nullptr; } int main() { #define NUM 4 pthread_mutex_t lock; pthread_mutex_init(&lock,nullptr); std::vector<pthread_t> tids(NUM); for(int i =0;i<NUM;i++) { char buffer[64]; snprintf(buffer,sizeof(buffer),"thread %d",i+1); ThreadData *td = new ThreadData(buffer,&lock); pthread_create(&tids[i],nullptr,get_ticket,td); } for(const auto&tid:tids) { pthread_join(tid,nullptr); } pthread_mutex_destroy(&lock); return 0; }
总结分析:
此时的运行结果每次都是能够减到1,但是运行的速度也变慢了。这是因为加锁和加锁的过程是多个线程串行执行的,程序变慢了,同时这里看到每次都是只有一个线程在抢票,这是因为锁只规定互斥访问,并没有规定谁来优先执行所以谁的竞争力强就谁来持有锁。要想解决这个问题:想想抢完票就结束了吗?实际的生活当中,抢完票后还有一些工作需要完成:比如发送订单
💫 Mutex.hpp——mutex的封装
如果我们想简单的使用,该如何进行封装设计 ——做一个简单设计,传入一个锁自动帮我们加锁和解锁,RAII风格加锁:
Mutex.hpp:
//Mutex.hpp #pragma once #include <iostream> #include <pthread.h> class Mutex { public: Mutex(pthread_mutex_t *lock_p=nullptr):lock_p_(lock_p) {} void lock() { if(lock_p_) pthread_mutex_lock(lock_p_); } void unlock() { if(lock_p_) pthread_mutex_unlock(lock_p_); } ~Mutex() { } private: pthread_mutex_t *lock_p_; }; class LockGuard { public: LockGuard(pthread_mutex_t *mutex):mutex_(mutex) { mutex_.lock();//在构造函数中加锁 } ~LockGuard() { mutex_.unlock();//在析构函数中解锁 } private: Mutex mutex_; };
main.cc:
#include <iostream> #include <string> #include <vector> #include <unistd.h> #include <pthread.h> #include "Mutex.hpp" using std::cout; using std::endl; int tickets = 1000; pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; void *get_ticket(void *args) { std::string username = static_cast<const char *>(args); while (true) { {//代码块,不给usleep加锁 LockGuard lockguard(&lock); if (tickets > 0) { usleep(1111); cout << username << "正在抢票 : " << tickets << endl; tickets--; } else { break; } } usleep(1000); } return nullptr; } int main() { pthread_t t1, t2, t3, t4; pthread_create(&t1, nullptr, get_ticket, (void *)"thread 1"); pthread_create(&t2, nullptr, get_ticket, (void *)"thread 2"); pthread_create(&t3, nullptr, get_ticket, (void *)"thread 3"); pthread_create(&t4, nullptr, get_ticket, (void *)"thread 4"); pthread_join(t1, nullptr); pthread_join(t2, nullptr); pthread_join(t3, nullptr); pthread_join(t4, nullptr); pthread_join(t4, nullptr); return 0; }
💫 可重入函数&线程安全
线程安全:
- 在多个执行流中对同一个临界资源进行操作访问, 而不会造成数据二义性 .
- 也就是说, 在拥有共享数据的多条线程并行执行的程序中, 通过同步和互斥保证各个线程都可以正常且正确的执行, 不会出现数据污染等意外情况, 也就是保证了线程安全 .
重入 :
同一个函数被不同的执行流调用, 当前一个流程还没有执行完, 就有其他的执行流再次进入, 我们称之为重入. 一个函数在重入的情况下, 运行结果不会出现任何不同或者任何问题, 则该函数被称为可重入函数, 否则, 是不可重入函数.
- 调用了 malloc / free 函数, 因为malloc函数是用全局链表来管理堆的.
- 调用了标准I/O库函数, 标准I/O库的很多实现都以不可重入的方式使用全局数据结构.
- 可重入函数体内使用了静态的数据结构.
- 函数是可重入的, 那就是线程安全的 .
- 线程安全不一定是可重入的, 而可重入函数则一定是线程安全的 .
💫 死锁
概念:
多个执行流在对多个锁资源进程争抢操作时, 因为推进顺序不当, 而导致互相等待, 流程无法继续推进的情况.
产生死锁的四个必要条件(要产生死锁, 下面条件缺一不可),死锁这部分内容中所说的执行流可以是进程也可以是线程
- 互斥条件:一个资源每次只能被一个执行流使用
- 请求与保持条件:一个执行流因请求资源而阻塞时, 对已获得的资源保持不放(一个执行流拿着A锁请求B锁, 没有请求到B锁,阻塞等待时并没有释放A锁, 属于占着xx不拉x)
- 不可剥夺条件: 一个执行流已获得的资源, 在末使用完之前, 不能强行剥夺(一个执行流加的锁, 其他的执行流不能解锁)
- 循环等待条件: 若干执行流之间形成一种头尾相接的循环等待资源的关系避免 (一个执行流拿着A锁去请求B锁, 另一个执行流拿着B锁请求A锁, 谁也拿不到谁的.)
预防死锁(从三个必要条件入手)
- 加锁顺序一致 -- 破坏循环等待条件(当多个执行流的加锁顺序一致时, 就不会产生上面所说的循环等待条件了)
- 避免锁未释放的场景 -- 破坏了请求与保持条件(如果一个执行流拿着A锁, 请求B锁失败, 则该执行流在阻塞等待前必需将所有已经拿到的锁全部解锁)
- 资源一次性分配 -- 破坏不可剥夺条件(创建执行流时, 要求它申请所需的全部资源, 要么全都获取到, 要么一个也不给)
避免死锁的方法:
银行家算法(有效的避免死锁), 死锁检测算法(检测出死锁后解除死锁)
🌙Linux线程同步
概念引入:
引入一些情景:自习室VIP,先到先得,上厕所时反锁,别人进不去,离资源近竞争力强,一直是你自己,重复放钥匙拿钥匙,造成其他人饥饿状态;再比如抢票系统我们看到一个线程一直连续抢票,造成了其他线程的饥饿,为了解决这个问题:我们在数据安全的情况下让这些线程按照一定的顺序进行访问,这就是线程同步。
- 饥饿状态:得不到锁资源而无法访问公共资源的线程处于饥饿状态。但是并没有错,但是不合理。
- 竞态条件:因为时序问题,而导致程序异常,我们称为竞态条件。
- 线程同步:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,叫做同步。
💫 条件变量
概念:
当一个线程互斥地访问某个变量时,它可能发现在其他线程改变状态之前,它什么也做不了,例如一个线程访问队列时,发现队列为空,它只能等待,直到其他线程将一个节点添加到队列中。这种情况就需要用到条件变量。
条件变量通常需要配合互斥锁一起使用。
条件变量的使用:
一个线程等待条件变量的条件成立而被挂起;另一个线程使条件成立后唤醒等待的线程。
💫 条件变量接口
条件变量类型:
pthread_cond_t 类型, 是一个结构体。
初始化:
- 静态初始化 : pthread_cond_t cont = PTHREAD_COND_INITIALIZER ; //不需要销毁
- 动态初始化 : pthread_cond_init( pthread_cond_t* restrict cond, const pthread_condattr_t* restrict attr) //需要释放销毁(因为在堆上, 用完后需要用函数pthread_mutex_destroy来销毁)
- 功能 : 初始化条件变量
参数 :
- cond : 在这个条件变量上等待
- attr : 条件的属性, 通常传入NULL,传入NULL为默认属性
- restrict关键字 : 只用于限制指针, 告诉编译器, 所有修改该指针指向内存中内容的操作, 只能通过本指针完成. 不能通过除本指针以外的其他变量或指针修改.
- 返回值 : 成功返回0, 失败返回值>0, 返回的是错误码errno
等待:
- 原型 : pthread_cond_wait(pthread_cond_t* restrict cond, pthread_mutex_t* restrict mutex)
- 功能 : 等待条件满足,完成了三步操作: 解锁 -> 等待(加入等待的PCB队列) -> 被唤醒后重新加锁. (其中解锁和休眠是一个原子操作)其中, 解锁是为了让资源可以被别的执行流访问(其他执行流可能会产生可用资源), 当可用资源产生后, 再唤醒这个执 行流, 唤醒之后需要保证操作的原子性, 又要加锁.
- 参数 :
- cond : 在这个条件变量上等待
- mutex : 给判断条件加的互斥锁
- 返回值 : 成功返回0, 失败返回值>0, 返回的是错误码errno
唤醒:
- 原型 : pthread_cond_signal(pthread_cond_t* restrict cond)
- 功能 : 唤醒至少一个条件变量等待队列中的执行流(可能唤醒一个, 也可能是多个)
- 参数 :cond : 在这个条件变量上等待
- 返回值 : 成功返回0, 失败返回值>0, 返回的是错误码errno
- 原型 : pthread_cond_broadcast(pthread_cond_t* restrict cond)
- 功能 : 广播唤醒所有条件变量等待队列中的执行流
- 参数 :cond : 在这个条件变量上等待
- 返回值 : 成功返回0, 失败返回值>0, 返回的是错误码errno
销毁:
- 原型 : pthread_cond_destroy(pthread_cond_t* cond)
- 功能 : 在条件变量不再使用之后, 销毁释放资源(只针对init函数初始化的条件变量)
- 参数 : 要销毁的条件变量的地址
- 返回值 : 成功返回0, 失败返回值>0, 返回的是错误码errno
💫 理解条件变量
举个例子:公司进行招聘:应聘者要面试,大家不能同时进入房间进行面试,但是没有由于没有组织,上一个人面试完之后,面试官打开门准备面试下一个,一群人在外面等待面试,但是有人抢不过别人,人太多了,面试官记不住谁面试过了,所以有可能一个人面试完之后又去面试了,造成其他人饥饿问题,这时候效率很低,后来hr重新进行管理:设立一个等待区,先排队去等待区进行等待面试,现在每个人都进行排队,都有机会面试了,而这个等待区就是条件变量,如果一个人想面试,先得去排队等待区等待,未来所有应聘者都要去条件变量。
💫 条件变量的使用
一次唤醒一个线程:(创建2个线程,通过条件变量一秒唤醒一个线程(或者全部唤醒))
#include <iostream> #include <string> #include <vector> #include <unistd.h> #include <pthread.h> using namespace std; int tickets = 1000; pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; pthread_cond_t cond = PTHREAD_COND_INITIALIZER; void* start_routine(void* args) { string name = static_cast<const char*>(args); while(true) { pthread_mutex_lock(&mutex); pthread_cond_wait(&cond,&mutex); //判断省略 cout<< name <<" -> "<<tickets<<endl; tickets--; pthread_mutex_unlock(&mutex); } } int main() { pthread_t t1,t2; pthread_create(&t1,nullptr,start_routine,(void*)"thread 1"); pthread_create(&t1,nullptr,start_routine,(void*)"thread 2"); while(true) { sleep(1); pthread_cond_signal(&cond); cout<<"main thread wakeup one thread..."<<endl; } pthread_join(t1,nullptr); pthread_join(t2,nullptr); return 0; }
一次唤醒一大批线程:
#include <iostream> #include <string> #include <vector> #include <unistd.h> #include <pthread.h> using namespace std; int tickets = 1000; pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; pthread_cond_t cond = PTHREAD_COND_INITIALIZER; void* start_routine(void* args) { string name = static_cast<const char*>(args); while(true) { pthread_mutex_lock(&mutex); pthread_cond_wait(&cond,&mutex); //判断省略 cout<<name<<" -> "<<tickets<<endl; tickets--; pthread_mutex_unlock(&mutex); } } int main() { pthread_t t1,t2; pthread_t t[5]; for(int i = 0;i<5;i++) { char*name = new char[64]; snprintf(name,64,"thread %d",i+1); pthread_create(t+i,nullptr,start_routine,name); } while(true) { sleep(1); pthread_cond_broadcast(&cond); cout<<"main thread wakeup one thread..."<<endl; } for(int i = 0;i<5;i++) { pthread_join(t[i],nullptr); } return 0; }
🌟结束语
今天内容就到这里啦,时间过得很快,大家沉下心来好好学习,会有一定的收获的,大家多多坚持,嘻嘻,成功路上注定孤独,因为坚持的人不多。那请大家举起自己的小手给博主一键三连,有你们的支持是我最大的动力💞💞💞,回见。