Linux——线程深度剖析(二),拿下线程安全

本文深入探讨Linux线程中的互斥与同步,介绍临界资源、临界区和线程安全概念。通过互斥锁确保线程安全,详细解析加锁、解锁操作,并通过黄牛抢票的实例说明实际应用。同时,阐述条件变量在PCB等待队列中的作用,展示其在消费者-生产者模型中的应用。
摘要由CSDN通过智能技术生成

互斥与同步

  • 互斥是指散布在不同进程之间的若干程序片断,当某个进程运行其中一个程序片段时,其它进程就不能运行它 们之中的任一程序片段,只能等到该进程运行完这个程序片段后才可以运行。

  • 同步是散布在不同进程之间的若干程序片断,它们的运行必须严格按照规定的 某种先后次序来运行,这种先后次序依赖于要完成的特定的任务。

临界资源与临界区

  • 临界资源:多个线程共享的资源

  • 临界区:多个线程访问临界资源的代码段

线程安全

因为进程中的线程共享了进程的虚拟地址空间,因此线程间通信将变得更加简单,但是缺点也随之而来:缺乏数据的访问控制容易造成数据混乱(因为大家都在争抢访问公共资源),导致程序结果出现二义性。

举个例子:

  • 在一个进程中有两个线程,线程A,线程B,两个线程同时对一个整形的全局变量a=10进程++操作,预期结果a=12;
  • 当线程A拿到CPU之后,对全局a进行++操作,但是该操作是非原子性的,当线程A将10从内存中读取到寄存器中后,时间片到期,此时线程A就被切换出去了,此时a的值在内存中不变。
  • 抢占式执行的前提下,假设线程B在这个时候拿到了CPU资源,完成了++操作且未被打断,并将11写回到了内存当中去。
  • 这时候,线程A重新拥有资源,根据程序计数器(保存下一条执行的指令),上下文信息(寄存器中的值),恢复现场,此时线程A从寄存器中拿到的值为10,并完成++操作,将11写回内存。

结果与我们的初衷完全不一样,这就是线程完全问题。

如何保证线程安全

  • 通过互斥锁,让线程之间互斥

互斥锁

  • 在互斥锁内部有一个计数器,名为互斥量。计数器的取值只能为0或为1。
  • 当计数器取值为0时,表示当前线程无法获得互斥锁,无法访问临界资源
  • 当计数器取值为1时,表示当前线程可以获取互斥锁,可以访问临界资源。

互斥锁中的计数器如何保证原子性?
在这里插入图片描述

获取资源的时候:
1.寄存器中的值赋值为0
2.将寄存器中的值与内存的值交换
3.判断寄存器的值,得出结果

在这里插入图片描述

在第二步交换的过程中,系统通过xchgb交换操作,该操作是汇编代码,一次性完成,该操作保证了原子性。

1.互斥锁的初始化

 int pthread_mutex_init(pthread_mutex_t *_mutex,_const pthread_mutex_mutexattr_t* _mutexattr)
  • mutex:传入互斥锁变量的地址,pthread_mutex_init会初始化互斥变量
  • attr:属性,一般传递NULL,采用默认属性
  • pthread_mutex_init:互斥锁变量的类型

2.加锁

int pthread_mutex_lock(pthread_mutex_t *mutex);
  • mutex:传入互斥锁变量的地址。
  • 如果mutex当中的计数器的值为1,则pthread_mutex_lock接口就返回了,表示说加锁成功,同时计数器当中值会被更改为0,
  • 如果mutex当中的计数器的值为0,则pthread_mutex_lock接口就阻塞了,pthread_mutex_lock接口没有返回,阻塞在该函数的内部,直到加锁成功

3.解锁

int pthread_mutex_unlock(pthread_mutex_t*mutex)
  • 不管是哪一个加锁接口加锁成功的,都可以使用该接口进行解锁
  • 解锁的时候,会将互斥锁变量当中的计数器的值,从0变成1,表示其他线程可以获取互斥锁

4.互斥锁的销毁

pthread_mutex_destroy (pthread_mutex_t*);

实例的运用:黄牛抢票

创建四个线程,每个线程在抢票时候进行加锁,结束后解锁。

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

#define THREADCOUNT 4

int g_tickets = 100;
pthread_mutex_t lock_;

void* MyStartThread(void* arg)
{
    (void)arg;
    while(1)
    {
        pthread_mutex_lock(&lock_);
        if(g_tickets > 0)
        {
            printf("i am thread %p, i have ticket %d\n", pthread_self(), g_tickets);
            g_tickets--;
        }
        else
        {
            pthread_mutex_unlock(&lock_);
            break;
        }
        pthread_mutex_unlock(&lock_);
    }
    return NULL;
}

