Liunx下线程互斥与同步

news2024/9/24 17:14:44

文章目录

  • 前言
  • 1.线程相关问题
  • 2.加锁操作
    • 1.相关接口
    • 2.加锁原理
  • 3.线程安全
  • 4.线程同步

前言

本文主要围绕Linux下线程互斥问题进行相关讲解,同时也会线程同步相关问题。

1.线程相关问题

我们知道进程地址空间很多资源是被线程共享的。线程在并发访问这些共享资源的时候,如果不加以保护就可能会出现问题。我们看到如下的代码:

#include<iostream>
#include<unistd.h>
#include<string>
#include<pthread.h>
using namespace std;
int tickets=1000;
void *thread_run(void* name)
{
    string tname=static_cast<const char*>(name);
    while(1)
    {
        if(tickets>0)
        {  
            //模拟抢票花费时间
          usleep(2000);
          cout<<tname<<"get a ticket: "<<tickets--<<endl;
            
        }

        else
        {
            break;
        }
    }
    return nullptr;
}

int main()
{
   pthread_t t[4];
   int n=sizeof(t)/sizeof(t[0]);
   for(int i=0;i<n;i++)
   {
    char *data=new char[64];
    snprintf(data,64,"thread -%d",i+1);
    pthread_create(t+i,nullptr,thread_run,data);
   }

   for(int i=0;i<n;i++)
   {
    pthread_join(t[i],nullptr);
   }
   return 0;

}

在这里插入图片描述

上述代码是模拟抢票逻辑,票数是是全局变量,对于线程来说就是共享资源。在各个线程执行抢票逻辑的时候都是会去访问这个资源,在没有保护的措施的前提下执行上述代码就出现了问题。这就是多线程的并发访问带来的问题。

为啥会出现这种问题呢?因为这个ticket–操作不是原子性的。当cpu调度执行上述代码时,ticket实际上是有3个步骤的。

在这里插入图片描述

ticket减减操作不是原子性的,线程每次对ticket操作完后被cpu切走的时候都会有相应的寄存器保留当前执行状态的上下文。直到被cpu重新调度后,接着原来的线程上下文继续执行。这样一来如果有个线程执行的时候没有把寄存器的值重新写会内存中就被切走了,下一个线程看到这个ticket变量的时候还是原来的值,减到最后就可能出现上述代码的问题。

在这里插入图片描述

这里我们就看到了如果不加保护的去访问公共资源,各个线程就会产生数据不一致问题,从而造成代码有问题。这个时候我们就需要加锁来访问临界资源。加锁之后我们可以保证线程对ticket访问是互斥且原子的。

2.加锁操作

1.相关接口

加锁操作可以让线程访问临界资源是原子的。让线程串行化执行相关代码,线程库中提供了这种互斥锁变量。pthread_mutex_t mutex,这里的pthread_mutex_t 是线程库中自定义的类型,表示互斥锁。mutex是我随便起的变量名。

在这里插入图片描述

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

第一个产生mutex:需要初始化的互斥锁,第二人个参数attr:初始化互斥量的属性 一般设置为NULL即可

当我们定义一个全局变量锁或者静态变量锁的时候,可以直接使用PTHREAD_MUTEX_INITIALIZER这个宏去初始化锁。这样初始化锁之后不用调用相关函数去摧毁锁。

int pthread_mutex_destroy(pthread_mutex_t *mutex)

这个函数是用来摧毁锁的,当确保锁不再使用后就要及时的摧毁锁。mutex:需要摧毁的互斥量

int pthread_mutex_lock(pthread_mutex_t *mutex);

加锁函数,参数mutex:需要加锁的互斥量,调用 pthread_ lock 时,可能会遇到以下情况:互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功,发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么pthread_ lock调用会陷入阻塞(执行流被挂起),等待互斥量解锁。

int pthread_mutex_unlock(pthread_mutex_t *mutex);

解锁函数,参数mutex:需要加锁的互斥量。一般加锁和解锁是成对存在的,加锁之后需要及时解锁。

代码示例

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

int ticktets=1000;
class TData
{
public:
   TData(const string &name,pthread_mutex_t* mutex)
   :_name(name),_pmutex(mutex){}
   ~TData(){}
public:
    string _name;
    pthread_mutex_t *_pmutex;


};
void *runnig(void *arg)
{   
    TData* td=static_cast<TData*>(arg);
    while(1)
    {   pthread_mutex_lock(td->_pmutex);
        if(ticktets>0)
        { //模拟抢票花费时间
          usleep(2000);
          cout<<td->_name<<"get a ticket: "<<ticktets--<<endl;
          pthread_mutex_unlock(td->_pmutex);
        }
        else
        {    
            pthread_mutex_unlock(td->_pmutex);
            break;
        }
    }
    return nullptr;
}
int main()
{  
   pthread_mutex_t mutex;
   pthread_mutex_init(&mutex,nullptr);
   pthread_t t[4];
   int n=sizeof(t)/sizeof(t[0]);
   for(int i=0;i<n;i++)
   {
     char name [64];
    snprintf(name,64,"thread -%d",i+1);
    TData* td=new TData(name,&mutex);
    pthread_create(t+i,nullptr,runnig,td);
   }

   for(int i=0;i<n;i++)
   {
    pthread_join(t[i],nullptr);
   }
   pthread_mutex_destroy(&mutex);
   return 0;
}

