【Linux线程(二)】线程互斥和同步

news2025/1/20 10:56:25

前言:

在上一篇博客中,我们讲解了什么是线程以及如何对线程进行控制,那么了解了这些,我们就可以在程序中创建多线程了,可是多线程往往会带有许多问题,比如竞态条件、死锁、数据竞争、内存泄漏等问题,解决这些问题的关键在于如何实现线程的互斥和同步

互斥:

  • 互斥是指一次只允许一个线程访问共享资源。这意味着当一个线程正在访问共享资源时,其他线程必须等待,直到该线程释放了资源。
  • 互斥通常通过互斥锁来实现。当一个线程获得了互斥锁时,其他线程就无法获得该锁,只能等待锁被释放。

同步:

  • 同步是指协调多个线程的执行顺序,以确保它们按照预期的顺序执行。
  • 同步机制可以确保在多个线程之间正确的共享信息和控制流。常见的同步机制包括信号量、条件变量和屏障等。
  • 同步通常用于控制线程之间的竞态条件和避免数据竞争的发生。

今天我们从多线程中的数据竞争问题入手,进一步了解多线程并且利用互斥机制来解决问题。

(一)多线程中的数据竞争

1.相关概念

在分析多线程中的数据竞争问题之前,需要先了解一些相关的概念:

并发访问:

并发指的是在一段时间内,多个任务交替地执行,这些任务可能在同一时间段内启动和执行,但并不一定同时执行。

临界资源&临界区:

  • 临界资源是指在多线程环境下需要互斥访问的共享资源,例如共享变量、共享内存区域、文件等。如果多个线程并发地访问和修改临界资源,可能会导致数据竞争和程序错误。
  • 临界区是指包含对临界资源访问的代码段或程序区域,这些代码段在任何给定时间点只能被一个线程执行,以确保对临界资源的安全访问。

原子性:

原子性是指在并发编程中操作的不可分割性,即一个操作要么完全执行,要么不执行,不存在中间状态。原子操作在执行过程中不会被中断,也不会被其他线程的操作干扰。

锁:

在并发编程中,(Lock)是一种同步机制,用于控制对临界区的访问,确保在任何给定时间点只有一个线程可以进入临界区执行代码。锁主要用于解决多线程环境下的竞态条件和数据竞争问题。

 2.多线程抢票场景

在日常生活中,高铁、火车抢票是很平常的一件事。假设票总量为1000,用户进入系统,如果剩余票的数量大于0,那么就代表还有票,用户抢到一张票,剩余票数量减一。这个场景其实就是多线程并发访问的场景,每个用户就代表一个线程,票代表共享资源,下面我们用代码来模拟一下

int ticket = 10000;
void *StartRoutine(void *args)
{
    const string name = static_cast<char*>(args);
    while(true)
    {
        if(ticket > 0)
        {
            cout<<name<<" get a ticket:"<<ticket<<endl;
            ticket--;
        }
        else
        {
            break;
        }
        usleep(1000);
    }
    return nullptr;
}
int main()
{
    pthread_t td1,td2,td3,td4;
    pthread_create(&td1,nullptr,StartRoutine,(void*)"thread-1");
    pthread_create(&td2,nullptr,StartRoutine,(void*)"thread-2");
    pthread_create(&td3,nullptr,StartRoutine,(void*)"thread-3");
    pthread_create(&td4,nullptr,StartRoutine,(void*)"thread-4");
    
    pthread_join(td1,nullptr);
    pthread_join(td2,nullptr);
    pthread_join(td3,nullptr);
    pthread_join(td4,nullptr);
    
    return 0;
}

可是当我们运行程序时,却发现运行的结果并不完全一致,有时候票的编号甚至会减少到0或-1、-2,这是为什么呢?

显然我们这个程序的多线程并发访问共享数据是有问题的。

3.并发访问问题分析

在这个程序中,对全局变量ticket进行访问的操作有: if(ticket > 0) 和 ticket--;

如果想要对共享数据进行操作,至少要分为三步:

  1. 将内存中数据的值拷贝到CPU中的寄存器中。
  2. 在CPU内部通过对寄存器的运算完成操作。
  3. 将寄存器中的结果拷贝回内存中。

