进程间通信告诉我们,两个进程要想进行通信,就必须先看到一份临界资源。而对于临界资源的操作,管道是自带同步与互斥机制的。
假若我们对于一份临界资源不加某种限制的话,两个线程同时对其进行操作时,就可能会发生数据不一致的问题。
今有一全局变量tickets,作为临界资源;有2个线程对于tickets进行操作:tickets++,假设tickets初始为0
thread1 执行tickets++时,会发生以下的事件:
但是我们知道,线程每次将数据放入cpu寄存器并进行运算的时候,都会有一个时间片,如果时间片到了,那么寄存器会保存上下文,该线程进入等待队列,等待下一次的载入寄存器。
当我们的thread1完成了++之后,此时时间片恰好到了,寄存器里的值(1)没有写回到内存,而此时,thread2来了,thread2看到的内存中的值并不是++后的(1)而是++前的(0),所以,thread2从0加到1,并把1写回到内存。
此时thread2时间片到了;thread1来了,thread1恢复上下文,并把1写回到内存。
最终,thread1和thread2虽然各++了一次,但是内存中的tickets值却是1
归根结底,造成这一切的罪魁祸首是thread1 执行cnt++还没结束,就因为时间片到了被切出去了,然后thread2对于临界资源cnt进行了修改
线程切换的时机:内核态返回用户态(比如进行系统调用就会发生内核态用户态的切换)
我们必须引入互斥量,对于临界资源的操作进行一定的限制
互斥量mutex
#include
#include
#include
#define NUM 5
using namespace std;
int tickets = 1000;
void* routine(void* args) {
while(true) {
if(tickets > 0) {
usleep(1000);
printf("thread NO. 0x%x get a ticket, %d left\n", pthread_self(), tickets--);
usleep(1000);
}
else {
printf("0x%x qiut... ticket: %d\n", pthread_self(), tickets);
break;
}
}
return nullptr;
}
int main()
{
pthread_t tids[NUM];
for(int i = 0; i < NUM; i++) {
pthread_create(tids+i, nullptr, routine, nullptr);
}
for(int i = 0; i < NUM; i++) {
pthread_join(tids[i], nullptr);
}
return 0;
}
五个线程不加锁同时抢票,就会发生同时访问临界资源造成操作不一致的问题,票会被抢到负数
所以说,我们要让抢票这一过程变成原子的
做到原子需要满足:
我们直接加锁!
接口:
创建
pthread_mutex_t lock;
初始化
pthread_mutex_init(&lock, nullptr);
加
pthread_mutex_lock(&lock);
解
pthread_mutex_unlock(&lock);
销毁
pthread_mutex_destroy(&lock);
#include
#include
#include
#define NUM 5
using namespace std;
int tickets = 1000;
pthread_mutex_t lock;
struct arg{
int x;
};
void* routine(void* args) {
while(true) {
pthread_mutex_lock(&lock);
if(tickets > 0) {
usleep(1000);
printf("thread NO. %d 0x%x get a ticket, %d left\n", ((arg*)args)->x, pthread_self(), tickets--);
usleep(1000);
pthread_mutex_unlock(&lock);
}
else {
printf("0x%x qiut... ticket: %d\n", pthread_self(), tickets);
pthread_mutex_unlock(&lock);
break;
}
}
return nullptr;
}
int main()
{
pthread_mutex_init(&lock, nullptr);
arg* arg1 = new arg;
arg1->x = 1;
pthread_t tids[NUM];
for(int i = 0; i < NUM; i++) {
pthread_create(tids+i, nullptr, routine, arg1);
}
for(int i = 0; i < NUM; i++) {
pthread_join(tids[i], nullptr);
}
pthread_mutex_destroy(&lock);
return 0;
}
加锁带来的是我们对于临界资源的操作变成原子的:
我们有没有想过,锁既然能被所有线程看到,那么是不是就是说锁本身就是一个临界资源,那么既然如此,也就是说,申请锁/释放锁的操作本身就是原子的。那这是如何实现的呢?
为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。lock和unlock的汇编伪代码:
%al是一个寄存器的值,也就是上下文数据,是线程私有的
mutex是一个内存中的一个存储空间,空间里的值是1
exchange是一条汇编指令,保证加锁的原子性
整个过程中,值为1的mutex只有一份,拿到1的线程才拿到了锁
一条exchange汇编就能完成内存和寄存器的数据的交换
mutex_lock是原子的
我们来看一下如果申请锁的时候分别在1、2、3步线程切出去会怎么样:
调用了malloc/free函数,因为malloc函数是用全局链表来管理堆的
调用了标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构
可重入函数体内使用了静态的数据结构
极端一点,如果捕捉了进程的信号,信号处理函数改为申请锁,那么你发送信号时若锁还未释放,信号处理函数又申请了锁,就会死锁
当线程向系统申请资源时,系统必须判断如果分配了资源,会不会导致资源不安全的状态;会,就不分配;否则就分配
检查状态是否安全的方法是看是否有足够的资源满足一个距最大需求最近的进程
如果可以,则认为这些资源是可以收回的,然后检查下一个距最大需求最近的进程,如此反复下去。如果所有资源最终都被收回,则该状态是安全的,最初的申请可以满足
对于临界区进行保护,所有的执行线程都必须遵守这个规则
lock->访问临界区->unlock
加锁的前提是所有的线程必须先看到同一把锁,锁本身就是临界资源->锁本身要保证自身安全
申请锁的过程,不能有中间状态,也就是两态,lock->原子性
一次保证只有一个线程进入临界区访问临界资源,就叫做互斥!