在这里插入图片描述

我们加锁之后就不用担心会出现问题了,锁本身就是属于公共资源,因此加锁和解锁的操作本身就是原子的。这个锁是用来保护临界资源的,锁本身就得先保证自己没问题才能去保护临界资源。

2.加锁原理

对应线程来说,只有加锁和没上锁两种状态,这就保证了原子性。当持有的锁的线程被切走了,只要改线程没有归还锁也就是解锁,其他线程来访问临界资源的时候依旧被拒之门外。这样就保证了线程串行化的访问临界资源。

为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的 总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。

在这里插入图片描述

上图就是加锁解锁的伪代码,其实锁可以看作变量1,当某个线程申请到锁之后,在没有规划锁的情况下就被切走了,这个时候其他线程来申请锁的时候,由于mutex这个1被拿走了,这个时候mutex存放只是0.其他线程以为申请到了1但是拿到的只是0而已。这样底层汇编在判断的时候对于没有拿到1的线程就会将其挂起。这个1就相当于是一把钥匙,这也就是加锁解锁的原理。

3.线程安全

线程安全:多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作,并且没有锁保护的情况下,会出现该问题。重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数。这个函数是否可重入只是函数的特性而已,不能作为判断函数好坏的依据。

常见不可重入的情况

调用了malloc/free函数,因为malloc函数是用全局链表来管理堆的
调用了标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构
可重入函数体内使用了静态的数据结构

常见可重入的情况

不使用全局变量或静态变量
不使用用malloc或者new开辟出的空间
不调用不可重入函数
不返回静态或全局数据,所有数据都有函数的调用者提供
使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据

常见的线程安全的情况

每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的,或者接口对于线程来说都是原子操作多个线程之间的切换不会导致该接口的执行结果存在二义性。

可重入与线程安全联系

函数是可重入的,那就是线程安全的函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题 如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的。

可重入与线程安全区别

可重入函数是线程安全函数的一种.线程安全不一定是可重入的,而可重入函数则一定是线程安全的。如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生死锁,因此是不可重入的。

死锁现象

死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态。锁死就像是小红和小明各有5毛钱,他们都想要彼此的5毛钱去买辣条,从而陷入等待状态。造成锁死的必要的条件有4个:互斥条件:一个资源每次只能被一个执行流使用;请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放;不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺;循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系.

消除死锁的方法

避免死锁:破坏死锁的四个必要条件,加锁顺序一致,避免锁未释放的场景,资源一次性分配。

4.线程同步

之前抢票代码为例,假如票数为0后,过段时间后重新放票,线程不断加锁解锁去访问临界资源,当票数为0时,众多线程还是这样去做无用功,线程只能等待放票,从而造成饥饿现象。这样无疑是很影响效率的。由此就提出的线程同步的概念,当访问资源的条件不满足的时候,可以让相关线程进行休眠,等道条件满足时在将线程唤醒,去重新竞争申请资源。这样就可以提到效率。线程同步的主要通过条件变量来实现的,在保证线程安全的前提下,线程有序的访问资源,提高执行效率。

条件变量

概念: 用来描述某种临界资源是否就绪的一种数据化描述,条件变量通常需要配合mutex互斥锁一起使用。它的动作主要有两个:一个线程等待条件变量的条件成立而被挂起,另一个线程使条件成立后唤醒等待的线程。

相关接口

pthread_cond_t cond;

定义一个条件变量cond,它的类型是pthread_cond_t.

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

初始化条件变量的函数,参数说明cond:需要初始化的条件变量attr:初始化条件变量的属性 一般设置为NULL即可。另外定义全局或者静态条件变量可以宏PTHREAD_COND_INITIALIZER初始化,这样初始化不用调用相关函数销毁不用的条件变量。

int pthread_cond_destroy(pthread_cond_t *cond);

销毁条件变量的函数,参数说明:cond:需要销毁的条件变量

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

等待条件变量函数,参数说明:cond:需要等待的条件变量,mutex:当前线程所处临界区对应的互斥锁。

int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);