int main()
{
    pthread_mutex_init(&lock_, NULL);
    pthread_t tid[THREADCOUNT];
    for(int i = 0; i < THREADCOUNT; i++)
    {
        int ret = pthread_create(&tid[i], NULL, MyStartThread, NULL);
        if(ret < 0)
        {
            perror("pthread_create");
            return -1;
        }
    }

    for(int i = 0; i < THREADCOUNT; i++)
    {
        pthread_join(tid[i], NULL);
    }

    pthread_mutex_destroy(&lock_);
    return 0;
}

在这里插入图片描述
注意:

  • 在任何开始访问临界资源的地方进行加锁
  • 在所有可能线程退出的地方进行解锁,否则可能造成死锁

条件变量(PCB等待队列)

它可以使用在资源不满足的情况下处于休眠状态,让出CPU资源,而资源状态满足时唤醒线程

1.初始化

int pthread_cond_init(pthread_cond_t* cond,pthread_condattr_t* attr)
  • cond : pthread_cond_t :条件变量的类型,传参的时候还是传入条件变量的地址
  • attr:条件变量的属性,通常传递NULL,采用默认属性

2.等待:将调用该接口的线程放到PCB等待队列当中

int pthread_cond_wait(pthread_cond_t* cond,pthread_mutex_t* mutex);
  • cond:条件变量
  • mutex:互斥锁
  • pthread_cond_wait会在内部进行解互斥锁,且顺序是:先放入PCB等待队列,再进行解锁
  • 接口内部实现的逻辑:1.从PCB等待队列中移除。 2.抢占互斥锁 如果没有抢到互斥锁,那将会阻塞在pthread_cond_wait的内部抢锁逻辑当中

3.唤醒

int pthread_cond_siganl(pthread_cond_t* cond)
  • 通知PCB等待队列当中的线程,将其从队列当中出来,唤醒该线程
  • 唤醒至少一个PCB等待队列当中的线程
int pthread_cond_broadcast (pthread_cond_t*cond)
  • 唤醒PCB等待队列当中全部的线程

4.释放

int pthread_cond_destroy (pthread_cond_t* cond)

结合条件变量实战一下消费者模型:消费者吃面,生产者做面,当碗里没面时,将消费真放进pcb等待队列,做好后有生产者通知其出队,当碗里有面的时,将生产者放入pcb等待队列,当碗里吃完的时候由消费者通知其做面。

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

#define THREADCOUNT 2

int g_bowl = 0;
pthread_mutex_t g_mut;
pthread_cond_t g_cond;

void* EatStart(void* arg)
{
    while(1)
    {
        //eat
        pthread_mutex_lock(&g_mut);
        while(g_bowl <= 0)
        {
            pthread_cond_wait(&g_cond, &g_mut);
        }
        printf("i am %p, i eat %d\n", pthread_self(), g_bowl);
        g_bowl--;
        pthread_mutex_unlock(&g_mut);
        pthread_cond_signal(&g_cond);
    }
    return NULL;
}

void* MakeStart(void* arg)
{
    while(1)
    {
        //make
        pthread_mutex_lock(&g_mut);
        while(g_bowl > 0)
        {
            pthread_cond_wait(&g_cond, &g_mut);
        }
        g_bowl++;
        printf("i am %p, i make %d\n", pthread_self(), g_bowl);
        pthread_mutex_unlock(&g_mut);
        pthread_cond_signal(&g_cond);
    }
    return NULL;
}

int main()
{
    pthread_mutex_init(&g_mut, NULL);
    pthread_cond_init(&g_cond, NULL);
    pthread_t consume[THREADCOUNT], product[THREADCOUNT];
    for(int i = 0; i < THREADCOUNT; i++)
    {
        int ret = pthread_create(&consume[i], NULL, EatStart, NULL);
        if(ret < 0)
        {
            perror("pthread_create");
            return -1;
        }

        ret = pthread_create(&product[i], NULL, MakeStart, NULL);
        if(ret < 0)
        {
            perror("pthread_create");
            return -1;
        }
    }

    for(int i = 0; i < THREADCOUNT; i++)
    {
        pthread_join(consume[i], NULL);
        pthread_join(product[i], NULL);
    }

    pthread_mutex_destroy(&g_mut);
    pthread_cond_destroy(&g_cond);
    return 0;
}
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值