Linux——多线程,互斥与同步

news2024/11/26 1:29:16

目录

一.linux互斥

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

2.互斥量mutex

3.加锁互斥锁mutex

4.锁的底层原理

 二.可重入VS线程安全

1.概念

2.常见的线程不安全的情况

3.常见的线程安全的情况 

4.常见不可重入的情况 

5..常见可重入的情况

6.可重入与线程安全联系

 三.死锁

1.死锁四个必要条件

2.避免死锁

3.避免死锁算法

四.Linux线程同步

1.条件变量

2.同步概念与竞态条件

 3.条件变量函数

4.代码样例


一.linux互斥

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

  1. 临界资源:多线程执行流共享的资源就叫做临界资源。
  2. 临界区:每个线程内部,访问临界资源的代码,就叫做临界区。
  3. 互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用。
  4. 原子性(后面讨论如何实现):不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成。

2.互斥量mutex

大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个线程,其他线程无法获得这种变量。

但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互。

多个线程并发的操作共享变量,会带来一些问题:

测试代码:

#include <iostream>
#include <cstdlib>
#include <cstdio>
#include <unistd.h>
#include <pthread.h>

using namespace std;

#define NUM 4      // 线程数
int ticket = 1000; // 1000票

void *RobTicket(void *args)
{
    const char *name = static_cast<const char *>(args);
    while (1)
    {
        if (ticket > 0)
        {
            usleep(2000);
            printf("%s-ticket,%d\n", name, ticket);
            ticket--; // 四个线程同时对ticket--,直到ticket为0时结束
        }
        else
        {
            break;
        }
        usleep(100);
    }
}

int main()
{
    pthread_t tid[NUM];
    for (int i = 0; i < NUM; i++)
    {
        char *name = new char[50];
        sprintf(name, "Thread-%d", i + 1);
        pthread_create(tid + i, NULL, RobTicket, name);
    }

    for (int i = 0; i < NUM; i++)
    {
        pthread_join(tid[i], NULL);
    }

    return 0;
}

测试结果:

 说明:

  1. 由于 if 语句判断条件为真以后,代码可以并发的切换到其他线程。
  2. usleep 这个模拟漫长业务的过程,在这个漫长的业务过程中,可能有很多个线程会进入该代码段。
  3. -- ticket 操作本身就不是一个原子操作,转换成汇编有三条汇编指令。
  4. 当ticket的值已经等于1时,由于某一个usleep会有较长时间的等待,此时又会有几个线程进入,if语句内部,所以这几个线程又会对ticket多次-- 操作。也就会出现ticket出现小于0的情况。
  5. 上述中,ticket就是临界资源,整个if语句就是临界区。

 取出ticket--部分的汇编代码
objdump -d a.out > test.objdump
152 40064b: 8b 05 e3 04 20 00 mov 0x2004e3(%rip),%eax # 600b34 <ticket>
153 400651: 83 e8 01 sub $0x1,%eax
154 400654: 89 05 da 04 20 00 mov %eax,0x2004da(%rip) # 600b34 <ticket>

3.加锁互斥锁mutex

解决办法:

  • 代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。
  • 如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区。
  • 如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。

 要做到这三点,本质上就是需要一把锁。Linux上提供的这把锁叫互斥量。

创建锁:

pthread_mutex_t mutex;

初始化锁:

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

方法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:要初始化的互斥量
  • attr:NULL

销毁锁:

int pthread_mutex_destroy(pthread_mutex_t *mutex);

注意:

  1. 使用 PTHREAD_ MUTEX_ INITIALIZER 初始化的互斥量不需要销毁。
  2. 不要销毁一个已经加锁的互斥量。
  3. 已经销毁的互斥量,要确保后面不会有线程再尝试加锁。

加锁与解锁:

int pthread_mutex_lock(pthread_mutex_t *mutex);//加锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);//解锁

返回值:成功返回0,失败返回错误号。

调用 pthread_ lock 时,可能会遇到以下情况:

  1. 该锁没被其他线程持有,直接申请成功该锁并且返回。
  2. 该锁已经被其他线程所持有,或者存在其他线程同时申请锁,但没有竞争到锁,那么pthread_ lock调用线程会陷入阻塞(执行流被挂起),等待其他线程解锁。

 测试代码:

将上述的代码对临界区加锁,使得临界区一次只能进入一个线程,每次只能有一个线程对临界资源访问,即每次都只能有一个线程对ticket--。

#include <iostream>
#include <cstdlib>
#include <cstdio>
#include <unistd.h>
#include <pthread.h>

using namespace std;

#define NUM 4          // 线程数
int ticket = 1000;     // 1000票
pthread_mutex_t mutex; // 锁

