linux:线程互斥

这篇具有很好参考价值的文章主要介绍了linux:线程互斥。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

linux:线程互斥,Linux,linux

个人主页 : 个人主页
个人专栏 : 《数据结构》 《C语言》《C++》《Linux》


前言

本文是对于线程互斥的知识总结


一、线程互斥

问题

我们先看下面代码。

#include <iostream>
#include <vector>
#include <string>
#include <pthread.h>
#include <unistd.h>

const int numbers = 3;

int ticket = 10000;
void *threadRoutine(void *args)
{
    std::string name = static_cast<char *>(args);
    while (true)
    {
        if (ticket > 0)
        {
            usleep(1000);
            std::cout << name << "get a ticket: " << ticket << std::endl;
            ticket--;
        }
        else
        {
            break;
        }
    }
	return nullptr;
}

int main()
{
    std::vector<pthread_t> tds;
    for (int i = 0; i < numbers; ++i)
    {
        pthread_t td;
        char buff[64];
        snprintf(buff, sizeof(buff), "thread-%d", i);

        pthread_create(&td, nullptr, threadRoutine, (void *)buff);
        usleep(1000);
        tds.push_back(td);
    }

    pthread_join(tds[0], nullptr);
    pthread_join(tds[1], nullptr);
    pthread_join(tds[2], nullptr);

    return 0;
}

该代码创建三个线程-1,线程-2,线程-3,去抢夺ticket资源,当ticket从10000依次减到0时,三个线程退出。那该代码运行结果是什么呢?ticket最后会是0吗?
linux:线程互斥,Linux,linux
显而易见ticket最后是-1!这是为什么?三个线程在ticket为0时,不应该退出吗,ticket为什么会是-1?更奇怪的还是下图
linux:线程互斥,Linux,linux
ticket值尽然有相同的情况,ticket的值不应该依次递减吗?


解释

先不要着急,我们先来明确几个概念

  • 临界资源:多线程执行流共享的资源叫做临界资源(如上面代码中的ticket全局变量)

  • 临界区:每个线程内部,访问临界资源的代码,就叫做临界区(如下图红框部分的代码)
    linux:线程互斥,Linux,linux

  • 互斥:任何时候,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源其保护作用

  • 原子性:不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成

在了解上面四个概念后,我们还需要了解,在C++/C中前置++ or 后置++,判断操作是原子的吗?
linux:线程互斥,Linux,linux
linux:线程互斥,Linux,linux

显然这些操作都不是原子的,判断在汇编指令是要先判断,再依据判断结果跳转执行流,后置–是先从内存中读取变量的值放到寄存器中,再对寄存器中的值-1, 最后将寄存器中的值放回变量中。

现在知道了上述知识,我们可以来理解为什么ticket会不变和变为-1。

我们现将这些汇编指令分别表明为步骤1,步骤2…
linux:线程互斥,Linux,linux
我们先来解释为什么ticket的值可能不变。
linux:线程互斥,Linux,linux
当线程thread-0执行步骤3,将内存中icket的值34,读取到寄存器中;线程thread-0被线程thread-1切换,寄存器中的34,会作为线程thread-0的上下文数据,被线程thread-0保留,此时线程thread-1执行步骤3,将内存中icket的值34,读取到寄存器中;线程thread-1被线程thread-2切换,同理寄存器中的34作为线程thread-1的上下文数据被保留;此时线程thread-2执行步骤3,将内存中icket的值34,读取到寄存器中,再执行步骤4,将寄存器中的34变为33,在执行步骤5,将寄存器中的33放回到内存中ticket处,此时ticket = 33;线程thread-2打印ticket的值;线程thread-2被线程thread-0切换,要恢复线程thread-0的上下文数据,寄存器中存储的是34,线程thread-0在执行步骤4,将寄存器中的34变为33,在执行步骤5,将寄存器中的33拷贝到内存中ticket处,ticket = 33,线程thread-0打印ticket的值为33;同理线程thread-2最后也会打印ticket的值为33。这就是线程0,1,2都会打印33的原因。
linux:线程互斥,Linux,linux

