Linux多线程(8.3 线程同步与互斥)

3. 线程的同步与互斥

为什么需要同步与互斥

​ 一个进程运行时,数据存储在内存中。如果一个数据要进行运算,必须先将数据拷贝到寄存器中。比如要对栈上的一个int i进行“++”操作,需要将i的值拷贝到寄存器中,将该值自加后再拷贝到原来的内存。

​ 如果此时有两个线程均进行的是这样的操作,可能出现两个线程都拷贝了i原来的值到寄存器,然后各种加一,再拷贝到i对应内存的情况,最终导致i这个变量只自加了一次。

(个人理解:写数据的过程为:

  1. 把内存中的i拷贝进寄存器;
  2. i++;
  3. 读取寄存器中的i到内存;

两个线程同时写产生的问题:

  1. A线程把i拷贝进寄存器,i++;
  2. 此时B线程又把内存中的i拷贝进了寄存器,覆写了原本寄存器中已经+1的i,B线程执行i++操作;
  3. A、B线程分别读取寄存器中的i;

此时的i就只自加了一次。

线程的同步与互斥的三种机制

  • 互斥量
  • 信号量
  • 条件变量

3.1 互斥量

(1)原子性

​ 如果一个线程锁定了一个互斥量,那么临界区内的操作要么全部完成,要么一个也不执行。

(2)唯一性

​ 需要注意的问题:有可能上锁后忘记解锁;或者在解锁语句前程序抛出异常,导致无法解锁。

​ 在c++中,lock()之后容易忘记unlock(),与忘记释放指针所指堆内存空间导致内存泄露的情况类似。于是与智能指针类似,也有了lock_guard,用来防止开发人员忘了解锁。

lock_guard:

(3)非繁忙等待

3.2 互斥量使用

互斥量使用过程如下

(1)互斥量声明与初始化

(2)互斥量加锁

​ C语言中,通过系统调用pthread_mutex_lock对互斥量加锁。

(3)互斥量判断是否加锁

​ pthread_mutex_trylock()用于判断线程是否加锁,语义与pthread_mutex_lock类似,不同的是在锁被占用时返回EBUSY错误而不是挂起等待。

(4)访问共享资源

(5)互斥量解锁

​ pthread_mutex_unlock调用对指定的互斥量解锁。

(6)互斥量销毁

​ pthread_mutex_destroy()用于注销一个互斥量。

3.3 信号量

​ 信号量可以实现线程的同步与互斥(设置不同的初始值,例如同步设为0,互斥设为1),其本质就是P/V操作,

也就是wait(S)和signal(S)这两个原语。

1. 记录型信号量

value:代表资源数目的整型变量;

L:进程链表,用于链接所有等待该资源的进程。

记录型信号量可描述为:

typedef struct{
  int value;
  struct process *L;
}semaphore;

相应的wait(S)和signal(S)操作如下:

void wait(semaphore S)	//相当于申请资源
{
    S.value--;
    if(S.value < 0)
    {
        add this process to S.L;
        block(S.L); //block原语,进行自我阻塞,放弃处理机
    }
}
void signal(semaphore S)	//相当于释放资源
{
    S.value++; //释放一个进程,使可供分配的资源数+1
    if(S.value <= 0)	//若+1后仍是S.value <= 0,则表示在S.L中仍有等待该资源的进程被阻塞
    {
        remove a process P from S.L;
        wakeup(P); //wakeup原语,唤醒S.L中的第一个等待进程
    }
}

2. 利用信号量实现同步

设S为进程P1,P2的公共信号量,初值为0

进程P2中的y语句要使用P1中x语句的运行结果,所以只有当x执行完毕后y才可以执行。

实现进程同步的算法如下:

semaphore S = 0;	//初始化信号量
P1()
{
    x;	//语句x
    V(S);	//告诉进程P2,语句x已经完成
    ...
}
P2()
{
    ...
    P(S);	//检查语句x是否执行完成
    y;	//检查无误,运行y语句
    ...
}

3. 利用信号量实现进程互斥

设S为进程P1,P2的公共信号量,初值为1

实现进程互斥的算法如下:

semaphore S = 1;	//初始化信号量
P1()
{
    ...
    P(S);	//准备开始访问临界区,加锁
    进程P1的临界区;
    V(S);	//访问结束,解锁
    ...
}
P2()
{
    ...
    P(S);	//准备开始访问临界区,加锁
    进程P2的临界区;
    V(S);	//访问结束,解锁
    ...
}

3.4 信号量的使用方法

(1)信号量初始化

(2)信号量的P操作

​ 如果信号量的value值为0,则阻塞当前线程;若不为0,则将value减1。

(3)信号量的V操作

(4)信号量的销毁

(5)信号量的其它调用

posted @ 2023-05-17 21:11  曹剑雨  阅读(86)  评论(0编辑  收藏  举报