void *RobTicket(void *args)
{
    const char *name = static_cast<const char *>(args);
    while (1)
    {
        pthread_mutex_lock(&mutex); // 加锁
        if (ticket > 0)
        {
            usleep(2000);
            printf("%s-ticket,%d\n", name, ticket);
            ticket--;                     // 四个线程同时对ticket--,直到ticket为0
            pthread_mutex_unlock(&mutex); // 解锁
        }
        else
        {
            pthread_mutex_unlock(&mutex); // 解锁
            break;
        }
        usleep(100);
    }
}

int main()
{
    pthread_mutex_init(&mutex, NULL); // 初始化锁
    pthread_t tid[NUM];
    for (int i = 0; i < NUM; i++)
    {
        char *name = new char[50];
        sprintf(name, "Thread-%d", i + 1);
        pthread_create(tid + i, NULL, RobTicket, name);
    }

    // 线程等待
    for (int i = 0; i < NUM; i++)
    {
        pthread_join(tid[i], NULL);
    }

    return 0;
}

测试结果:

 说明:

  1. 不会再出现ticket为负数的情况了。
  2. 我们可以明显的发现,代码的打印速度变慢了,因为临界区的代码,包括像显示器打印的语句都被串行化了。

 4.锁的底层原理

经过上面的例子,大家已经意识到单纯的 i++ 或者 ++i 都不是原子的,有可能会有数据一致性问题。

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

 说明:

由于初始化pthread_mutex_init () 初始化会将mutex在内存初始化为1,第一个线程申请锁的时候,会将自己线程内部的一个寄存器%al初始化为0,并且将寄存器%al的值与mutex内存的数据交换,那么现在该申请锁的线程的%al寄存器中就存储的是1,mutex的内存中存储的就是0,再有经过检测,如果%al的值是大于0的return之后,继续往后运行。后续的线程再申请锁的时候,exchange之后,他们的%al寄存器只能存储的是0,所以会被挂起等待。

解锁仅仅需要将mutex的内存数据重新赋值为1,并且唤醒挂起等待的进程。

5.锁封装

class Mutex//自己不维护锁,由外部传入
{

public:
    Mutex(pthread_mutex_t *mutex)
        : _mutex(mutex)
    {
    }

    void lock()
    {
        pthread_mutex_lock(_mutex);
    }
    void unlock()
    {
        pthread_mutex_unlock(_mutex);
    }
    ~Mutex()
    {
    }

private:
    pthread_mutex_t *_mutex;
};

class MutexGuard
{
public:
    MutexGuard(pthread_mutex_t *mutex)
        : _mutex(mutex)
    {
        _mutex.lock();
    }
    ~MutexGuard()
    {
        _mutex.unlock();
    }

private:
    Mutex _mutex;
};

 二.可重入VS线程安全

1.概念

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

2.常见的线程不安全的情况

  1. 不保护共享变量的函数。
  2. 函数状态随着被调用,状态发生变化的函数。
  3. 返回指向静态变量指针的函数。
  4. 调用线程不安全函数的函数。

3.常见的线程安全的情况 

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

4.常见不可重入的情况 

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

5..常见可重入的情况

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

6.可重入与线程安全联系

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

 三.死锁

死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态。

1.死锁四个必要条件

  • 互斥条件:一个资源每次只能被一个执行流使用。
  • 请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放。
  • 不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺。
  • 循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系。

2.避免死锁

  1. 破坏死锁的四个必要条件
  2. 加锁顺序一致
  3. 避免锁未释放的场景
  4. 资源一次性分配

 3.避免死锁算法

  1. 死锁检测算法(了解)
  2. 银行家算法(了解) 

四.Linux线程同步

1.条件变量

  1. 当一个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。
  2. 例如一个线程访问队列时,发现队列为空,它只能等待,只到其它线程将一个节点添加到队列中。这种情况就需要用到条件变量。

2.同步概念与竞态条件

  1. 同步:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,叫做同步。
  2. 竞态条件:因为时序问题,而导致程序异常,我们称之为竞态条件。 

 3.条件变量函数

定义条件变量:

pthread_cond_t cond;

初始化条件变量:

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

参数:

  • cond:要初始化的条件变量。
  • attr:NULL。

销毁条件变量:

