【Linux】线程安全-同步与互斥

线程安全:多个线程执行流对临界资源的不安全争抢操作

实现:如何让线程之间安全对临界资源进行操作就是同步与互斥

互斥:同一时间临界资源的唯一访问性

mutex(互斥量)

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

我们先来看一个例子:操作共享变量的售票系统。

#include <stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>
#include<string.h>


int ticket = 100;
void* route(void* arg)
{
    int id = (int)arg;
    while(1)
    {
        if(ticket > 0)
        {
            usleep(1000);
            printf("pthread %d -> %d\n", id, ticket);
            ticket--;
        }
        else
            break;
    }

    return NULL;
}



int main()
{
    pthread_t tid[4];
    int i, ret;

    for(i = 0; i < 4; ++i)
    {
        ret = pthread_create(&tid[i], NULL, route, (void*)i);
        if(ret != 0)
            return -1;
    }

    for(i = 0; i < 4; ++i)
        pthread_join(tid[i], NULL);
    return 0;
}

很明显,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_t *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_ lock 时,可能会遇到以下情况:

  • 互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功
  • 发起函数调⽤时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么pthread_ lock调⽤会陷⼊阻塞,等待互斥量解锁。

改进上面的售票系统

#include <stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>
#include<string.h>


int ticket = 100;
pthread_mutex_t mutex;
void* route(void* arg)
{
    int id = (int)arg;
    while(1)
    {
        pthread_mutex_lock(&mutex);
        if(ticket > 0)
        {
            usleep(1000);
            printf("pthread %d -> %d\n", id, ticket);
            ticket--;
        }
        else
        {
            pthread_mutex_unlock(&mutex);
            break;
        }
        pthread_mutex_unlock(&mutex);
    }

    return NULL;
}



int main()
{
    pthread_t tid[4];
    int i, ret;

    pthread_mutex_init(&mutex, NULL);
    for(i = 0; i < 4; ++i)
    {
        ret = pthread_create(&tid[i], NULL, route, (void*)i);
        if(ret != 0)
            return -1;
    }

    for(i = 0; i < 4; ++i)
        pthread_join(tid[i], NULL);

    pthread_mutex_destroy(&mutex);
    return 0;
}

很明显,加锁之后线程安全得到了保证。至于只有一个线程访问,这与时间片有关。

死锁:

产生死锁的原因主要是:
(1) 因为系统资源不足。
(2) 进程运行推进的顺序不合适。
(3) 资源分配不当等。
如果系统资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则就会因争夺有限的资源而陷入死锁。其次,进程运行推进顺序与速度不同,也可能产生死锁。
产生死锁的四个必要条件:
(1) 互斥条件:一个资源每次只能被一个进程使用。
(2) 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
(3) 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
(4) 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁。
死锁的解除与预防:
理解了死锁的原因,尤其是产生死锁的四个必要条件,就可以最大可能地避免、预防和
解除死锁。所以,在系统设计、进程调度等方面注意如何不让这四个必要条件成立,如何确
定资源的合理分配算法,避免进程永久占据系统资源。此外,也要防止进程在处于等待状态
的情况下占用资源。因此,对资源的分配要给予合理的规划。

避免死锁:银行家算法,死锁检测算法

同步:对临界资源操作的时序可控性

条件变量:等待、唤醒

条件变量一共提供了两个功能,一个是等待,一个是唤醒

对于一个外部条件进行判断,如果条件满足则继续操作;如果条件不满足怎等待

为了能够让程序继续操作,需要其他执行流修改条件,是满足条件,并唤醒对方

这里所说的外部条件,条件变量不会提供是我们用户设置的判断依据

条件变量

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

条件变量函数

初始化

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);

示例:

#include <stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<pthread.h>

pthread_cond_t sale;
pthread_cond_t eat;
pthread_mutex_t mutex;
int have_noodle = 0;

