Linux线程的同步与互斥

Linux线程互斥

我们先来谈谈进程线程间互斥相关背景概念:

  • 临界资源:多线程执行流共享的资源叫做临界资源。
  • 临界区:每个线程内部访问临界资源的代码叫做临界区。
  • 互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用。
  • 原子性:不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成。

互斥量

大部分情况下,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个线程,其他线程无法获得这种变量。但有时候很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据共享,完成线程之间的交互。多个线程并发的操作共享变量会带来一些问题。

我们可以写一个售票系统看一下:

运行结果:

....

从结果可以看出,还出现了ticket为负数的情况。这是为什么呢?

代码中if语句判断条件为真以后,代码并发的切换到其他线程。usleep模拟漫长的业务过程,在这个过程中,可能有很多线程会进入该代码段。并且ticket操作本身就不是一个原子操作,而是对应三条汇编指令:

  • load:将共享变量ticket从内存加载到寄存器中
  • update:更新寄存器里面的值,执行-1操作
  • store:将新值从寄存器写回到共享变量ticket的内存地址中

因此要解决以上问题,需要做到三点:

  • 代码必须要有互斥行为:当代码进入临界区执行时不允许其他线程进入该临界区。
  • 如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区。
  • 如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。

其实这三点要求本质上就是需要一把锁,Linux中把这把锁叫互斥量。锁资源也是临界资源,锁资源保证原子性。

互斥量的接口

初始化互斥量

初始化互斥量的两种方法:

  • ,方法1,静态分配

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER

  • 方法2,动态分配

int pthread_mutex_init(pthread_mutex *restrict mutex,const pthread_mutexattr_t *restrict attr);

参数:

       mutex:要初始化的互斥量

      attr:NULL

销毁互斥量

需要注意的是:

  • 使用PTHREAD_MUTEX_INITIALIZER初始化的互斥量不需要销毁
  • 不要销毁一个已经加锁的互斥量
  • 已经销毁的互斥量要确保后面不会有线程尝试加锁

销毁函数:

int pthread_mutex_destroy(pthread_mutex_t *mutex);

互斥量的加锁和解锁函数:

int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t * mutex);
返回值:成功返回0,失败返回错误号

调用pthread_mutex_lock时,可能遇到的情况:

  • 互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功。
  • 发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么调用此函数时会被阻塞(执行流被挂起),等待互斥量解锁。

在上面的代码基础上加上互斥锁

通过上面的研究,我们发现,单纯的i++和i--都不是原子的,有可能会有数据不一致的问题存在。所以为了实现互斥锁操作,大多数系统结构都提供了swap或exchange指令,该指令的作用是把寄存器和内存单元的数据进行交换。由于只有一条指令,保证了原子性。即使是多处理平台,访问内存的总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。

lock和unloc的伪代码如下:

lock:
    movb $0,%a1
    xchgb %a1,mutex
    if(a1寄存器的内容>0)
    {
        return 0;
    }
    else
    {
        挂起等待;
    }
    goto lock;

unlock:
    movb $1,mutex
    唤醒等待mutex的线程;
    return 0;

可重入与线程安全

线程安全:多个线程并发同一段代码时,不会出现不同的结果。出现线程安全问题的情况一般是:对全局变量或者静态变量进行操作,并且没有锁保护。

重入:同一函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,称为不可重入函数。

常见的线程不安全的情况

  • 不保护共享变量的函数
  • 函数状态随着被调用,状态发生变化的函数
  • 返回指向静态变量指针的函数
  • 调用线程不安全函数的函数

常见的线程安全的情况

  • 每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的。
  • 类或者接口对于线程来说都是原子操作。
  • 多个线程之间的切换不会导致该接口的执行结果存在二义性。

常见不可重入的情况

  • 调用malloc/free函数,因为malloc函数是用全局链表来管理堆的
  • 调用了标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构
  • 可重入函数体内使用了静态的数据结构