int pthread_cond_destroy(pthread_cond_t *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_broadcast:一次唤醒所有在cond上等待的线程。
  • pthread_cond_signal:唤醒一个在cond上等待的线程。

4.代码样例

#include <iostream>
#include <queue>
#include <cstdlib>
#include <cstdio>
#include <ctime>
#include <unistd.h>
#include <pthread.h>

using namespace std;

queue<int> V;
pthread_mutex_t mutex = PTHREAD_ADAPTIVE_MUTEX_INITIALIZER_NP;
pthread_cond_t cond;
void *push_date(void *args)
{
    const char *name = static_cast<const char *>(args);
    while (1)
    {
        int date;
        cin >> date; // 输入数据
        // 对临界资源的访问需要加锁
        pthread_mutex_lock(&mutex);
        V.push(date);
        // 当队列中有数据以后,需要唤醒get_date线程
        pthread_cond_signal(&cond);
        cout << name << ":push 一个数据 :" << date << endl;
        pthread_mutex_unlock(&mutex);
        sleep(1);
    }
}
void *get_date(void *args)
{
    const char *name = static_cast<const char *>(args);
    while (1)
    {
        pthread_mutex_lock(&mutex);
        //如果队列为空
        if (V.empty())
        {
            pthread_cond_wait(&cond, &mutex);
        }
        int date = V.front();
        V.pop();

        cout << name << ":我得到一个数据:" << date << endl;
        pthread_mutex_unlock(&mutex);
    }
}

int main()
{
    srand(time(nullptr));
    //
    pthread_t tid_push, tid_get;
    pthread_create(&tid_get, nullptr, get_date, (void *)"Thread_get");
    pthread_create(&tid_get, nullptr, push_date, (void *)"Thread_push");

    pthread_join(tid_get, NULL);
    pthread_join(tid_push, NULL);

    return 0;
}

 测试结果:

说明:

  1. 只有我们输入之后,get_date线程才会去队列中获取数据。
  2. 说明,get_gate线程再队列为空的时候是不会去拿数据的,使得我们的push线程先push数据的逻辑始终在get数据之前。这就是线程同步。

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

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

相关文章

华为云应用中间件DCS系列—Redis实现(视频直播)消息弹幕

云服务、API、SDK&#xff0c;调试&#xff0c;查看&#xff0c;我都行 阅读短文您可以学习到&#xff1a;应用中间件系列之Redis实现&#xff08;视频直播&#xff09;消息弹幕 1 什么是DEVKIT 华为云开发者插件&#xff08;Huawei Cloud Toolkit&#xff09;&#xff…

为什么智能相机需要搭配镜头使用?

镜头作用是将光学图像聚焦在图像传感器的光敏面阵上。不同类型的工业镜头&#xff0c;成像质量也各不相同&#xff0c;成像质量也有差异&#xff0c;影响工业镜头的因素有哪些呢 图像中心与边缘的影响图像中心较边缘分辨率高&#xff1b;图像中心较边缘光场照度高&#xff1b;像…

springboot配置swagger

springboot配置swagger Swagger 是什么Swagger配置springboot代码展示总结 Swagger 是什么 Swagger 是一个用于构建、文档和调用 RESTful Web 服务的强大工具。它提供了以下几方面的好处&#xff1a; 自动生成 API 文档: Swagger 可以自动生成 API 文档&#xff0c;包括接口的…

JS代码控制台临时测试

在进行js修改测试时&#xff0c;有可能需要我们不断的清楚缓存去加载我们修改的js文件&#xff0c;这样操作对于频繁的js修改测试很不友好&#xff0c;实际上&#xff0c;我们可以通过控制台覆盖原js代码段进行测试&#xff0c;接下来&#xff0c;以本平台为例&#xff0c;我实…

React高级特性之HOC高阶组件

一、概念 把一个组件当成另外一个组件的参数传入&#xff0c;然后通过一定的判断&#xff0c;返回新的组件。 二、基本用法 三、实战 例1&#xff1a;监听鼠标位置 App.js import React from react import HOCDemo from ./6.高级特性/4.HOCDemofunction App() {return (<…

redis(普通连接和连接池、字符串类型、hash类型、列表类型)

1 redis普通连接和连接池 1.1 普通连接 1.2 连接池 2 redis字符串类型 3 redis hash类型 4 redis列表类型 1 redis普通连接和连接池 #1 python 代码作为客户端---》连接# 2 安装模块&#xff1a;pip install redis1.1 普通连接 from redis import Redisconn Redis(host&qu…

【vim 学习系列文章 10 -- vim 将代码中空格高亮显示方法】

文章目录 vim 高亮空格使用背景如何配置vim 可以自动显示空格呢&#xff1f;vim highlight 命令使用介绍vim 空白行的处理vim match 命令详细介绍 vim 高亮空格使用背景 开发人员在编写代码之后&#xff0c;在review通过之后会将代码推到服务器然后merge&#xff0c;但是有些代…

02、RocketMQ -- 应用场景、核心概念

目录 1、消息中间件应用场景场景一&#xff1a;异步解耦同步调用异步调用 场景二&#xff1a;流量削峰 2、常用消息中间件ActiveMQKafkaRabbitMQRocketMQ 3、RocketMQ的核心概念生产者Producer消费者Consumer名字服务Name Server代理服务器Broker Server消息主题Topic消息队列M…

idea使用Spring Initializer创建springboot项目的坑【保姆级教学】

项目场景&#xff1a; 提示&#xff1a;这里先简述项目创建后遇到的问题和解决方案&#xff1a; idea 使用 Spring Initializer 创建springboot项目后&#xff0c; 有以下问题&#xff1a; ① 右键没有Run ② 右键New新建文件发现无Java Class选项 然后解决掉 ①② 问题后出…

AbortController中止请求通信[模糊搜索案例]

AbortController中止请求通信[模糊搜索案例] AbortController中止请求通信(模糊搜索案例) AbortController中止请求通信(模糊搜索案例) 这里用模糊搜索来做示例&#xff0c;这里是调用后端模糊搜索接口 该案例的中止请求可以用于很多地方&#xff0c;比如取消上传/下载文件等 完…

【分享】哇,不愧是国家出品!逆袭必备!!

哈喽&#xff0c;大家好&#xff0c;木易巷又发现好东西了 你还在为学习技能花&#x1f4b0;吗&#xff1f; 别傻了&#xff0c;偷偷告诉你&#x1f92b;&#xff0c;国家早就为我们提供了免费的学习网站&#xff01;不仅可以免费学习各种技能&#xff0c;还可以拿职业证书&a…

华为数通方向HCIP-DataCom H12-831题库(单选题:281-300)

第281题 如图所示,某工程师利用4台路由器进行网络互通测试,其中R1、R2、R3部署OSPF (Area0)实现网络互通,R2、R3、R4部署IS-IS(均部署为Level-2路由器)实现网络互通,现在该工程师在R1的OSPF进程中引入直连路由,在R2的IS-IS进程中引入OSPF路由,则以下关于该场景的描述,正…

Fiddler+逍遥模拟器抓包显示tunnel to无法抓包的解决方法

1.安装OpenSSL 下载便携式安装包&#xff1a;https://slproweb.com/products/Win32OpenSSL.html 如果不放心第三方的也可以打开官网去下载安装&#xff0c;官网地址 我这里用的第三方的 下载下来一直下一步安时就好&#xff0c;到了最后一步是给坐着捐赠&#xffe5;10&am…

高速电路设计----第三章

一、数字信号需要上拉的情况 1、 一般信号上拉接多大的电阻要看对于芯片的电流要求。看芯片datasheet的I(BHLO)和I&#xff08;BHHO&#xff09;两个参数。平时的话: 3.3V的上拉为1K~3.3k即可 5V的上拉电阻为4.7K到10K即可。 2、数字信号的逻辑控制&a…

操作系统四大特征

OS四大特征 1.OS的并发性&#xff08;同一时间间隔内执行和调度多个程序的能力&#xff09; 宏观上&#xff0c;处理机同时执行多道程序 微观上&#xff0c;处理机在多道程序间高速切换(分时交替执行)&#xff0c;微观上并非是同时执行的。 关注单个处理机同一时间段内处理任…

2023最新性能测试八股文【附答案】,软测人必备!

1. 请描述什么是性能测试、什么是负载测试、什么是压力测试&#xff1f; 【参考答案】 性能测试&#xff1a;性能测试是和功能测试相对应的。根据用户场景进行的单个用户操作&#xff0c;是属于功能测试领域&#xff0c;主要是验证软件是否可以满足用户的功能需求。比如&#x…

C++之IO流

IO流 C语言的输入与输出流是什么CIO流C标准IO流C文件IO流 stringstream的介绍 C语言的输入与输出 在C语言当中&#xff0c;我们使用最频繁的输入输出方式就是scanf与printf&#xff1a; scanf&#xff1a; 从标准输入设备&#xff08;键盘&#xff09;读取数据&#xff0c;并…

Spring的执行流程 Bean的作用域与生命周期

目录 Bean的作用域 设置作用域 Spring的执行流程 Bean的生命周期 Bean的作用域 Bean的作用域是指, Bean在Spring框架中的某种行为模式 1.singleton 单例模式 singleton是Spring中的默认的Bean作用域,它表示在整个应用程序中只存在一个Bean实例,每 次请求该Bean实例时都会…

【光流法实现目标追踪:Python实战指南】

文章目录 概要一、目标追踪概述二、光流法进行目标追踪小结 概要 在当今计算机视觉领域&#xff0c;图像处理被广泛应用于多个关键领域&#xff0c;包括图像分类、目标检测、语义分割、实例分割和目标追踪。其中&#xff0c;图像分类和目标检测作为基础应用为其他高级领域奠定…