明白了为什么ticket会打印3次33。那ticket为什么会变为-1,就好理解了。
我们假定此时ticket = 1;线程thread-0执行步骤1( 1 > 0)判断为真,被线程thread-1切换,判断结果作为线程thread-0的上下文数据被保存;线程thread-1也指向步骤1( 1 > 0)判断为真,在执行步骤2,步骤3,步骤4,步骤5,从内存中读取ticket的值(ticket = 1),再在寄存器内将1 -> 0,再将0拷贝到内存ticket处(ticket= 0),线程tithread-1被线程thread-2切换,线程thread-2执行步骤1( 0 > 0)判断为假,结束循环;线程thread-2被线程thread-0切换,线程thread-0执行步骤2,步骤3,步骤4,步骤5,从内存中读取ticket的值(ticket = 0),再在寄存器内将0 -> -1,再将-1拷贝到内存中ticket处(ticket = -1)。这就是ticket为什么会是-1的原因。

这就是我们多线程访问共享数据而导致数据不一致问题,那如何解决呢?

要解决以上数据不一致问题,就要保证只能有一个执行流在临界区执行代码。而这就是锁,linux上提供的这把锁叫做互斥量。
linux:线程互斥,Linux,linux


互斥量的接口

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

  • 静态分配
    linux:线程互斥,Linux,linux

  • 动态分配
    linux:线程互斥,Linux,linux
    mutex:要初始化的互斥量;
    attr:用于设置互斥锁的属性(传递 NULL 作为 attr 参数的值,那么互斥锁会使用默认的属性进行初始化。)

销毁互斥量
linux:线程互斥,Linux,linux
如果成功销毁互斥锁,则返回0;
如果发生错误,则返回错误码(EBUSY:在尝试销毁一个正在使用的互斥锁时,通常会返回这个错误;EINVAL:传递个函数的mutex指针无效)
需要注意的是:

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

互斥量加锁和解锁

linux:线程互斥,Linux,linux
加锁成功,返回0;加锁失败,返回错误码(如EDEADLK, EINVAL, EBUSY)
当互斥锁已经被其它线程锁定时,调用pthread_mutex_lock的线程通常会被阻塞,直到互斥锁被解锁。如果不想线程申请锁失败被阻塞,可以使用pthread_mutex_trylock函数。

linux:线程互斥,Linux,linux
加锁成功,返回0; 加锁失败,返回错误码(如EBUSY:当前互斥锁被其他线程锁定)
pthread_mutex_trylock不会让调用线程在互斥锁不可用时进入阻塞状态,这使得可以用轮询的方式来申请锁。

linux:线程互斥,Linux,linux
成功解除锁,返回0;解除锁失败,返回错误码(如传递给函数的mutex指针无效,解除一个未由当前线程锁定的锁)

现在我们对开始的代码进行改进

#include <iostream>
#include <vector>
#include <string>
#include <pthread.h>
#include <unistd.h>

const int numbers = 3;
// 定义锁
pthread_mutex_t mutex = PTHREAD_ADAPTIVE_MUTEX_INITIALIZER_NP;

int ticket = 10000;
void *threadRoutine(void *args)
{
    std::string name = static_cast<char *>(args);
    while (true)
    {
    	//加锁
        pthread_mutex_lock(&mutex);
        if (ticket > 0)
        {
            usleep(1000);
            std::cout << name << "get a ticket: " << ticket << std::endl;
            ticket--;
            // 解锁
            pthread_mutex_unlock(&mutex);
        }
        else
        {
        	// 解锁
            pthread_mutex_unlock(&mutex);
            break;
        }
    }
}

int main()
{
    std::vector<pthread_t> tds;
    for (int i = 0; i < numbers; ++i)
    {
        pthread_t td;
        char buff[64];
        snprintf(buff, sizeof(buff), "thread-%d", i);

        pthread_create(&td, nullptr, threadRoutine, (void *)buff);
        usleep(1000);
        tds.push_back(td);
    }

    pthread_join(tds[0], nullptr);
    pthread_join(tds[1], nullptr);
    pthread_join(tds[2], nullptr);

    return 0;
}

