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

前言:

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

互斥:

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

同步:

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

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

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

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也不会产生任何效果。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

福建聚鼎:做装饰画到底能不能赚钱

在探讨做装饰画能否成为盈利的行当之前&#xff0c;我们必须认识到任何一门艺术或手工的价值并非仅仅取决于其直接的经济收益。艺术创作本身就是一种文化传承和个人情感表达的方式&#xff0c;它对创作者和社会都有着不可估量的精神价值。然而&#xff0c;将话题限定在经济回报…

【机器学习300问】81、什么是动量梯度下降算法?

动量梯度下降算法&#xff08;Momentum&#xff09;是利用指数加权移动平均的思想来实现梯度下降的算法。让我们先来回顾一下基础的梯度下降方法以及看看它有哪些不足之处。接着引出动量梯度下降算法&#xff0c;在理解了它的原理后看看它是如何规避之前方法的不足的。 如果不知…

Java开发大厂面试第01讲:String 的特点及其重要的方法都有哪些?

几乎所有的 Java 面试都是以 String 开始的&#xff0c;如果第一个问题没有回答好&#xff0c;则会给面试官留下非常不好的第一印象&#xff0c;而糟糕的第一印象则会直接影响到自己的面试结果&#xff0c;就好像刚破壳的小鹅一样&#xff0c;会把第一眼看到的动物当成自己的母…

制药行业新突破:CANOpen转PROFINET网关配置案例解析

在药品制造工业环境中&#xff0c;实现CanOpen转Profinet协议之间转换的网关配置是一个关键过程&#xff0c;确保了不同通信协议的设备能够互相协作。以开疆智能CanOpen转Profinet网关为例&#xff0c;以下是其配置流程&#xff1a;首先安装CanOpen转Profinet网关的配置软件&am…

Linux禁用危险命令和防止误操作

禁用rm命令 编辑/etc/profile文件&#xff0c;结尾添加 ###### rm prevent ###### alias rmecho can not use rm command使用source命令生效 source /etc/profile效果 使用mv命令代替rm命令 将需要删除的文件移动到特定的目录&#xff0c;比如/home/sharedir/ 在.bashrc目…

波卡 2024 一季度报告:XCM 创下历史新高,JAM 链将引领 Polkadot 2.0 新风向

作者&#xff1a;Nicholas Garcia&#xff5c;Messari 研究分析师 编译&#xff1a;OneBlock 原文&#xff1a;https://messari.io/report/state-of-polkadot-q1-2024 近期&#xff0c;Messari Crypto 发布了 Polkadot 2024 年 Q1 状况的数据报告。OneBlock 为你梳理了本篇报…

python批量为图片做灰度处理