void* sale_noodle(void* arg)
{
    int id = (int)arg;
    while(1)
    {
        pthread_mutex_lock(&mutex);
        if(have_noodle == 1)
            pthread_cond_wait(&sale, &mutex);
        printf("pthread %d create noodle!!\n", id);
        have_noodle = 1;

        //生产出来后通知买方便面的人
        pthread_cond_signal(&eat);
        pthread_mutex_unlock(&mutex);

    }

    return NULL;
}

void* eat_noodle(void* arg)
{
    while(1)
    {
        //因为have_noodle的操作也是一个临界资源的操作,因此需要被
        //保护,使用互斥锁进行保护
        pthread_mutex_lock(&mutex);
        if (have_noodle == 0) 
        {
            //因为等待时间不确定,因此有可能会浪费很多等待时间
            //因此使用环境变量提供的死等操作,但是这个死等需要能够
            // 被唤醒,这样的话,一旦方便面生产出来直接唤醒我们的死
            //等,不会浪费多余的等待时间
            //防止不满足条件陷入休眠,没有解锁,对方无法获取锁,没
            //办法生产方便面,因此需要解锁,
            //但是解锁和休眠必须是原子操作
             pthread_cond_wait(&eat, &mutex);
            //被唤醒,这时候可以继续吃面,并且修改条件,但是条件是
            //临界资源,因此需要加锁,
            // pthread_cond_wait整体操作
            // 解锁-》休眠-》被唤醒后加锁(但是这不是一个阻塞操作,
            //而是直接计数器置0)

        }
        printf("eat noodle!! good!!\n");
        have_noodle = 0;
        pthread_mutex_unlock(&mutex);
        //吃完之后通知一下卖方便面的
        pthread_cond_signal(&sale);
    }
}

int main()
{
    pthread_t tid1, tid2;
    int ret;

    //条件变量初始化
    pthread_cond_init(&eat, NULL);
    pthread_cond_init(&sale, NULL);
    pthread_mutex_init(&mutex, NULL);

    ret = pthread_create(&tid1, NULL, sale_noodle, (void*)1);
    if(ret != 0)
        return -1;
    ret = pthread_create(&tid1, NULL, sale_noodle, (void*)2);
    if(ret != 0)
        return -1;
    ret = pthread_create(&tid2, NULL, eat_noodle, NULL);
    if(ret != 0)
        return -1;

    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);

    //条件变量销毁
    pthread_cond_destroy(&eat);
    pthread_cond_destroy(&sale);
    pthread_mutex_destroy(&mutex);
    return 0;
}

为什么pthread_ cond_ wait 需要互斥量?

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

按照上⾯的说法,我们设计出如下的代码:先上锁,发现条件不满⾜,解锁,然后等待在条件变量上不就⾏了,如下代码:

// 错误的设计
 pthread_mutex_lock(&mutex);
 while (condition_is_false) {
 pthread_mutex_unlock(&mutex);
 //解锁之后,等待之前,条件可能已经满⾜,信号已经发出,但是该信号可能被错过
 pthread_cond_wait(&cond);
 pthread_mutex_lock(&mutex);
 }
 pthread_mutex_unlock(&unlock);
  • 由于解锁和等待不是原⼦操作。调⽤解锁之后,pthread_ cond_ wait之前,如果已经有其他线程获取到互斥量,摒弃条件满⾜,发送了信号,那么pthread_ cond_ wait将错过这个信号,可能会导致线程永远阻塞在这个pthread_ cond_ wait。所以解锁和等待必须是⼀个原⼦操作。
  • nt pthread_ cond_ wait(pthread_ cond_ t *cond,pthread_ mutex_ t * mutex); 进⼊该函数后,会去看条件量等于0不?等于,就把互斥量变成1,直到cond_ wait返回,把条件量改成1,把互斥量恢复成原样。

条件变量使⽤规范

  • 等待条件代码
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);

猜你喜欢

转载自blog.csdn.net/yulong__li/article/details/85055692