大致图解如下: 

如果是在单线程中,上面这三步操作并不会被打断,可是在多线程中,由于上面的操作并不是原子的,而且线程会被调度,所以在中间可能会被打断。

比如线程A对ticket进行--操作,在执行完第二步后,寄存器中的内容已经由100减到99了,然后线程A将要执行第三步时,却发生了线程的调度,例如线程A的时间片到了,然后线程A会保存上下文数据并切走,保存上下文就是将数据单独给自己一份。

这时线程B会开始它对ticket的操作,并且线程B执行的很顺利,在线程A调度完成返回时,线程B已经完成了好几轮操作,内存中的数据被修改只剩1了,这时候线程A回来将继续执行第三步,它会将自己上下文中的数据拷贝回内存,这时候ticket又会编程99,所以多线程中并发访问是不安全的。

(二)互斥锁

通过上面的问题分析,我们要想安全的使用多线程,必须对多个线程都需要访问的共享资源进行保护,也就是将共享资源转变为临界资源。这样就可以实现线程的互斥,通常情况下我们可以使用信号量、条件变量、原子操作、互斥锁、读写锁等,今天我们利用互斥锁来实现线程互斥。

互斥锁(Mutex)是一种常见的同步机制,用于保护临界资源,确保在任何给定时间点只有一个线程能够访问临界资源。其基本原理是在进入临界区之前先锁定互斥锁,然后在退出临界区时释放锁。

1.互斥锁的初始化

我们既可以在程序中定义全局的锁,也可以定义局部的锁,如果使用全局锁,就需要

       pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

进行初始化

       #include <pthread.h>

       int pthread_mutex_init(pthread_mutex_t *restrict mutex,
           const pthread_mutexattr_t *restrict attr);

 参数

  • pthread_mutex_t *restrict mutex:一个指向锁的指针。
  • const pthread_mutexattr_t *restrict attr):是一个互斥锁属性对象的类型,用于指定互斥锁的属性,不需要设置时可以传入nullptr。

返回值: 

  • 返回值为0:表示函数执行成功,互斥锁初始化成功。
  • 返回值为正数(通常是正整数):表示函数执行失败,但没有明确的错误码定义,具体含义可以根据系统的文档或头文件进行查找。
  • 返回值为负数:表示函数执行失败,并且会返回一个标准的错误码,可以通过 errno 全局变量获取,具体的错误码可以通过查看系统头文件 <errno.h> 来获得。

2.互斥锁的释放

释放一个锁可以通过pthread_mutex_destroy() 函数来实现

       #include <pthread.h>

       int pthread_mutex_destroy(pthread_mutex_t *mutex);

返回值:

  • 返回值为0:表示函数执行成功,互斥锁销毁成功。
  • 返回值为正数(通常是正整数):表示函数执行失败,但没有明确的错误码定义,具体含义可以根据系统的文档或头文件进行查找。
  • 返回值为负数:表示函数执行失败,并且会返回一个标准的错误码,可以通过 errno 全局变量获取,具体的错误码可以通过查看系统头文件 <errno.h> 来获得。

3.互斥锁加锁

对临界资源加锁,需要使用pthread_mutex_lock()函数,它用于获取(加锁)互斥锁的函数。它的作用是在进入临界区之前,尝试获取互斥锁,如果互斥锁已经被其他线程持有,则当前线程会被阻塞,直到获取到互斥锁为止。

       #include <pthread.h>

       int pthread_mutex_lock(pthread_mutex_t *mutex);

返回值:

  • 返回值为0表示函数执行成功,当前线程成功获取了互斥锁。
  • 返回值为正数表示函数执行失败,但没有明确的错误码定义,具体含义可以根据系统的文档或头文件进行查找。
  • 返回值为负数表示函数执行失败,并且会返回一个标准的错误码,可以通过 errno 全局变量获取,具体的错误码可以通过查看系统头文件 <errno.h> 来获得。

4.互斥锁解锁

对临界资源解锁,需要使用pthread_mutex_unlock()函数,它是用于释放(解锁)互斥锁的函数。它的作用是在临界区代码执行完毕后,释放互斥锁,以便其他线程可以获取到互斥锁进入临界区执行代码。

       #include <pthread.h>

       int pthread_mutex_unlock(pthread_mutex_t *mutex);

