Linux: posix标准:线程互斥&& 互斥量的原理&&抢票问题

一. Linux线程互斥

1.1 进程线程间的互斥相关背景概念

进程线程间的互斥相关背景概念
---临界资源:多线程执行流共享的资源,而且每次只能一个执行流访问的资源就叫做临界资源
--- 加锁保护: 加锁保护的区域具有排他性,也就是互斥能保证每次访问只有一个执行流
----临界区:在加锁保护的代码区域,就叫做临界区
互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用
原子性(后面讨论如何实现):不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成

1.2 互斥量mutex

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

  1. 互斥量,又叫做互斥锁,也就是好像是你只有拿着互斥锁才能访问临界区域,这就保证了我们的共享资源的安全问题

二. 多执行流导致的不同步的----抢票问题

单一执行流的抢票如下,我就创建一个线程去抢票,共享资源就是一个全局变量票数ticket 肯定不会有不同步问题,但是如果是多线程都同时使用ticket呢? 情况如如何

单线程抢票

cpp 复制代码
#include <iostream>
#include <pthread.h>
#include <unistd.h>

// 存在多执行流 数据不同步的 抢票问题

// 全局共享资源
int ticket = 100;
void *getticket(void *arg)
{

    const char *name = (const char *)arg;
    /// 传入线程id
    while (1)
    {
        if (ticket > 0)
        {

            usleep(100000); // 模拟抢票流程消耗的时间
            ticket--;
            printf("%s 弄到一张票:%d\n", name, ticket);
        }
    }
}

void test1()
{

    // 单一执行流肯定能保证 是一直减到0
    pthread_t t1;
    pthread_create(&t1, nullptr, getticket, (void *)"thread1");
    pthread_join(t1, nullptr);
}

int main()
{
    test1();
}

多线程抢票

cpp 复制代码
// 全局共享资源
int ticket = 100;
void *getticket(void *arg)
{

    const char *name = (const char *)arg;
    /// 传入线程id
    while (1)
    {
        if (ticket > 0)
        {

            usleep(100000); // 模拟抢票流程消耗的时间
            ticket--;
            printf("%s 弄到一张票:%d\n", name, ticket);
        }
    }
}

void test1()
{

    // 单一执行流肯定能保证 是一直减到0
    pthread_t t1, t2, t3, t4, t5;
    pthread_create(&t1, nullptr, getticket, (void *)"thread1");
    pthread_create(&t2, nullptr, getticket, (void *)"thread2");
    pthread_create(&t3, nullptr, getticket, (void *)"thread3");
    pthread_create(&t4, nullptr, getticket, (void *)"thread4");
    pthread_create(&t5, nullptr, getticket, (void *)"thread5");

    pthread_join(t1, nullptr);
    pthread_join(t2, nullptr);
    pthread_join(t3, nullptr);
    pthread_join(t4, nullptr);
    pthread_join(t5, nullptr);
}

int main()
{
    test1();
}

output:

小小的震惊先不说,我明明if了啊 我在ticket>0的时候我才进去抢票啊怎么会这样:
简单来说你的共享资源ticket 每次都是多执行流哦,首先我们的这些线程都是并发的,我们看看哈,假设我的cpu是单核哈,
核心业务:

if (ticket > 0)

{

usleep(100000); // 模拟抢票流程消耗的时间

ticket--;

printf("%s 弄到一张票:%d\n", name, ticket);

}

核心内容也就是

  1. 判断大于0 进入拿票 2. 休眠 3. 拿到票数让票数-- 4. 打印票数
    原因-- 操作并非并发问题

a. 操作并不是原子的,如if语句 以及:ticket--
b. 多执行流分配的时间片不同,到时间会切换
具体来说:

  1. if语句 判断ticket>0 : 具体来看看对应的汇编指令
cpp 复制代码
; 假设 ticket 是一个整型变量,存储在内存中

mov eax, [ticket]     ; 1. 从内存中读取 ticket 的值到寄存器 eax
cmp eax, 0            ; 2. 与 0 进行比较(实际上是 eax - 0,只设置标志位)
jle else_label        ; 3. 条件跳转:如果小于等于 0,跳转到 else 部分
                      ;    如果大于 0,继续执行 if 块代码

; if 块的代码
...

else_label:
; else 块的代码(如果有的话)

在这三步中 任意一步都可能存在执行流切换,如何访问这个共享资源,可能刚好切换回来其他执行流又让ticket-- ,使得票数为0 ;

  1. ticket--

这个语句同样如此,换成汇编就是 :

  1. 把数从内存加载到寄存器

  2. 把数修改-- 在寄存器修改返回内存

核心问题是我们要解决这样多执行流导致的问题核心就是要让,每次访问共享资源只有一个执行流,而且它访问完!之后才能让别的执行流进来访问,这个过程就是同步,串行的访问共享资源