常见的可重入函数

  • 不使用全局变量或静态变量
  • 不使用malloc或new开辟的空间
  • 不调用不可重入函数
  • 不返回静态或全局数据,所有数据都有函数的调用者提供
  • 使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据

可重入函数与线程安全的联系

  • 函数是可重入的,那就是线程安全的
  • 函数是不可重入的,就不能由多个线程使用,不然就可能引发线程安全问题
  • 如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的

可重入函数与线程安全的区别

  • 可重入函数是线程安全的一种
  • 线程安全不一定是可重入的,我们可以使用一些手段将不可重入的函数变成线程安全,可重入函数一定是线程安全的。
  • 如果对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个函数是线程安全的,但这个重入的函数若锁还未释放则会产生死锁,因此是不可重入的。

死锁的相关概念

死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所占用不会释放的资源而处于一种永久等待的状态。

死锁的四个必要条件

  • 互斥条件:一个资源每次只能被一个执行流使用
  • 请求与保持条件:一个执行流因请求资源而阻塞,对已获得的资源保持不放
  • 不剥夺条件:一个执行流已获得的资源,在未使用完之前,不能强行剥夺
  • 循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系

怎样避免死锁

  • 破坏死锁的四个必要条件
  • 加锁顺序一致
  • 避免锁未释放的场景
  • 资源一次性分配

避免死锁的算法

  • 死锁检测算法
  • 银行家算法

Linux线程同步

通常情况下,互斥是为了保证安全性,同步是为了保持合理性

条件变量​​​​​​:当一个线程互斥地访问某个变量时,它可以发现在其它线程改变状态之前,它什么也做不了。例如一个线程访问队列时,发现队列为空,它只能等待,直到其他线程将一个节点添加到队列中,这种情况就要用到条件变量。

条件变量是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:一个线程等待"条件变量的条件成立"而挂起;另一个线程使"条件成立"(给出条件成立信号)。为了防止竞争,条件变量的使用总是和一个互斥锁结合在一起。

同步与竞态条件

  • 同步:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,这种机制叫做同步。
  • 竞态条件:当两个线程竞争同一资源时,如果对资源的访问顺序敏感,就称存在竞态条件

条件变量函数的初始化

int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
参数:
    cond:要初始化的条件变量
    attr:NULL

条件变量函数的销毁

int pthread_cond_destroy(pthread_cond_t *cond);

等待条件满足

int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
参数:
    cond:要在这个条件变量上等待
    mutex:互斥量

唤醒等待

int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);

为什么pthread_cond_wait需要互斥量?

为了使wait之前释放互斥锁

  • 条件等待是线程间同步的一种手段,如果只有一个线程,条件不满足,一直等下去都不会满足,所以必须有一个线程通过某些操作,改变共享变量,使原先不满足条件的变得满足,并且友好的通知等待在条件变量上的线程。
  • 条件不会无缘无故的变得满足,必然会牵扯到共享数据的变化。所以一定要用互斥锁来保护。没有互斥锁就无法安全的获取和修改共享数据。

由于解锁和等待不是原子操作,调用解锁之后,调用pthread_cond_wait之前,如果已经有其他线程获取到互斥量,摒弃条件满足,发送了信号,那么pthread_cond_wait将会错过这个信号,可能会导致线程永远阻塞在pthread_cond_wait。所以解锁和等待必须是一个原子操作。

因此条件变量使用的顺序应该是:先解锁,然后等待,当条件满足是再解锁

条件变量使用规范

  • 等待条件代码
pthread_mutex_lock(&mutex);
while(条件为假)
{
    pthread_cond_wait(&cond,&mutex);
}
修改条件
pthread_mutex_unlock(&mutex);
  • 给条件发送信号代码
pthread_mutex_lock(&mutex);
设置条件为真
pthread_cond_signal(cond);
pthread_mutex_unlock(&mutex);
发布了119 篇原创文章 · 获赞 17 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/tangya3158613488/article/details/102767803