返回值:

  • 返回值为0表示函数执行成功,互斥锁成功释放。
  • 返回值为正数表示函数执行失败,但没有明确的错误码定义,具体含义可以根据系统的文档或头文件进行查找。
  • 返回值为负数表示函数执行失败,并且会返回一个标准的错误码,可以通过 errno 全局变量获取,具体的错误码可以通过查看系统头文件 <errno.h> 来获得。

5.代码示例

pthread_mutex_t _mutex = PTHREAD_MUTEX_INITIALIZER;

int ticket = 1000;

void *StartRoutine(void *args)
{
    const string name = static_cast<char *>(args);
    while (true)
    {
        pthread_mutex_lock(&_mutex);
        if (ticket > 0)
        {
            cout << name << " get a ticket:" << ticket << endl;
            ticket--;
            sum++;
            pthread_mutex_unlock(&_mutex);
        }
        else
        {
            pthread_mutex_unlock(&_mutex);
            break;
        }
        usleep(1000);
    }
    return nullptr;
}
int main()
{
    pthread_mutex_init(&_mutex, nullptr);
    pthread_t td1, td2, td3, td4;
    pthread_create(&td1, nullptr, StartRoutine, (void *)"thread-1");
    pthread_create(&td2, nullptr, StartRoutine, (void *)"thread-2");

    pthread_join(td1, nullptr);
    pthread_join(td2, nullptr);

    pthread_mutex_destroy(&_mutex);
    return 0;
}

上面的代码利用互斥锁实现了线程的互斥,使得在多个线程抢票的时候不会出现数据竞争的问题,也就不会让票的数量出现异常。

(三)锁的本质

大多数体系结构都提供了exchange或swap命令,该指令的作用是将寄存器和内存单元的数据进行交换,这个交换过程是原子的。

将pthread_mutex_lock()函数的汇编代码抽象出来

lock:
	movb $0, %al
	xchgb %al, mutex
	if(al寄存器里的内容 > 0){
		return 0;
	} else
		挂起等待;
	goto lock;

 

xchgb作用:将一个共享的mutex资源,交换到自己的上下文中,属于线程自己 。

这段伪代码的过程可以概括为: 

  • movb $0, %al:将0赋值给al寄存器中。
  • xchgb %al, mutex :将mutex的值赋值给al寄存器,我们默认mutex是1(大于0的值)。
  • 判断al寄存器中的数据是否大于0,如果大于0返回0,代表获取锁成功;如果小于0,就挂起等待,代表锁已经被别人获取了。

再看pthread_mutex_unlock()函数

unlcok:
    movb $l,mutex 
    唤醒等待Mutex的线程;
    return 0;
  • 将线程上下文中mutex资源跟内存中的mutex交换。

我对加锁解锁的理解就是:将锁看作一把钥匙。临界区看作一间房子,钥匙原本挂在房子里,第一个进入的线程会把钥匙放到自己口袋(上下文)里,如果线程在临界区被调度走,它会把钥匙也带走并关上房门,这样别的线程想要进来但是没有钥匙,而有钥匙的线程回来时还能够进入房子里,当线程解锁,就将钥匙放回到房子里,并打开房门。

(四)死锁

1.概念

死锁是在并发系统中的一种常见问题,它指的是两个或多个进程或线程因相互持有对方所需的资源而无法继续执行的状态。在死锁状态下,各进程或线程都在等待其他进程或线程释放资源,而导致它们都无法继续执行,从而形成了一种僵局。

2.必要条件

  • 互斥条件:一个资源只能每次只能被一个执行流使用。

  • 请求与保持条件:一个执行流因为请求资源而阻塞时,对已获得的资源保持不放。

  • 不剥夺条件:一个执行流已获得的资源,在未使用之前,不能强行剥夺。

  • 循环等待条件:若干执行流之间形成一种头尾相连的循环等待资源的关系。