欢迎关注我👆,收藏下次不迷路┗|`O′|┛ 嗷~~ 目录 一.前言 二.代码 三.使用 四.总结

MES管理系统在柔性制造中有何重要作用

在当今这个瞬息万变的商业环境中&#xff0c;制造业正经历着一场前所未有的转型。消费者需求的多样化和市场动态的快速变化要求企业必须具备高度的灵活性和适应性。为了应对这些挑战&#xff0c;柔性制造策略应运而生&#xff0c;它以其快速响应和灵活调整的能力&#xff0c;成…

vue3中的watch侦听器

在有些情况下&#xff0c;我们需要在状态变化时执行一些“副作用”&#xff1a;例如更改 DOM &#xff0c;或是根据异步操作的结果去修改另一处的状态。在组合式 API 中&#xff0c;我们可以使用 watch 函数在每次响应式状态发生变化时触发回调函数。 watch 函数可以侦听被 ref…

计算机的一些基础知识分享

windows操作系统中&#xff0c;用于查看当前文件下的目录是&#xff1f; 在Windows操作系统中&#xff0c;如果您想要查看当前文件夹下的目录&#xff0c;您可以使用命令提示符&#xff08;CMD&#xff09;或PowerShell。在这些环境中&#xff0c;可以使用以下命令&#xff1a;…

bmi088-linux驱动(I2C)

电气特性&#xff1a; 在正常工作时&#xff0c;gyro 工作电流为5mA&#xff0c;acc 工作电流为150uA。 SPI 时钟和数据电平范围 0 -3.6 结构框图如下&#xff1a; 硬件连接图如下&#xff1a; note&#xff1a; 1. 通过PS引脚选择通讯协议&#xff0c;上拉引脚则选择的是I2C…

HCIP的学习(16)

BGP的状态机 ​ OSPF的状态机是在描述整个协议的完整工作过程&#xff0c;而BGP的状态机仅描述的是对等体关系建立过程中的状态变化。-----因为BGP将邻居建立过程以及BGP路由收发过程完全隔离。 ​ IGP协议在启动后&#xff0c;需要通过network命令激活接口&#xff0c;从而使…

企业运维背后的故事:TASKCTL带你了解日常工作与技术演进

今天&#xff0c;作为一名经验丰富、从业多年经常与运维人员打交道的人&#xff0c;我想与大家聊聊运维的日常工作、部门协调以及未来发展&#xff0c;希望能为即将转行或正在从事运维工作的你&#xff0c;提供一些新的视角和启发。 运维的日常工作&#xff1a;挑战与乐趣并存 …

朱啸虎:AI应用明年肯定大爆发;第3款爆火AI游戏出现了;AI应用定价策略「不能说的秘密」;人类数据不够用了怎么办 | ShowMeAI日报

&#x1f440;日报&周刊合集 | &#x1f3a1;生产力工具与行业应用大全 | &#x1f9e1; 点赞关注评论拜托啦&#xff01; 1. 换你来当爹&#xff1a;国内第3款爆火出圈的AI游戏应用&#xff0c;hhh 太搞笑了 周末的时候&#xff0c;社群里伙伴们开始玩一款「换你来当爹」的…

[Java EE] 多线程(九):JUC剩余部分与线程安全的集合类(多线程完结)

&#x1f338;个人主页:https://blog.csdn.net/2301_80050796?spm1000.2115.3001.5343 &#x1f3f5;️热门专栏:&#x1f355; Collection与数据结构 (91平均质量分)https://blog.csdn.net/2301_80050796/category_12621348.html?spm1001.2014.3001.5482 &#x1f9c0;Java …

群晖 Synology DSM7 过热关机解决方法

最近已经提示我过热关机过两次了&#xff0c;这两次一次是用虚拟机&#xff0c;一次是批量使用Synology Photos批量上传照片&#xff1a; 但是我没有对主机进行任何的位置移动以及硬件修改操作&#xff0c;散热环境没有发生变化。以前使用从来没有出现过这个问题&#xff0c;同…

MySQL Workbench创建数据库和景点评价表

创建一个数据库和一张用于存储景点评价的表。 一 创建数据库 1.双击选择 local instance MySQL80 2. 输入密码 连接成功 3. 创建 mydatabase数据库 4.确认创建 mydatabase数据库 5.选择finish 6.选择 Schemas查看刚才创建的mydatabast数据库 二、创建表 1.创建表 2.设置表信…

视觉SLAM十四讲:从理论到实践(Chapter3:三维空间刚体运动)

前言 学习笔记&#xff0c;仅供学习&#xff0c;不做商用&#xff0c;如有侵权&#xff0c;联系我删除即可 目标 理解三维空间的刚体运动描述方式&#xff1a;旋转矩阵、变换矩阵、四元数和欧拉角。掌握Eigen库的矩阵、几何模块的使用方法。 3.1 旋转矩阵 3.1.1 点、向量和…

建立一物一码数字化营销体系,纳宝科技助力五丰黎红在调味品行业再创佳绩!

五丰黎红隶属于华润五丰集团&#xff0c;公司历史可溯源至1979年&#xff0c;前身是汉源花椒油厂&#xff0c;是一家拥有悠久历史的调味品品牌。一直以来&#xff0c;五丰黎红坚持调味品原料、研发、生产、加工一体化的全产业链经营模式&#xff0c;以“质量”为核心&#xff0…

快捷自由定时重启、注销、关机

首先&#xff0c;需要用到的这个工具&#xff1a; 度娘网盘 提取码&#xff1a;qwu2 蓝奏云 提取码&#xff1a;2r1z 1、打开工具&#xff0c;进入定时器编辑版块 2、左侧目录新建一个定时器 3、选择需要的周期&#xff0c;这里是每天0点&#xff0c;一次执行一条 4、添加具…