# 同步与互斥
现代操作系统基本都是多任务操作系统,即同时有大量可调度实体在运行。在多任务操作系统中,同时运行的多个任务可能:
> - 都需要访问/使用同一种资源
> - 多个任务之间有依赖关系,某个任务的运行依赖于另一个任务
这两种情形是多任务编程中遇到的最基本的问题,也是多任务编程中的核心问题,<font color=FF0000>**同步和互斥**</font>就是用于解决这两个问题的。
<font color=FF0000>**互斥**</font>:是指散布在不同任务之间的若干程序片断,当某个任务运行其中一个程序片段时,其它任务就不能运行它们之中的任一程序片段,只能等到该任务运行完这个程序片段后才可以运行。最基本的场景就是:一个公共资源同一时刻只能被一个进程或线程使用,多个进程或线程不能同时使用公共资源。
----------------
<font color=FF0000>**同步**</font>:是指散布在不同任务之间的若干程序片断,它们的运行必须严格按照规定的某种先后次序来运行,这种先后次序依赖于要完成的特定的任务。最基本的场景就是:两个或两个以上的进程或线程在运行过程中协同步调,按预定的先后次序运行。比如 A 任务的运行依赖于 B 任务产生的数据。
举例:内存中有100字节, 线程 T1 欲填入全 1,线程 T2 欲全填入 0.但如果 T1 在执行了 50各字节后失去了 CPU, 此时 T2执行,会将 T1 写过的内容覆盖。当 T1 再次获得 CPU 时,继续从失去 CPU 的位置开始向后写入 1,当执行结束后,内存中的数据就是混乱的。
产生的这种现象叫做:`与时间有关的错误`,为了避免这种数据混乱,线程需要同步,<font color=FF0000>指一个线程发出某一功能调用时,在没有得到结果之前,该调用不返回。同时其它线程为保证数据一致性,不能调用该功能</font>。
`同步`的目的,是为了避免数据混乱,解决与时间有关的错误。实际上,**不仅线程间需要同步,进程间、信号间等等都需要同步机制**。因此,所有<font color=0000FF>“多个控制流,共同操作一个共享资源”</font>的情况,都需要同步,例如 `信号`。
## 数据混乱
数据混乱的原因有:
- 资源共享(独享资源不会混乱)
- 调度随机(对数据的访问出现竞争,随机执行)
- 线程间缺乏必要的同步机制
以上3点中,前两点不能改变,欲提高效率,传递数据,资源必须共享。只要共享资源,就一定会出现竞争。只要存在竞争关系,数据就很容易出现混乱。
所以只能从第三点着手解决。使多个线程在访问共享资源的时候,出现互斥。
# 互斥锁
为什么需要互斥锁?
> 在多任务操作系统中,同时运行的多个任务可能都需要使用同一种资源。这个过程有点类似于,> 公司部门里,我在使用着打印机打印东西的同时(还没有打印完),别人刚好也在此刻使用打印机打印东西,> > 如果不做任何处理的话,打印出来的东西肯定是错乱的。
<H4>这里一份小 demo 可以作为测试,模拟一下上述情况</H4>
```c
#include<stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
void *tun(void *arg){
while(1){
printf("hello ");
sleep(rand() % 3); //模拟长时间操作共享数据,导致 CPU 易主,即打印机同时打印了其他东西
printf("world\n");
sleep(rand() % 3);
}
pthread_exit(0);
}
int main(){
pthread_t tid;
srand(time(NULL));
pthread_create(&tid, NULL, tun, NULL);
while(1){
printf("HELLO ");
sleep(rand()%3);
printf("WORLD\n");
sleep(rand()%3);
}
pthread_join(tid, NULL);
return 0;
}
```
实际上,打印机是有做处理的,我在打印着的时候别人是不允许打印的,只有等我打印结束后别人才允许打印。这个过程有点类似于,把打印机放在一个房间里,给这个房间安把锁,这个锁默认是打开的。当 A 需要打印时,他先过来检查这把锁有没有锁着,没有的话就进去,同时上锁在房间里打印。而在这时,刚好 B 也需要打印,B 同样先检查锁,发现锁是锁住的,他就在门外等着。而当 A 打印结束后,他会开锁出来,这时候 B 才进去上锁打印。
Linux 中提供了一把互斥锁 `mutex`(也称为互斥量)。每个线程在对共享资源操作前都尝试先加锁,成功加锁后才能操作,操作结束后解锁,以便其他线程加锁。在加锁的过程前,资源还是共享的,线程还是竞争的,但是通过 `锁` 就将资源的访问变成互斥操作,而后与时间有关的错误也不会产生了。
<font color=FF0000>注意:同一时刻,只能由一个线程持有该锁</font>
当 A 线程对某个全局变量进行加锁访问,B 在访问前尝试加锁,拿不到锁,B 线程阻塞, C 线程不去加锁,而直接访问该全局变量,依然能够访问,但会出现数据混乱。
所以,互斥锁实质上是操作系统提供的一把 “建议锁”(又称作“协同锁”),建议程序中有多线程访问共享资源的时候使用该机制。但,没有强制限定。
因此,即使有了互斥锁,如果线程不按规则来访问数据,依然会造成数据混乱
## 相关操作
互斥锁的基本操作流程如下:
> 1. 在访问共享资源后临界区域前,对互斥锁进行加锁
> 2. 在访问完成后释放互斥锁导上的锁
> 3. 对互斥锁进行加锁后,任何其他试图再次对互斥锁加锁的线程将会被阻塞,直到锁被释放
互斥锁的数据类型为:`pthread_mutex_t`,本质上是一个结构体,为了简化理解,应用时可以忽略其实现细节,**简单当成整数看待,取值只有 1 和 0 两种**