3.解决方法

  • 资源分配策略:设计合理的资源分配策略,避免同时持有多个资源,从而减少死锁的发生可能性。
  • 加锁顺序:确保所有进程或线程都以相同的顺序请求资源,从而避免形成循环等待。
  • 超时机制:对于资源请求,设置超时机制,如果超过一定时间仍未能获取资源,则放弃当前请求,避免长时间等待而导致死锁。
  • 死锁检测和解除:定期检测系统中是否存在死锁,并采取相应的措施来解除死锁,例如终止部分进程或线程,释放资源等。

(五)线程同步

1.同步概念

在上面的互斥示例程序中,我们用抢票的例子来实现线程互斥,可是在打印时却有一个现象:总是有一个线程会抢占大多数的票,导致了其他线程一直在等待,抢不上票。这种现象叫做线程饥饿问题,指的是一个或多个线程无法获得所需的资源或者无法被调度执行而长时间等待

那么如何解决饥饿问题呢?这就需要同步了。接下来我用一个比喻来理解线程同步。

假设有一个VIP自习室,每次只能让一个同学进入学习,只要自习室内有人,别的同学就无法进入

 张三在一天的早上6点第一个到达自习室,并且一直在自习室里学习,这就表示张三一直在使用里面的资源。到了中午12点,张三想去吃饭,但是吃饭出去的话就必须将钥匙归还,然后吃完饭回来就得等待其他同学出来,可张三并不想等,所以张三又饿又不想出去,他将钥匙放回去又拿回来,在自习室里反复横跳。最后外面的同学一直没等到钥匙,没吃上饭,而张三一直持有钥匙,也没吃上饭,这就导致了其他同学的饥饿问题。

管理人员知道了这件事,定了下面两条规矩:

  • 刚把钥匙归还的同学不能再次立即申请钥匙。
  • 在外面等待钥匙的同学必须排队。

定了这两条规矩,张三再也不会反复横跳了,也就不会导致饥饿问题了。这就是利用线程同步。

线程同步是指在多线程环境中,对共享资源的访问进行协调和管理,以确保线程之间的正确交互和数据一致性。在并发编程中,线程同步是至关重要的,因为多个线程同时访问共享资源可能导致数据竞争和不确定性的结果。

2.条件变量

条件变量是在多线程编程环境中用来线程通信的机制,它通常和互斥锁使用来实现线程同步的效果。

条件变量实现了线程的等待和通知机制:

  • 等待(Wait):线程在等待条件变量时会释放它所持有的互斥锁,并进入阻塞状态,直到其他线程通知条件变量满足了某个条件。

  • 通知(Notify):线程在某个条件发生变化时可以通过条件变量通知等待条件变量的一个或多个线程,以唤醒它们继续执行。

2.1创建和销毁

       #include <pthread.h>

       int pthread_cond_destroy(pthread_cond_t *cond);
       int pthread_cond_init(pthread_cond_t *restrict cond,
           const pthread_condattr_t *restrict attr);

2.2等待

       #include <pthread.h>

       int pthread_cond_wait(pthread_cond_t *restrict cond,
           pthread_mutex_t *restrict mutex);

 pthread_cond_wait 函数会使当前线程等待在指定的条件变量 cond 上,同时会释放传入的互斥锁 mutex,并将当前线程置于等待状态,直到有其他线程调用 pthread_cond_signalpthread_cond_broadcast 来唤醒它,或者出现了异常情况(如信号中断)。

2.3唤醒

       #include <pthread.h>

       int pthread_cond_signal(pthread_cond_t *cond);
       #include <pthread.h>

       int pthread_cond_broadcast(pthread_cond_t *cond);
  1. pthread_cond_signal

    • pthread_cond_signal函数用于唤醒等待在条件变量上的一个线程。
    • 如果有多个线程等待在条件变量上,调用pthread_cond_signal只会唤醒其中一个线程,具体唤醒哪个线程由系统决定(通常是按照先等待先唤醒的顺序)。
    • 如果没有线程等待在条件变量上,调用pthread_cond_signal也不会产生任何效果。
  2. pthread_cond_broadcast

    • pthread_cond_broadcast函数用于唤醒等待在条件变量上的所有线程。
    • 调用pthread_cond_broadcast会唤醒所有等待在条件变量上的线程,使它们都可以继续执行。
    • 如果没有线程等待在条件变量上,调用pthread_cond_broadcast也不会产生任何效果。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1674109.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Unity】 HTFramework框架(四十八)使用Location设置Transform位置、旋转、缩放

