Linux线程【互斥与同步】

目录

1.资源共享问题 

1.1多线程并发访问 

1.2临界区和临界资源 

1.3互斥锁 

2.多线程抢票 

2.1并发抢票 

2.2 引发问题 

3.线程互斥 

3.1互斥锁相关操作 

3.1.1互斥锁创建与销毁 

3.1.2、加锁操作 

3.1.3 解锁操作 

3.2.解决抢票问题 

3.2.1互斥锁细节 

3.3互斥锁原理 

3.4多线程封装 

3.5互斥锁的封装

3.5.1RAII风格

4.线程安全VS重入

5、常见锁概念

5.1、死锁问题

6.线程同步

6.1同步概念

6.2.同步相关操作

6.2.1条件变量创建与销毁 

6.2.2条件等待

6.2.3唤醒线程

6.3同步demo



1.资源共享问题 

1.1多线程并发访问 

 比如存在全局变量 g_val 以及两个线程 thread_A 和 thread_B,两个线程同时不断对 g_val 做 减减 -- 操作

 

注意:用户的代码无法直接对内存中的 g_val 做修改,需要借助 CPU 

如果想要对 g_val 进行修改,至少要分为三步:

  1. 先将 g_val 的值拷贝至寄存器中
  2. 在 CPU 内部通过运算寄存器完成计算
  3. 将寄存器中的值拷贝回内存

假设 g_val 初始值为 100,如果 thread_A 想要进行 g_val--,就必须这样做

 

也就是说,简单的一句 g_val-- 语句实际上至少会被分成 三步

单线程场景下步骤分得再细也没事,因为没有其他线程干扰它,但我们现在是在 多线程 场景中,存在 线程调度问题,假设此时 thread_A 在执行完第2步后被强行切走了,换成 thread_B 运行

 

thread_A 的第3步还没有完成,内存中 g_val 的值还没有被修改,但 thread_A 认为自己已经修改了(完成了第2步),在线程调度时,thread_A 的上下文及相关数据会被保存,thread_A 被切走后,thread_B 会被即刻调度入场,不断执行 g_val-- 操作

thread_B 的运气比较好,进行很多次 g_val-- 操作后都没有被切走

 

当 thread_B 将 g_val 中的值修改为 10 后,就被操作系统切走了,此时轮到 thread_A 登场,thread_A 带着自己的之前的上下文数据,继续进行它的未尽事业(完成第3步操作),当然 thread_B 的上下文数据也会被保存 

 

  此时尴尬的事情发生了:thread_A 把 g_val 的值改成了 99,这对于 thread_B 来说很不公平,倘若下次再从内存中读取 g_val时,结果为 99,自己又得重新进行计算,但站在两个线程的角度来说,两者都没有错

  • thread_A: 将自己的上下文恢复后继续执行操作,合情合理
  • thread_B: 按照要求不断对 g_val 进行操作,也是合情合理

 

错就错在 thread_A 在错误的时机被切走了,保存了老旧的 g_val 值(对于 thread_B 来说),直接影响就是 g_val 的值飘忽不定

倘若再出现一个线程 thread_C 不断打印 g_val 的值,那么将会看到 g_val 值减为 10 后又突然变为 99 的 “灵异现象”

产出结论:多线程场景中对全局变量并发访问不是 100% 可靠的

1.2临界区和临界资源 

 在多线程场景中,对于诸如 g_val 这种可以被多线程看到的同一份资源称为 临界资源,涉及对 临界资源 进行操作的上下文代码区域称为 临界区

 

 临界资源 本质上就是 多线程共享资源,而 临界区 则是 涉及共享资源操作的代码区间

1.3互斥锁 

 临界资源 要想被安全的访问,就得确保 临界资源使用时的安全性

对于 临界资源 访问时的安全问题,也可以通过 加锁 来保证,实现多线程间的 互斥访问互斥锁 就是解决多线程并发访问问题的手段之一 

我们可以 在进入临界区之前加锁,出临界区之后解锁, 这样可以确保并发访问 临界资源 时的绝对串行化,比如之前的 thread_A 和 thread_B 在并发访问 g_val 时,如果进行了 加锁,在 thread_A 被切走后,thread_B 无法对 g_val 进行操作,因为此时 锁 被 thread_A 持有,thread_B 只能 阻塞式等待锁,直到 thread_A 解锁(意味着 thread_A 的整个操作都完成了)

 因此,对于 thread_A 来说,在 加锁 环境中,只要接手了访问临界资源 g_val 的任务,要么完成、要么不完成,不会出现中间状态,像这种不会出现中间状态、结果可预期的特性称为 原子性

 说白了 加锁 的本质就是为了实现 原子性