linux:线程互斥,Linux,linux
现在ticket == 1时,程序退出。

二、加锁的原理

pthread_mutex_t的结构如下:
linux:线程互斥,Linux,linux

我们先简单的将mutex这个结构体理解为一个int,将mutex = 1视为锁资源空闲,将mutex = 0视为锁资源已经被占用。
linux:线程互斥,Linux,linux
当一个线程要加锁时,其要先执行movb指令,将0移动到%al寄存器中(表示未持有锁),再执行xchgb指令,将mutex中的值与%al寄存器交换,如果%al寄存器中的值大于0,表示加锁成功,可以执行临界区代码;%al寄存器的值小于等于0,表示加锁失败,要挂起等待其它持有该锁的线程释放锁,再执行goto语句,重新申请锁。
linux:线程互斥,Linux,linux
当一个线程释放锁时,其要执行movb指令,将1移动到mutex处(表示锁资源空闲),再唤醒挂起等待的线程;

看了上面内容,我们可能还有点疑惑,为什么pthread_mutex_lock函数就是原子的呢?下面让我们已两个线程1,2申请锁为列,来理解。
我们假定当前有一个空闲的锁mutex,线程1执行movb指令,将0移动到%al寄存器中,线程1被线程2切换,线程1保存%al寄存器中的内容0;线程2执行movb指令,将0移动到%al寄存器中,再执行xchgb指令,将mutex的值与%al寄存器中的值交换(mutex = 0, %al = 1),线程2被线程1切换,%al寄存器中的内容1作为线程2的上下文数据被保存,线程1回复上下文数据(%al = 0),再执行xchgb指令,交换mutex和%al寄存器的内容(%al = 0, mutex = 0),再执行if判断为假,线程1挂起等待;线程1被线程2切换,回复上下文数据(%al = 1),线程2执行if判断为真,线程2执行临界区代码。

linux:线程互斥,Linux,linux

现在我们就可以理解,为什么pthread_mutex_lock函数是原子的了。

三、 死锁

死锁是指在一组进程中各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态。
如下图所示:
linux:线程互斥,Linux,linux
线程A,B只有同时拥有锁1,锁2才能访问临界区代码,此时线程A拥有lock1,申请lock2,线程B拥有lock2,申请lock1,线程A,B都会因为所申请的资源被其它线程所占有而等待,这就是死锁。

死锁四个必要条件

  • 互斥条件:一个资源每次只能被一个执行流使用
  • 请求与保持条件:一个执行流因请求资源而阻塞时,对已获取资源保持不放
  • 不剥夺条件:一个执行流已获取的资源,在未使用完之前,不能强行剥夺
  • 循环等待条件:若干执行流之间现成一个头尾相接的循环等待资源的关系(如上图,线程A需要线程B持有的lock2,线程B需要线程A持有的lock1,线程A,B形成头尾相接的循环等待)

避免死锁

  • 破坏死锁的四个必要条件
  • 加锁顺序一致(如要求线程A,B都因先申请lock1,再申请lock2)
  • 避免锁未释放的场景(如将pthread_mutex_unlock误写成pthread_mutex_lock,从而导致只有一个线程造成的死锁)
  • 资源一次性分配

总结

以上就是我对于线程互斥的总结。

linux:线程互斥,Linux,linux文章来源地址https://www.toymoban.com/news/detail-842061.html