三.临界资源&& 同步互斥锁

3.1 互斥---临界资源

如上图,互斥锁又被叫做,互斥量,
原理如下: 1. 互斥量本身在内核中是一个变量,值为1
2. 每次进入临界区之前都要先获取互斥量, 获取的操作是原子的!!!! 之后细谈
3. 因为获取互斥量是原子,以及在获取(互斥量)信号量的过程的原子操作是非常断,时间片是毫秒级别 而获取信号量是纳秒几乎不会被打断,如果极端情况被打断,那么原子性要么做要么不做, 那么就会获取信号量失败,然后把信号量还回去,切换到其他线程

  1. 有了上面的条件那么,如果有线程拿走了这个互斥量其他线程访问这个值就是0了,也就阻塞等待直到该信号量被返回,这样就保证始终都是同步!!!!串行进入到共享资源啦
    因为这个互斥量的概念,现在很多人都觉得像锁,就是这个互斥量是1本质上就是信号量的值(就是一个原子的计数器) 互斥量也被成为互斥锁所以posix的标准也叫做
    pthread_mutex

3.2 posix的互斥锁 pthread_mutex

3.2.1 初始化

cpp 复制代码
初始化互斥量
初始化互斥量有两种方法:
方法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: 
       第一个参数是你要传入的锁变量
        第二个是初始化锁属性 我们这里平常学习通常设置为nullptr

区别是什么呢?

  1. 静态初始化就是,你对比哈我们之前聊过互斥量的原理,本质上还是一个原子的变量,值为1,被获取了就--。 静态的互斥量也就是说它这个变量是栈上的将来你不用主动去消耗它,以及它对锁的属性也是默认的不能自定义设置

  2. 动态初始化就是,这个变量的内存是动态开辟的之后你要主动销毁它

3.2.2 加锁与解锁/ 互斥量的获取与返回

cpp 复制代码
互斥量加锁和解锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
返回值:成功返回0,失败返回错误号

   int pthread_mutex_lock(pthread_mutex_t *mutex);

         //这之间的就是临界区了能保证只有一个线程进来
   
int pthread_mutex_unlock(pthread_mutex_t *mutex);

互斥量的获取/互斥锁加锁一码事哈,那么我们来说这个接口 :

  1. 调用 lock的时候,如果信号量值(理解位计数器的值)依然是1那么就获取到

  2. 如果信号量值是0就被阻塞住

3.2.3 互斥量/互斥锁的销毁

cpp 复制代码
销毁互斥量
销毁互斥量需要注意:
  
使用 PTHREAD_ MUTEX_ INITIALIZER 初始化的互斥量不需要销毁
a. 不要销毁一个已经加锁的互斥量
b. 已经销毁的互斥量,要确保后面不会有线程再尝试加锁
  
 动态的互斥量才要销毁 也就是你调用init的
int pthread_mutex_destroy(pthread_mutex_t *mutex)

3.3 加锁保护的抢票demo

cpp 复制代码
// 全局共享资源
int ticket = 100;
pthread_mutex_t _mtx;
void *getticket(void *arg)
{

    const char *name = (const char *)arg;
    /// 传入线程id
    while (1)
    {

        // 来到这里的线程先获取锁或者说获取信号量
        pthread_mutex_lock(&_mtx);
        if (ticket > 0)
        {

            usleep(100000); // 模拟抢票流程消耗的时间
            printf("%s 弄到一张票:%d\n", name, ticket--);
            //// sched_yield(); 放弃CPU
        }
        pthread_mutex_unlock(&_mtx);
    }
}

void test1()
{
    // 初始化锁
    pthread_mutex_init(&_mtx, nullptr);
    // 单一执行流肯定能保证 是一直减到0
    pthread_t t1, t2, t3, t4, t5;
    pthread_create(&t1, nullptr, getticket, (void *)"thread1");
    pthread_create(&t2, nullptr, getticket, (void *)"thread2");
    pthread_create(&t3, nullptr, getticket, (void *)"thread3");
    pthread_create(&t4, nullptr, getticket, (void *)"thread4");
    pthread_create(&t5, nullptr, getticket, (void *)"thread5");

    pthread_join(t1, nullptr);
    pthread_join(t2, nullptr);
    pthread_join(t3, nullptr);
    pthread_join(t4, nullptr);
    pthread_join(t5, nullptr);
    pthread_mutex_destroy(&_mtx);
}

int main()
{
    test1();
}

output

如上图: 代码运行的结果正常了,没有出现负数了,而且所有的获取票都是正常的递减.
这说明我们的互斥锁确实保证同步的获取了

3.2.3 活锁--线程的饥饿问题 ---- 抢票问题