注意:

  • 加锁、解锁是比较耗费系统资源的,会在一定程序上降低程序的运行速度
  • 加锁后的代码是串行化执行的,势必会影响多线程场景中的运行速度
  • 所以为了尽可能的降低影响,加锁粒度要尽可能的细

2.多线程抢票 

 实践出真知,接下来通过代码演示多线程并发访问问题

2.1并发抢票 

思路很简单:存在 1000 张票和 5 个线程,5 个线程同时抢票,直到票数为 0,程序结束后,可以看看每个线程分别抢到了几张票,以及最终的票数是否为 0

共识:购票需要时间,抢票成功后也需要时间,这里通过 usleep 函数模拟耗费时间

#include <iostream>
#include <string>
#include <unistd.h>
#include <pthread.h>
using namespace std;

int tickets = 1000; // 有 1000 张票

void* threadRoutine(void* args)
{
    int sum = 0;
    const char* name = static_cast<const char*>(args); 
    while(true)
    {
        // 如果票数 > 0 才能抢
        if(tickets > 0)
        {
            usleep(2000); // 耗时 2ms
            sum++;
            --tickets;
        }
        else
            break; // 没有票了

        usleep(2000); //抢到票后也需要时间处理
    }

    cout << "线程 " << name << " 抢票完毕,最终抢到的票数 " << sum << endl;
    delete name;
    return nullptr;
}

int main()
{
    pthread_t pt[5];
    for(int i = 0; i < 5; i++)
    {
        char* name = new char(16);
        snprintf(name, 16, "thread-%d", i);
        pthread_create(pt + i, nullptr, threadRoutine, name);
    }

    for(int i = 0; i < 5; i++)
        pthread_join(pt[i], nullptr);

    cout << "所有线程均已退出,剩余票数: " << tickets << endl;

    return 0;
}

 最终剩余票数 -1,难道 12306 还欠了 1张票?这显然是不可能的,5 个线程抢到的票数之和为 1001,这就更奇怪了,总共 1000张票还多出来 1 张?

 显然多线程并发访问是绝对存在问题的

2.2 引发问题 

这其实就是 thread_A thread_B 并发访问 g_val 时遇到的问题,举个例子:假设 tickets = 500,thread-0 在抢票,准备完成第3步,将数据拷贝回内存时被切走了,thread-1 抢票后,tickets = 499;轮到 thread-0 回来时,它也是把 tickets 修改成了 499,这就意味着 thread-0 和 thread-1 之间有一个人白嫖了一张票(按理来说 tickets = 498 才对)

 对于  这种 临界资源,可以通过 加锁 进行保护,即实现 线程间的互斥访问,确保多线程购票时的 原子性

  • 3 条汇编指令要么不执行,要么全部一起执行完

 --tickets 本质上是 3 条汇编指令,在任意一条执行过程中切走线程都会引发并发访问问题

3.线程互斥 

互斥 -> 互斥排斥:事件 A 与事件 B 不会同时发生

比如 多线程并发抢票场景中可以通过添加 互斥锁 的方式,来确保同一张票不会被多个线程同时抢到

3.1互斥锁相关操作 

3.1.1互斥锁创建与销毁 

 互斥锁 同样出自 原生线程库,类型为 pthread_mutex_t互斥锁 在创建后需要进行 初始化

#include <pthread.h>

pthread_mutex_t mtx; // 定义一把互斥锁
int pthread_mutex_init(pthread_mutex_t *restrict mutex, 
					   const pthread_mutexattr_t *restrict attr);

其中:

参数1 pthread_mutex_t* 表示想要初始化的锁,这里传的是地址,因为需要在初始化函数中对 互斥锁 进行初始化

参数2 const pthread_mutexattr_t* 表示初始化时 互斥锁 的相关属性设置,传递 nullptr 使用默认属性

返回值:初始化成功返回 0,失败返回 error number

互斥锁 是一种向系统申请的资源,在 使用完毕后需要销毁

#include <pthread.h>

int pthread_mutex_destroy(p
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值