到了这里,关于linux:线程互斥的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Linux多线程互斥锁

    迷途小书童的 Note 读完需要 5 分钟 速读仅需 2 分钟 1 引言 在 Linux 编程中,多线程是一种常见的并发编程模型。为了保证多线程之间的数据同步和互斥访问,pthread_mutex(互斥锁)是一个重要的工具。本文将深入探讨 pthread_mutex 的底层实现原理、函数原型,并提供详细的使用方

    2024年02月10日
    浏览(54)
  • linux:线程互斥

    个人主页 : 个人主页 个人专栏 : 《数据结构》 《C语言》《C++》《Linux》 本文是对于线程互斥的知识总结 我们先看下面代码。 该代码创建三个线程-1,线程-2,线程-3,去抢夺ticket资源,当ticket从10000依次减到0时,三个线程退出。那该代码运行结果是什么呢?ticket最后会是

    2024年03月21日
    浏览(40)
  • 【linux】线程互斥

    喜欢的点赞,收藏,关注一下把! 到目前为止我们学了线程概念,线程控制接下来我们进行下一个话题,线程互斥。 有没有考虑过这样的一个问题,既然 线程一旦被创建,几乎所有资源都是被所有线程共享的。 那多个线程访问同一份共享资源有没有什么问题? 下面我们模

    2024年02月03日
    浏览(40)
  • 【关于Linux中----线程互斥】

    先来用代码模拟一个抢票的场景,四个线程不停地抢票,一共有1000张票,抢完为止,代码如下: 执行结果如下: 可以看到,最后出现了票数为负数的情况,很显然这是错误的,是不应该出现的。 为什么会出现这种情况? 首先要明确,上述的几个线程是不能同时执行抢票的

    2023年04月19日
    浏览(74)
  • Linux->线程互斥

    前言: 1 线程互斥 1.1 多线程并发问题 1.2 线程锁 1.3 锁的接口 2 线程安全与可重入函数  3 死锁         本篇文章主要讲解了线程互斥的实现方式,还有原理,并附上代码讲解。并且讲解了锁的概念,问题等。         还记得我上一篇文章的结尾有提过的问题吗?如果多个

    2024年02月10日
    浏览(41)
  • Linux之线程互斥

    目录 一、问题引入 二、线程互斥 1、相关概念 2、加锁保护 1、静态分配 2、动态分配 3、锁的原理 4、死锁 三、可重入与线程安全 1、概念 2、常见的线程不安全的情况 3、常见的线程安全的情况 4、常见不可重入的情况 5、常见可重入的情况 6、可重入与线程安全联系 7、可重

    2024年03月16日
    浏览(92)
  • Linux--线程--互斥锁

    1.互斥量 a)互斥量(mutex)从本质上来说是一把锁,一般在主线程中定义一个互斥量,就是定义一把锁。然后根据我们的需求来对线程操作这把锁。 b)如果给所有的线程都加上锁了,线程们会去争取内存空间,谁先争取到谁先运行,直到该线程解锁后,期间其他线程只能等

    2024年02月06日
    浏览(44)
  • Linux——线程的同步与互斥

    目录 模拟抢火车票的过程 代码示例 thread.cc Thread.hpp 运行结果 分析原因 tickets减到-2的本质  解决抢票出错的方案 临界资源的概念 原子性的概念 加锁 定义 初始化 销毁 代码形式如下 代码示例1: 代码示例2: 总结 如何看待锁 申请失败将会阻塞  pthread_mutex_tyrlock 互斥锁实现

    2024年02月06日
    浏览(43)
  • Linux——多线程,互斥与同步

    目录 一.linux互斥 1.进程线程间的互斥相关背景概念 2.互斥量mutex 3.加锁互斥锁mutex 4.锁的底层原理  二.可重入VS线程安全 1.概念 2.常见的线程不安全的情况 3.常见的线程安全的情况  4.常见不可重入的情况  5..常见可重入的情况 6.可重入与线程安全联系  三.死锁 1.死锁四个必

    2024年02月05日
    浏览(34)
  • 【Linux】多线程互斥与同步

    互斥 指的是一种机制,用于确保在同一时刻只有一个进程或线程能够访问共享资源或执行临界区代码。 互斥的目的是 防止多个并发执行的进程或线程访问共享资源时产生竞争条件,从而保证数据的一致性和正确性 ,下面我们来使用多线程来模拟实现一个抢票的场景,看看所

    2024年02月09日
    浏览(40)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包