不知道细心的你是不是发现如下图,以及上面同步抢票问题中有一个很奇怪的想象,抢到票的线程都是thread4 总之是同一个线程 ,这就是线程的饥饿问题:

饥饿问题图解:

a. **信号量:**互斥量本质上就是信号量的,二值信号量,值要么是0要么是1,也就是本质上是一把计数器,最大大小是1.
b.信号量的原理:每次进入临界区先获取信号量,如果信号量的值是0,就进入信号量的等待队列,正常情况下,如果线程返还信号量,就会回到等待队列的最后面,
c. cpu的调度策略: 但是互斥锁的默认调度策略不是从这个等待队列中的,最开始的那个位置拿,而是从最近使用的执行流让他继续调度毕竟认为这样效率更高。 所以我们可以采取一个策略,让该线程立刻以后主动放弃当前的时间片 接口就是

cpp 复制代码
// sched_yield(); 放弃cpu的时间片 
yield()放弃时间片的策略解决饥饿问题
cpp 复制代码
 while (1)
    {

        // 来到这里的线程先获取锁或者说获取信号量
        pthread_mutex_lock(&_mtx);
        if (ticket > 0)
        {

            usleep(100000); // 模拟抢票流程消耗的时间
            printf("%s 弄到一张票:%d\n", name, ticket--);
            //// sched_yield(); 放弃CPU
        }

        pthread_mutex_unlock(&_mtx);
        // 解锁之后主动放弃时间片
        sched_yield();
    }


改变锁的初始化属性

因为互斥锁初始化默认属性我们设置的是nullptr,也就意味着它的属性就是默认的,我们可以设置位公平调度策略,每次让cpu去等待队列拿队头 ,返回锁的回到队尾FIFO 也就是 公平调度策略

复制代码
    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_FAIR_NP);
cpp 复制代码
int ticket = 100;
pthread_mutex_t _mtx;
void *getticket(void *arg)
{

    const char *name = (const char *)arg;
    /// 传入线程id
    while (1)
    {

        // 来到这里的线程先获取锁或者说获取信号量
        pthread_mutex_lock(&_mtx);
        if (ticket > 0)
        {

            usleep(100000); // 模拟抢票流程消耗的时间
            printf("%s 弄到一张票:%d\n", name, ticket--);
            //// sched_yield(); 放弃CPU
        }

        pthread_mutex_unlock(&_mtx);
        // 解锁之后主动放弃时间片
        // sched_yield();
    }
}

void test1()
{
    // 初始化锁属性
    pthread_mutexattr_t attr;
    pthread_mutexattr_init(&attr);
    // 设置优先级继承,缓解饥饿(CentOS标准POSIX接口)
    pthread_mutexattr_setprotocol(&attr, PTHREAD_PRIO_INHERIT);
    pthread_mutex_init(&_mtx, &attr);

    // 单一执行流肯定能保证 是一直减到0
    pthread_t t1, t2, t3, t4, t5;
    pthread_create(&t1, nullptr, getticket, (void *)"thread1");
    pthread_create(&t2, nullptr, getticket, (void *)"thread2");
    pthread_create(&t3, nullptr, getticket, (void *)"thread3");
    pthread_create(&t4, nullptr, getticket, (void *)"thread4");
    pthread_create(&t5, nullptr, getticket, (void *)"thread5");

    pthread_join(t1, nullptr);
    pthread_join(t2, nullptr);
    pthread_join(t3, nullptr);
    pthread_join(t4, nullptr);
    pthread_join(t5, nullptr);
    pthread_mutex_destroy(&_mtx);
}

int main()
{
    test1();
}


相关推荐
好记忆不如烂笔头abc2 小时前
安装python新版本
开发语言·人工智能·python
漫漫求2 小时前
ubuntu设置软件开机自启动
linux·运维·ubuntu
Scholar With Saber2 小时前
kali Linux安装教程,ISO镜像安装(物理机,虚拟机皆可)kali安装2025最新,0基础可用,保姆级图文
linux·运维·网络安全
网硕互联的小客服2 小时前
哪些外在因素条件会导致服务器的延迟过高?
linux·运维·服务器·数据库·安全
csbysj20202 小时前
HTML 音频(Audio)
开发语言
枫叶丹42 小时前
【Qt开发】Qt事件(三)-> QMouseEvent 鼠标事件
c语言·开发语言·c++·qt·microsoft·计算机外设
Leonardo_Fibonacci2 小时前
skbbs-day5
java·开发语言·mybatis
徐安安ye2 小时前
Flutter 与 Rust 混合开发:打造毫秒级响应的高性能计算引擎
开发语言·flutter·rust
wregjru2 小时前
【操作系统】2.用户和权限
linux·服务器·unix