唤醒等待的函数,pthread_cond_signal函数用于唤醒等待队列中首个线程,pthread_cond_broadcast函数用于唤醒等待队列中的全部线程。参数说明:cond:唤醒在cond条件变量下等待的线程。

代码示例

#include<iostream>
#include<pthread.h>
#include<string>
#include<unistd.h>
using namespace std;
const int num=5;

pthread_cond_t cond=PTHREAD_COND_INITIALIZER;
pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;
void *active(void*arg)
{
    string name=static_cast<const char*>(arg);
    while(1)
    {   
        pthread_mutex_lock(&mutex);
        pthread_cond_wait(&cond,&mutex);
        cout<<name<<"启动"<<endl;
        pthread_mutex_unlock(&mutex);
    }
}
int main()
{
   pthread_t tids[num];
   for(int i=0;i<num;i++)
   {
    char *name=new char[32];
    snprintf(name,32,"thread -%d",i);
    pthread_create(tids+i,nullptr,active,name);
   }

   sleep(3);
   while(1)
   {
    cout<<"main thread wakeup thread..."<<endl;
    pthread_cond_signal(&cond);//每次唤醒一个线程
    //pthread_cond_broadcast(&cond);//唤醒全部线程

    sleep(1);
   }
   
   for(int i=0;i<num;i++)
   {
    pthread_join(tids[i],nullptr);
   }

   return 0;
}

每次唤醒一个线程
在这里插入图片描述
唤醒全部线程
在这里插入图片描述
为什么pthread_cond_wait需要互斥锁

简单来说,条件等待是线程间同步的一种手段,使用条件变量的场景肯定是不止一个线程,必须会有一个线程通过某些操作,改变共享变量,使原先不满足的条件变得满足,并且友好的通知等待在条件变量上的线程将其唤醒,条件不会无缘无故的突然变得满足了,必然会牵扯到共享数据的变化,所以一定要用互斥锁来保护,没有互斥锁就无法安全的获取和修改共享数据,当线程进入等待的时候这个锁也必须要释放,不然其他满足条件的线程无法进入临界区访问临界资源。
等待的时候往往是在临界区内等待的,当该线程进入等待的时候,互斥锁会自动释放,而当该线程被唤醒时,又会自动获得对应的互斥锁,条件变量需要配合互斥锁使用,其中条件变量是用来完成同步的,而互斥锁是用来完成互斥的。pthread_cond_wait函数有两个功能,一是让线程在特定的条件变量下进行等待,二是让线程释放掉自己申请到的互斥锁。当该线程被唤醒后,该线程会立马获得之前释放的互斥锁,然后继续向下执行。

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

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

相关文章

代码随想录day11

20. 有效的括号 思路&#xff1a;这里用模拟栈的方法会更好理解&#xff0c;也就是我们每次遇到朝左方向的三种类型的时候&#xff0c;就加入相反方向的右括号到result栈中。由于栈是一个先进后出的方式&#xff0c;所以我们会有一个判断stack当前为不为空&#xff0c;和stack[…

4G理论概述

文章目录 LTE网络基础架构UMTS&#xff08;通用移动通信系统&#xff0c;Universal Mobile Telecommunications System&#xff09;UTRAN&#xff08;UMTS陆地无线接入网&#xff0c;UMTS Terrestrial Radio Access Network&#xff09;RNC&#xff08;无线网络控制器&#xff…

Vue的响应式实现思路及源码分析

Vue # 思路 new Vue() 首先执行初始化,对 data 执行响应化处理,这个过程发生在 Observer 中同时对模板执行编译,找到其中动态绑定的数据,从 data 中获取并初始化视图,这个过程发生在 compile 中同时定义一个 更新函数 和 Watcher,将来对应数据变化时 Watcher 会调用 更新…

python编程——字符串讲解

作者&#xff1a;Insist-- 个人主页&#xff1a;insist--个人主页 本文专栏&#xff1a;python专栏 专栏介绍&#xff1a;本专栏为免费专栏&#xff0c;并且会持续更新python基础知识&#xff0c;欢迎各位订阅关注。 前言 本文将介绍python字符串是什么&#xff1f;以及它的几…

C++11(一)(列表初始化,变量类型推导(auto,decltype),nullptr,范围for循环等)

目录 C11简介 列表初始化 C98中,{}的初始化问题 内置类型的列表初始化 自定义类型的列表初始化 变量类型推导 auto decltype nullptr 范围for循环 final和override 默认成员函数的控制 显式缺省函数 删除默认函数 C11简介 在2003年C标准委员会曾经提交了一份技…

STM32F767-0-HAL库主从定时器产生固定数量的PWM

STM32F767-0-主从定时器产生固定数量的PWM 前言一、配置STM32F767主从定时器的详细步骤1.1 选择主定时器和从定时器&#xff1a;1.2 配置主定时器&#xff08;主从模式&#xff09;&#xff1a;1.2.1 设置主定时器的时钟源&#xff1a;1.2.2 配置主定时器的分频系数&#xff1a…