**头文件:** #include <pthread.h>
**返回值:**
- 成功:返回 0
- 失败:返回 errno
### 初始化互斥锁
**函数原型:** int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr);
**作用:** 初始化一个互斥锁。
**参数:**
- mutex:互斥锁地址
- attr:设置互斥量的属性,通常可采用默认属性,即可将 attr 设为 NULL
> 可以使用宏 `PTHREAD_MUTEX_INITIALIZER` 静态初始化互斥锁,比如:
> `pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;`
> 这种方法等价于使用 `NUL`L 指定的 `attr` 参数调用 `pthread_mutex_init()` 来完成动态初始化,不同之处在于 `PTHREAD_MUTEX_INITIALIZER` 宏不进行错误检查。
此外,成功申请的锁是默认打开的。
### 加锁
**函数原型:** int pthread_mutex_lock(pthread_mutex_t *mutex);
**作用:** 对互斥锁上锁,若互斥锁已经上锁,则调用者一直阻塞,直到互斥锁解锁后再上锁
**参数:**
- mutex:互斥锁地址
`int pthread_mutex_trylock(pthread_mutex_t *mutex);` 尝试加锁,调用该函数时,若互斥锁未加锁,则上锁,返回 0;若互斥锁已加锁,则函数直接返回失败,即 EBUSY,不会阻塞。
### 解锁
**函数原型:** int pthread_mutex_unlock(pthread_mutex_t * mutex);
**作用:** 对指定的互斥锁解锁
**参数:**
- mutex:互斥锁地址
### 销毁互斥锁
**函数原型:** int pthread_mutex_destroy(pthread_mutex_t *mutex);
**作用:** 销毁指定的一个互斥锁。互斥锁在使用完毕后,必须要对互斥锁进行销毁,以释放资源
**参数:**
- mutex:互斥锁地址
通过上述函数,完成,上述错乱的 `demo`
```c
#include<stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
pthread_mutex_t mutex; //创建锁
void *tun(void *arg){
while(1){
pthread_mutex_lock(&mutex); //加锁
printf("hello ");
sleep(rand() % 3);
printf("world\n");
pthread_mutex_unlock(&mutex); //在对共享数据操作结束后立即解锁,保证锁的粒度尽可能的小
sleep(rand() % 3);
}
pthread_exit(0);
}
int main(){
pthread_t tid;
srand(time(NULL));
pthread_mutex_init(&mutex, NULL); //初始化锁
pthread_create(&tid, NULL, tun, NULL);
while(1){
pthread_mutex_lock(&mutex);
printf("HELLO ");
sleep(rand()%3);
printf("WORLD\n");
pthread_mutex_unlock(&mutex);
sleep(rand()%3);
}
pthread_join(tid, NULL);
pthread_mutex_destroy(&mutex);
return 0;
}
```
此时代码已经可以按照期望的逻辑进行打印。
**结论:在访问共享数据前加锁,访问结束后<font color=red>立即解锁</font>,锁的 “粒度” 越小越好**
## 死锁
是使用锁不恰当所产生的现象,通常会出现以下两种情况:
1. 线程试图对同一个互斥锁 A 加锁两次
2. 线程 T1 拥有 A 锁,请求获得 B 锁,线程 T2 拥有 B 锁请求获得 A 锁
```c
#include<stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
pthread_mutex_t mutex;
int main(){
pthread_t tid;
srand(time(NULL));
pthread_mutex_init(&mutex, NULL);
pthread_mutex_lock(&mutex); //第一次加锁
printf("------------First lock\n ");
pthread_mutex_lock(&mutex); //重复加锁,会阻塞在这里
printf("------------Second lock\n ");
pthread_mutex_unlock(&mutex);
pthread_mutex_destroy(&mutex);
return 0;
}
```
```c
#include<stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
pthread_mutex_t mutexA, mutexB; //创建两把锁
int var1 = 0, var2 = 0;
void *tunA(void *arg){
pthread_mutex_lock(&mutexA); //A 线程获得 A 锁
printf("A thread get mutexA\n");
int t = var1;
sleep(5); //挂起,让 B 线程 获得 B 锁
printf("Thread A want get mutexB\n");
pthread_mutex_lock(&mutexB); // A 线程获取 B 锁, 阻塞
printf("Thread A id = %lu, var = %d, ++var = %d", pthread_self(), t, ++var1);
pthread_mutex_unlock(&mutexA); //解锁
usleep(10);
pthread_exit(0);
}
void *tunB(void *arg){
pthread_mutex_lock(&mutexB); // B 线程获得 B 锁
printf("B thread get mutexB\n");
int t = var2;
sleep(5); //挂起, 让 A 线程 获得 A 锁
printf("Thread B want get mutexA\n");
pthread_mutex_lock(&mutexA); // B 线程获取 A 锁, 阻塞
printf("Thread B id = %lu, var = %d, ++var = %d", pthread_self(), t, ++var2);
pthread_mutex_unlock(&mutexB);
usleep(10);
pthread_exit(0);
}
int main(){
pthread_t tid, cid;
srand(time(NULL));
pthread_mutex_init(&mutexA, NULL);
pthread_mutex_init(&mutexB, NULL);
pthread_create(&tid, NULL, tunA, NULL);
pthread_create(&cid, NULL, tunB, NULL);
pthread_join(tid, NULL);
pthread_join(cid, NULL);
pthread_mutex_destroy(&mutexA);
pthread_mutex_destroy(&mutexB);
return 0;
}
```
# 补充
1. 本文代母为了突出代码逻辑均未进行返回值检查,在之际编写中均应添加
Linux系统编程——多任务的同步与互斥