更新日期&#xff1a;2024年5月14日。 Github源码&#xff1a;[点我获取源码] Gitee源码&#xff1a;[点我获取源码] 索引 Location定义Location复制Location变量的值复制Transform组件的Location值粘贴Location变量的值粘贴Location值到Transform组件在代码中使用Location Loc…

GPT-4o omni全能 openAI新flagship旗舰模型,可以通过音频、视觉、文本推理。自然人机交互,听懂背景噪音、笑声、歌声或表达情感,也能输出。

新旗舰模型GPT-4o GPT-4o 是openAI新flagship旗舰模型&#xff0c;可以通过音频、视觉、文本推理reason&#xff0c;也能组合输出text, audio, and image。 接受文本、音频和图像的任意组合作为输入&#xff0c;并生成文本、音频和图像输出的任意组合。 速度快 2 倍&#xff…

华火5.0台嵌式喷火电燃单灶,更懂未来生活需求

在厨电技术不断革新的今天&#xff0c;第五代华火电燃灶以其独特的技术升级和卓越性能&#xff0c;成功吸引了市场的广泛关注。作为华火品牌的最新力作&#xff0c;第五代电燃灶不仅继承了前代产品的优点&#xff0c;更在多个方面进行了显著的升级和创新。下面&#xff0c;我们…

PXI/PXIe规格 A429/717 航电总线适配卡

A429是一款标准的PXI/PXIe1规格的多协议总线适配卡。该产品最多支持36个A429通道&#xff0c;或32个A429通道加4个A717通道&#xff0c;每个A429和A717通道可由软件配置成接收或发送&#xff0c;可满足A429总线和A717总线的通讯、测试和数据分析等应用需求。 该产品的每个A429通…

Simulink|虚拟同步发电机(VSG)惯量阻尼自适应控制仿真模型

主要内容 该模型为simulink仿真模型&#xff0c;主要实现的内容如下&#xff1a; 随着风力发电、光伏发电等新能源发电渗透率增加&#xff0c;电力系统的等效惯量和等效阻尼逐渐减小&#xff0c;其稳定性问题变得越来越严峻。虚拟同步发电机&#xff08;VSG&#xff09;技…

Django项目之电商购物商城 -- 修改/删除收货地址/设置默认地址

Django项目之电商购物商城 – 修改/删除收货地址/设置默认地址 修改和删除收货地址依旧实在user应用下进行 , 其思路和新增收货地址非常相似 依旧是更具前端的数据来写 在这里修改和删除地址的URL是相同的 , 所以我们只要设置一个模型类就可以实现这两个功能 一 . 修改地址…

Go 多模块工作区处理一个go项目下有多个module(即多个go.mod)的情况

背景 在现在微服务盛行的年代&#xff0c;一个人会维护多个代码仓库&#xff0c;很多的时候是多个仓库进行同时开发&#xff0c;也就是在当前项目下有多个目录&#xff0c;每个目录对应一个微服务&#xff0c;每个微服务都有一个go.mod文件。那么我在其中一个目录下要怎么导入…

4.Jmeter阶梯加压Stepping Thread Group

1. 先去Jmeter下载地址下载PluginsManager&#xff0c;放置在Jmeter的lib/ext 目录下 &#xff0c;重启Jmeter 2. 在插件管理器查找并安装jpgc - Standard Set,重启Jmeter 3.右键测试计划->添加->Threads(Users)->jpgc - Stepping Thread Group 然后设置阶梯加压参数…

java中不可变对象使用避坑

总结&#xff1a; 1&#xff0c;不要大量使用不可变对象和不可边对象提供的方法&#xff08;每次调用不可变对象的修改方法会创建出新的对象出来&#xff0c;导致频繁的YGC&#xff09; 2&#xff0c;计算密集型任务不要使用包装类&#xff08;包装类体积大&#xff0c;数据密度…

数据中心逆变电源的功率容量计算方法