不同路径(力扣)动态规划 JAVA

一个机器人位于一个 m x n 网格的左上角 &#xff08;起始点在下图中标记为 “Start” &#xff09;。 机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角&#xff08;在下图中标记为 “Finish” &#xff09;。 问总共有多少条不同的路径&#xff1f; 示例 1&a…

JWT的简单说明与使用

简要 JWT是"JSON Web Token"的缩写&#xff0c;是一种用于在不同系统之间传输信息的开放标准。它通过将信息进行加密后生成一个安全的令牌&#xff0c;以便在网络请求中进行身份验证和授权。 具体来说&#xff0c;JWT可以用于以下几个方面&#xff1a; 身份验证&…

CMakeLists.txt 语法介绍

CMake编译原理 CMake是一种跨平台编译工具,主要编写CMakeLists.txt文件,然后用cmake命令将CMakeLists.txt文件转化为make所需要的makefile文件&#xff0c;最后用make命令编译源码生成可执行程序或共享库.因此CMake的编译基本就两个步骤&#xff1a;cmake && make cm…

MySQL基础练习

Ⅰ Ⅱ 3.1 3.2 3.3 3.4 -- 单表查询练习 /* 素材 CREATE TABLE emp ( empno int(4) NOT NULL, ename varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, job varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, …

细数那些Compose新手容易犯的错误

作者&#xff1a;晴天小庭 笔者作为一个日常Jetpack Compose开发者&#xff0c;对Compose的理解也在逐渐加深中&#xff0c;最近回顾当初学习和实践的过程中&#xff0c;犯了不少错误和踩了很多坑&#xff0c;本篇文章作为小总结分享给大家&#xff0c;同时文章会持续更新&…

Leetcode-每日一题【1669.合并两个链表】

题目 给你两个链表 list1 和 list2 &#xff0c;它们包含的元素分别为 n 个和 m 个。 请你将 list1 中下标从 a 到 b 的全部节点都删除&#xff0c;并将list2 接在被删除节点的位置。 下图中蓝色边和节点展示了操作后的结果&#xff1a; 请你返回结果链表的头指针。 示例 1…

Unity游戏C# dll注入

案例游戏下载 首先在网上下载个游戏案例&#xff0c;我就以Captain Molly游戏为例。 这个游戏玩家默认生命值有5点&#xff0c;咱们通过dll注入修改为10点。 dnSpy 我使用dnSpy来查看Unity游戏生成的dll代码&#xff0c;当然你们也可以使用其他工具。 Unity游戏脚本代码最终…

Codeforces-Round-883-Div-3

Codeforces Round 883 (Div. 3) 链接&#xff1a;https://codeforces.com/contest/1846 A. Rudolph and Cut the Rope There are n n n nails driven into the wall, the i i i-th nail is driven a i a_i ai​ meters above the ground, one end of the b i b_i bi​ m…

Pinia: vue的最新状态管理库

Pinia: vue的最新状态管理库&#xff0c;vuex状态管理工具的替代品。 pinia官方文档 注意defineStore()的返回值还是一个方法&#xff0c;所以useCounterStore是一个方法&#xff0c;执行该方法得到一个对象。 getters: 异步action: storeToRefs: 补充 vuex&#xff…

20中文字符识别(matlab程序)

1.简述 随着计算机科学的飞速发展&#xff0c;以图像为主的多媒体信息迅速成为重要的信息传递媒介&#xff0c;在图像中&#xff0c;文字信息(如新闻标题等字幕) 包含了丰富的高层语义信息&#xff0c;提取出这些文字&#xff0c;对于图像高层语义的理解、索引和检索非常有帮助…

Zero-Shot Node Classification

零样本节点分类(Zero-shot node classification) 谱图卷积 图卷积网络 GCN的分解

pandas中比较两个对象相等性 .eq()函数

在使用pandas做数据分析时&#xff0c;往往我们会有这样的数据需求&#xff1a;为某有某一属性的客户打标签。此刻&#xff0c;.eq()函数&#xff0c;就可以实现它自身的价值。 Lets go&#xff0c;一起去探索它的神秘力量吧&#xff01; 先讲讲它的用途&#xff1a;可以用于…

银河麒麟高级服务器系统部署-尚文网络xUP楠哥

进Q群11372462领取专属报名福利! # 什么是银河麒麟 银河麒麟高级服务器操作系统是针对企业级关键业务&#xff0c;适应虚拟化、云计算、大数据、工业互联网时代对主机系统可靠性、安全性、性能、扩展性和实时性等需求&#xff0c;依据CMMI5级标准研制的提供内生本质安全、云原…