前两天学习了进程和线程的相关内容,今天来学习线程间的通信。
多线程程序是指多个线程跑在同一个进程里面,所有线程共享进程的全局变量、堆、文件描述符。按理来说,线程之间直接读写全局变量就能交换数据,看着很方便,但这里藏着很大的问题。
一、为什么不能随便直接用全局变量通信
多个线程同时读写同一个全局变量,就会产生资源竞争。
举个简单例子,一个变量count,线程A做count++,线程B也做count++。
count++并不是CPU一条指令直接完成,它分为:读变量、自增、写回内存三步。
如果两个线程刚好在中间互相打断,最后算出来的结果就会出错,这就是竞态问题。
原子操作:不能被CPU任务调度打断的最小操作,一步执行完,中途不会切走别的线程。我们的普通i++不是原子操作。
临界区:加锁和解锁中间包裹的那一段代码。这一块代码,同一时间只允许一个线程进去执行,别的线程要排队等待。
想要安全访问共享资源,就需要互斥锁。
二、互斥锁 pthread_mutex
互斥锁,作用就是保护临界区。谁拿到锁,谁才能执行临界区代码;没拿到锁的线程阻塞等待。
四个核心函数
-
pthread_mutex_init
初始化互斥锁。
参数:锁的地址,第二个attr填NULL使用默认属性。
返回0代表成功,失败返回非0。
-
pthread_mutex_destroy
销毁互斥锁。程序结束前要销毁,释放锁占用的资源。
-
pthread_mutex_lock
上锁。如果锁已经被别人拿走,当前线程就阻塞在这里,一直等到锁被释放。
-
pthread_mutex_unlock
解锁。用完共享资源一定要解锁,不然别的线程会一直卡死等待。
使用简单流程
pthread_mutex_init 初始化锁
线程内:
pthread_mutex_lock();
// 临界区,操作共享全局变量
pthread_mutex_unlock();
pthread_mutex_destroy(); // 最后销毁
注意:lock和unlock必须成对。中途return、break跳过unlock,就会死锁。
三、死锁
死锁:多个线程互相拿着对方需要的锁,谁都不肯释放,所有线程全部卡住不动,程序彻底僵住。
死锁四个必要条件
-
互斥条件:资源同一时刻只能一个线程使用
-
不可剥夺条件:已经拿到的锁,不能被别的线程强行抢走
-
请求保持:线程已经拿着锁,又去申请别的锁,原有锁不释放
-
循环等待:线程A拿锁1等锁2;线程B拿锁2等锁1,形成循环
四个条件全部满足,就会发生死锁。破坏任意一个,就可以避免死锁。
如何避免死锁?
-
使用pthread_mutex_trylock,尝试拿锁,拿不到不会阻塞,可以直接返回,避免卡死。
-
多把锁的时候,所有线程加锁顺序保持一致。大家都先拿锁A,再拿锁B,就不会出现循环等待。
四、信号量 sem
信号量可以把资源看成一个计数器,不仅仅能做互斥,还可以做线程同步。
资源是有限的,信号量就用来记录可用资源数量,支持初始化、销毁、申请、释放。
四个核心函数
- sem_init:初始化信号量
int sem_init(sem_t *sem, int pshared, unsigned int value);
• sem:信号量变量地址
• pshared:0代表同一个进程内部线程共享;非0代表进程之间共享
• value:信号量初始值
-
sem_destroy:销毁信号量,释放资源
-
sem_wait:申请资源(P操作)
资源计数大于0,计数减一,直接往下执行;
资源等于0,线程阻塞等待,直到有资源释放出来。
-
sem_post:释放资源(V操作)
信号量计数+1,唤醒正在等待这个信号量的线程。
小区别:
互斥锁:同一时间只能一个线程进入临界区。
信号量:初始值可以设置大于1,可以允许多个线程同时获取资源。
五、总结
-
互斥锁(mutex):主要用于互斥访问,保护临界区,锁只有两种状态:上锁、解锁。
-
信号量(sem):既可以实现互斥,更擅长做同步,控制线程执行先后顺序,自带计数器。
互斥锁、信号量,只解决同一个进程内部线程之间通信。
进程之间通信,要用管道、FIFO、消息队列这些,进程之间虚拟地址互相隔离,全局变量不能直接互通。