随着信息技术的快速发展&#xff0c;数据中心在现代社会中的地位日益凸显&#xff0c;各种企业和机构对数据中心的依赖程度也越来越高。而电源作为数据中心的核心基础设施&#xff0c;其可靠性和高效性直接影响着数据中心的稳定运行。因此&#xff0c;如何设计一款性能优越、可…

OpenAI 今日(北京时间 5 月 14 日凌晨两点)将发布的大更新,不是 GPT-5,也不是搜索引擎

&#x1f989; AI新闻 &#x1f680; OpenAI 今日&#xff08;5月13日&#xff09;将发布的大更新&#xff0c;不是 GPT-5&#xff0c;也不是搜索引擎 摘要&#xff1a;OpenAI 预计即将推出一款新的 AI 语音助手&#xff0c;该助手不仅可以进行语音和文字交流&#xff0c;还能…

【JavaScript】---- 使用 Tween 实现转盘抽奖

1. 实现效果 2. 需求分析 它和正常的转盘抽奖不一样&#xff0c;一般实现都是指针形式的&#xff0c;转盘转动&#xff0c;最后指针停留在奖品的随机位置&#xff1b;通过上边图发现奖品必须刚好停留在奖品的位置&#xff0c;因为不是指针&#xff0c;所以不能最后落到随机位置…

伦敦银晚盘预测方法:以经济数据为基础

晚盘是指北京时间晚上8点到凌晨的这个时段&#xff0c;覆盖了部分欧盘和大部分的美盘。一般来说&#xff0c;这个时段有欧美方面&#xff08;主要是美国&#xff09;的经济数据公布&#xff0c;其中一些重要的数据&#xff0c;如通胀数据、美联储公布利率决议等等&#xff0c;会…

企业为什么需要HTTPS

一.什么是HTTPS HTTPS &#xff08;全称&#xff1a;Hyper Text Transfer Protocol over SecureSocket Layer&#xff09;&#xff0c;是以安全为目标的 HTTP 通道&#xff0c;在HTTP的基础上通过传输加密和身份认证保证了传输过程的安全性 。HTTPS 在HTTP 的基础下加入SSL&a…

winserver系统设置图片查看器

新建 .bat 批处理执行文件&#xff0c;内容如下&#xff1a; echo off&cd&color 0a&cls echo Set Win10 Photo Viewer reg add "HKLM\SOFTWARE\Microsoft\Windows Photo Viewer\Capabilities\FileAssociations" /v ".jpg" /t REG_SZ /d Photo…

centos7.8 迁移为 TencentOS Server 2.4(TK4) 报错解决

文章目录 一 问题二 解决三 注意 一 问题 CentOS 官方计划停止维护 CentOS Linux 项目&#xff0c;公司某台腾讯云的centos7.8服务器计划迁移为 TencentOS Server 2.4(TK4) 。在下载rpm包执行迁移命令后报错&#xff0c;场景还原如下。 首先 安装 Python 3 yum install -y p…

Emby for Mac(轻松管理多媒体影音库)1.9.9中文版

Emby for Mac是一款强大的多媒体影音库管理工具&#xff0c;可以帮助用户轻松管理和浏览自己的影音资源。它可以将用户的个人视频、音乐和照片组合在一起&#xff0c;并将其流式传输到用户的设备上。 Emby for Mac 1.9.9中文版下载 Emby for Mac具有易于使用的界面&#xff0c;…

WMS系统业务学习

电商ERP 供应链整体流程 从采购到付款 需求—采购申请—报价请求—供应商报价—采购订单—接收—付款。 从销售到收款 销售定价—订单—发运—收款—开票。 WMS业务 收货管理 业务流程&#xff1a;采购订单—到货预约—货物交接—&#xff08;质检&#xff09;—上架。 …

【计算机毕业设计】ssm绿色农产品推广应用网站

21世纪的今天&#xff0c;随着社会的不断发展与进步&#xff0c;人们对于信息科学化的认识&#xff0c;已由低层次向高层次发展&#xff0c;由原来的感性认识向理性认识提高&#xff0c;管理工作的 重要性已逐渐被人们所认识&#xff0c;科学化的管理&#xff0c;使信息存储达到…