Linux多线程(中)

Linux多线程(中)

  • 1.Linux线程互斥
    • 1.1互斥量的接口
      • 1.1.1初始化互斥量
      • 1.1.2销毁互斥量
      • 1.1.3互斥量加锁和解锁
    • 1.2修改代码
    • 1.3互斥量实现原理
  • 2.可重入VS线程安全
  • 3.死锁
  • 4.Linux线程同步
  • 5.生产者消费者模型

🌟🌟hello,各位读者大大们你们好呀🌟🌟
🚀🚀系列专栏:【Linux的学习】
📝📝本篇内容:Linux线程互斥;可重入VS线程安全;死锁;Linux线程同步;生产者消费者模型
⬆⬆⬆⬆上一篇:Linux多线程(上)
💖💖作者简介:轩情吖,请多多指教(>> •̀֊•́ ) ̖́-

1.Linux线程互斥

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
int ticket = 100;
void *route(void *arg)
{
    char *id = (char *)arg;
    while (1)
    {
        if (ticket > 0)
        {
            usleep(1000);
            printf("%s sells ticket:%d\n", id, ticket);
            ticket--;
        }
        else
        {
            break;
        }
    }
}
int main(void)
{
    pthread_t t1, t2, t3, t4;
    pthread_create(&t1, NULL, route,(char*)"thread 1");
    pthread_create(&t2, NULL, route,(char*)"thread 2");
    pthread_create(&t3, NULL, route,(char*)"thread 3");
    pthread_create(&t4, NULL, route,(char*)"thread 4");
    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    pthread_join(t3, NULL);
    pthread_join(t4, NULL);
}

在这里插入图片描述
观察上面的代码,可以发现我们的抢票功能会出现问题,具体原因如下

if 语句判断条件为真以后,代码可以并发的切换到其他线程
usleep 这个模拟漫长业务的过程,在这个漫长的业务过程中,可能有很多个线程会进入该代码段
–ticket 操作本身就不是一个原子操作
– 操作并不是原子操作,而是对应三条汇编指令:
load :将共享变量ticket从内存加载到寄存器中
update : 更新寄存器里面的值,执行-1操作
store :将新值,从寄存器写回共享变量ticket的内存地址

因此我们需要做到以下三点:

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

1.1互斥量的接口

1.1.1初始化互斥量

一共分为两种方法:
①静态分配

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

②动态分配
在这里插入图片描述

参数:
mutex:要初始化的互斥量
attr:NULL

1.1.2销毁互斥量

在销毁互斥量的时候要注意两个点:①使用宏定义的初始化的互斥量不需要销毁;②不要销毁一个已经加锁的互斥量
函数:

在这里插入图片描述

1.1.3互斥量加锁和解锁

在这里插入图片描述

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

1.2修改代码

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
int ticket = 100;
pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;//使用锁
void *route(void *arg)
{
    char *id = (char *)arg;
    while (1)
    {
        pthread_mutex_lock(&mutex);//上锁
        if (ticket > 0)
        {
            usleep(1000);
            printf("%s sells ticket:%d\n", id, ticket);
            ticket--;
        pthread_mutex_unlock(&mutex);//解锁
        }
        else
        {
        pthread_mutex_unlock(&mutex);
        //这里也需要,因为假设有线程正好碰到ticket为0了,而它后面还有线程,那就需要解锁,让后面的线程进入临界区,不能一直挂起着
        break;
        }
    }
}
int main(void)
{
    pthread_t t1, t2, t3, t4;
    pthread_create(&t1, NULL, route,(char*)"thread 1");
    pthread_create(&t2, NULL, route,(char*)"thread 2");
    pthread_create(&t3, NULL, route,(char*)"thread 3");
    pthread_create(&t4, NULL, route,(char*)"thread 4");
    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    pthread_join(t3, NULL);
    pthread_join(t4, NULL);
    pthread_mutex_destroy(&mutex);
}

在这里插入图片描述
现在就能正常运行而不出现bug

1.3互斥量实现原理

其实在我们的C/C++中,val- -是一条语句,但是实际上它在汇编层面有三条语句,对全局变量做自减,没有任何保护的话,会存在并发的访问问题,进而导致数据不一致的问题
单纯的i++或者++i都不是原子的,都有可能出现数据一致性的问题,为了实现互斥锁的原理,大多数的体系结构都提供了swap和exchange命令,该指令的作用是把寄存器和内存单元的数据相交换,只有一条指令,保证了原子性
在这里插入图片描述
在这里插入图片描述
我们的线程会执行加锁和解锁的代码,我们电脑的寄存器硬件只有一套,但是寄存器内部的数据(执行流上下文)是每个数据都是线程各自的。执行xchgb%al,mutex就是交换,本质上是将共享数据交换到自己私有的上下文当中,就其实就是加锁,而且加锁只有一条汇编,因此加锁是原子性的

①凡是访问同一个临界资源的线程,都要进行加锁保护,而且是同一把锁;
②每一个线程访问临界区之前,得先加锁,加锁本质山是给临界区加锁,加锁的粒度要细一点;
③线程访问临界区的时候,需要先加锁,因此所有的线程都必须要先看到同一把锁,我们的锁本身就是共享资源公共资源,因此我们保证加锁和解锁是原子的;
④临界区可以是一行代码也可以是一批代码;我们的线程可能在临界区的时候被切换,我们不能特殊化加锁和解锁,还有临界区的代码;在切换的时候不会造成数据一致性的问题,因为当线程不在的时候,任何线程都没有办法进入临界区,因为其他线程没有办法申请到锁,锁已经被前面的线程拿走了
⑤其实这也体现了互斥的概念,线程有意义的状态有两种:持有锁(锁被我申请了),不持有锁(锁被我释放了),原子性的体现

2.可重入VS线程安全

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

可重入与线程安全联系:
函数是可重入的,那就是线程安全的
函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题
如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的。
可重入与线程安全区别:
可重入函数是线程安全函数的一种
线程安全不一定是可重入的,而可重入函数则一定是线程安全的。
如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生死锁,因此是不可重入的。

3.死锁

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

死锁的四个必要条件:
互斥条件:一个资源每次只能被一个执行流使用
请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放
不剥夺条件:一个执行流已获得的资源在未使用完前,不能被强行剥夺
循环等待条件:若干执行流之间形成了头尾相接的循环等待资源的关系、
避免死锁:(破坏死锁的四个必要条件)
①不加锁
②主动释放锁
③按顺序申请锁
④控制线程统一释放锁

4.Linux线程同步

同步:在保证数据安全的前提下,让线程能够按照某种特定顺序访问临界资源,从而有效避免饥饿问题(让多线程进行协同工作)
条件变量能够允许多线程在cond中队列式等待(就是一种顺序)
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
上述就是条件变量的函数,具体使用我们在生产消费模型来具体演示

5.生产者消费者模型

在这里插入图片描述
我们的阻塞队列是必须先被我们的生产者和消费者线程看到,因此一定是一个会被多线程并发访问的公共区域,为了保护共享资源的安全,要维护线程互斥同步的关系.
这样一来我们的生产者消费者模型会有这几个优点:支持并发;支持忙闲不均;效率高;解耦
对于我们的生产者消费者模型可以使用321原则来记忆:

3种关系:
生产者和生产者→互斥 消费者和消费者→互斥 生产者消费者→互斥和同步
2种角色:
生产者,消费者
一个交易场所:
通常是缓冲区

接下来是一个生产者消费者模型的代码演示:我们的需求是productor生产Task,customer消费Task,Task是基本运算

//main.cc
#include <iostream>
#include <unistd.h>
#include <string>
#include <pthread.h>
#include "Task.hpp"
#include "BlockQueue.hpp"
using namespace std;
void *ProductorRun(void *arg) // 生产者线程要使用的函数
{
        BlockQueue<Task> *bq = static_cast<BlockQueue<Task> *>(arg);
    while (1)
    {
        // 创建Task任务
        char arr[] = "+-*/%";
        int x = rand() % 10;
        int y = rand() % 20;
        char c = arr[rand() % 5];
        Task t(x, y, c);
        bq->push(t);
        cout << t.to_productor() << endl;
    }

    return nullptr;
}

void *CustomerRun(void *arg) // 消费者线程要使用的函数
{
    BlockQueue<Task> *bq = static_cast<BlockQueue<Task> *>(arg);
    while (1)
    {
        sleep(3);
        Task t;
        bq->pop(&t);
        t();
        cout << t.to_customer() << endl;
    }

    return nullptr;
}

int main()
{
    srand((uint64_t)time(0));
    BlockQueue<Task> bq;
    pthread_t customer[3];
    pthread_t productor[3];
    pthread_create(&customer[0],nullptr,CustomerRun,&bq);
    pthread_create(&customer[1],nullptr,CustomerRun,&bq);
    pthread_create(&customer[2],nullptr,CustomerRun,&bq);
    pthread_create(&productor[0],nullptr,ProductorRun,&bq);
    pthread_create(&productor[1],nullptr,ProductorRun,&bq);
    pthread_create(&productor[2],nullptr,ProductorRun,&bq);
    
    //join
    pthread_join(customer[0],nullptr);
    pthread_join(customer[1],nullptr);
    pthread_join(customer[2],nullptr);
    pthread_join(productor[0],nullptr);
    pthread_join(productor[1],nullptr);
    pthread_join(productor[2],nullptr);
    



    return 0;
}

//BlockQueue.hpp
#pragma once
#include <iostream>
#include <unistd.h>
#include <string>
#include <pthread.h>
#include <queue>
using namespace std;
#define SIZE 5
template<class T=int>
class BlockQueue
{
    public:
    BlockQueue()
    {
        pthread_mutex_init(&_mutex,nullptr);//锁初始化
        pthread_cond_init(&_productor,nullptr);//条件变量的初始化
        pthread_cond_init(&_customer,nullptr);
    }

    bool IsFull()
    {
        return _q.size()==_capacity;
    }

    bool IsEmpty()
    {
        return _q.size()==0;

    }

    void push(const T& data)
    {
        pthread_mutex_lock(&_mutex);//能够保证我们的队列一次只能有一个消费者或生产者使用
        while(IsFull())//这边不能使用if,因为有可能后续把全部的生产者唤醒,if的话只能判断一次
        {
            pthread_cond_wait(&_productor,&_mutex);//当线程进行wait时,mutex会被释放;当线程被signal或broadcast后,线程会重新获得mutex
        }
        _q.push(data);//生产
        pthread_cond_signal(&_customer);//唤醒一个消费者
        pthread_mutex_unlock(&_mutex);
    }

    void pop(T* data)
    {
        pthread_mutex_lock(&_mutex);
        while(IsEmpty())
        {
            pthread_cond_wait(&_customer,&_mutex);
        }
        *data=_q.front();//消费
        _q.pop();
        pthread_cond_signal(&_productor);
        pthread_mutex_unlock(&_mutex);     
    }






    private:
    queue<T> _q;
    pthread_mutex_t _mutex;
    pthread_cond_t _productor;
    pthread_cond_t _customer;
    int _capacity=SIZE;
};

//Task.hpp
#pragma once
#include <iostream>
#include <unistd.h>
#include<string>
using namespace std;
//阻塞队列中的内容,生产者生产,消费者消费
class Task
{
    public:
    Task()
    {}

    Task(int x,int y,char op)
    :_x(x),_y(y),_op(op)
    {}

    void operator()()
    {
        switch(_op)
        {
            case '+':
                    _result=_x+_y;
                    break;
            case '-':
                    _result=_x-_y;
                    break;
            case '/':
                    if(_y==0)
                    {
                        _code=-1;
                        break;
                    }
                    _result=_x/_y;
                    break;
            case '*':
                    _result=_x*_y;
                    break;
            case '%':
                    if(_y==0)
                    {
                        _code=-2;
                        break;
                    }
                    _result=_x%_y;
                    break;
        default:
                _code=-3;
                break;
        }
    }

    string to_productor()//打印productor生产的内容是什么
    {
        return to_string(_x)+_op+to_string(_y)+"=?";
    }

    string to_customer()//打印customer消费后的结果为什么
    {
        return to_string(_x)+_op+to_string(_y)+"="+to_string(_result)+";code="+to_string(_code);
    }



    private:
    int _x;//操作数
    int _y;//操作数
    char _op;//计算方法
    int _result=0;//计算结果
    int _code=0;//计算后的返回码
};


//_code:
//-1 -> /出错
//-2 -> %出错
//-3 -> _op出错
# makefile
main:main.cc
	g++ -o main main.cc -std=c++11 -lpthread

.PHONY:clean
clean:
	rm -rf main

注意细节:
①我们一定要保证,在任何一个时候,都是符合条件的,才进行一个生产(所以不能用if)
②我们只能在临界区内部判断临界资源是否就绪,注定了此时我们一定是持有锁的
③要让线程进行休眠等待,不能持有锁等待,不然会造成死锁,,因此pthread_cond_wait要有释放锁的能力
④当线程在pthread_cond_wait中会进行休眠,当醒来的时候,继续从临界区内部继续运行,继续在函数处向后运行,并重新申请锁,申请成功才会彻底返回
⑤对于这个模型的高效指的是当生产者在获取数据时,而消费者在阻塞队列中获取数据;当消费者处理数据时,而生产者在放数据到阻塞队列中
⑦只用一把锁是因为生产和消费访问的是同一个queue,queue被当成一个整体使用

在这里插入图片描述

🌸🌸Linux多线程(中)的知识大概就讲到这里啦,博主后续会继续更新更多Linux的相关知识,干货满满,如果觉得博主写的还不错的话,希望各位小伙伴不要吝啬手中的三连哦!你们的支持是博主坚持创作的动力!💪💪

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

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

相关文章

Java 自定义集合常量

文章目录 Java 自定义集合常量一、普通方法自定义集合常量信息1、定义 Map 集合信息&#xff08;1&#xff09;方法一&#xff1a;使用静态代码块&#xff08;2&#xff09;方法二&#xff1a;简单定义 Map 常量 2、定义 List 集合信息3、定义 Set 集合信息 二、通过 Collectio…

用win的控制台去远程连接虚拟机linux的终端

以Ubuntu为例&#xff0c;首先确保Ubuntu已经安装了ssh服务 sudo apt-get install openssh-server输入密码 安装完毕后查看ssh状态是否开启 sudo systemctl status ssh 显示绿色激活状态&#xff0c;可以关闭或开启 对应start和stop winr打开win端控制台 输入 ssh -p 22 …

python-22-零基础自学python-数据分析基础 打开文件 读取文件信息

学习内容&#xff1a;《python编程&#xff1a;从入门到实践》第二版 知识点&#xff1a; 读取文件 、逐行读取文件信息等 练习内容&#xff1a; 练习10-1:Python学习笔记 在文本编辑器中新建一个文件&#xff0c;写几句话来总结一下你至此学到的Python知识&#xff0c;其中…

ASCII码对照表(Matplotlib颜色对照表)

文章目录 1、简介1.1 颜色代码 2、Matplotlib库简介2.1 简介2.2 安装2.3 后端2.4 入门例子 3、Matplotlib库颜色3.1 概述3.2 颜色图的分类3.3 颜色格式表示3.4 内置颜色映射3.5 xkcd 颜色映射3.6 颜色命名表 4、Colorcet库5、颜色对照表结语 1、简介 1.1 颜色代码 颜色代码是…

声明队列和交换机 + 消息转换器

目录 1、声明队列和交换机 方法一&#xff1a;基于Bean的方式声明 方法二&#xff1a;基于Spring注解的方式声明 2、消息转换器 1、声明队列和交换机 方法一&#xff1a;基于Bean的方式声明 注&#xff1a;队列和交换机的声明是放在消费者这边的&#xff0c;这位发送的人他…

OSS存储桶漏洞总结

简介 OSS&#xff0c;对象存储服务&#xff0c;对象存储可以简单理解为用来存储图片、音频、视频等非结构化数据的数据池。相对于主机服务器&#xff0c;具有读写速度快&#xff0c;利于分享的特点。 OSS工作原理&#xff1a; 数据以对象&#xff08;Object&#xff09;的形式…

Java高级重点知识点-21-IO、字节流、字符流、IO异常处理、Properties中的load()方法

文章目录 IOIO的分类 字节流字节输出流【OutputStream】字节输入流【InputStream】图片复制 字符流字符输入流【FileReader】字符输出流【FileWriter】 IO异常的处理&#xff08;扩展知识&#xff09;Properties属性集(java.util) IO Java中I/O操作主要是指使用 java.io 包下的…

iOS中多个tableView 嵌套滚动特性探索

嵌套滚动的机制 目前的结构是这样的&#xff0c;整个页面是一个大的tableView, Cell 是整个页面的大小&#xff0c;cell 中嵌套了一个tableView 通过测试我们发现滚动的时候&#xff0c;系统的机制是这样的&#xff0c; 我们滑动内部小的tableView, 开始滑动的时候&#xff0c…

想知道你的电脑能不能和如何升级RAM吗?这里有你想要的一些提示

考虑给你的电脑增加更多的RAM,但不确定从哪里开始?本指南涵盖了有关升级Windows PC或笔记本电脑中RAM的所有信息。 你需要升级RAM吗 在深入研究升级RAM的过程之前,评估是否需要升级是至关重要的。你是否经历过系统滞后、频繁的BSOD错误或应用程序和程序突然崩溃?这些症状…

Lock与ReentrantLock

在 Java 中&#xff0c;Lock 接口和 ReentrantLock 类提供了比使用 synchronized 方法和代码块更广泛的锁定机制。 简单示例&#xff1a; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock;public class ReentrantLockExample {pr…

聊一下Maven打包的问题(jar要发布)

文章目录 一、问题和现象二、解决方法&#xff08;1&#xff09;方法一、maven-jar-pluginmaven-dependency-plugin&#xff08;2&#xff09;方法二、maven-assembly-plugin 一、问题和现象 现在的开发一直都是用spring boot&#xff0c;突然有一天&#xff0c;要自己开发一个…

【CUDA】

笔者在学习Softmax实现时遇到了一个问题&#xff0c;很多文章直接将softmax的计算分成了五个过程&#xff0c;而没有解释每个过程的含义&#xff0c;尤其是在阅读这篇文章时&#xff0c;作者想计算最基本的softmax的效率&#xff0c;以展示可行的优化空间&#xff1a; 贴一个g…

MybatisX插件的简单使用教程

搜索mybatis 开始生成 module path&#xff1a;当前项目 base package:生成的包名&#xff0c;建议先独立生成一个&#xff0c;和你原本的项目分开 encoding&#xff1a;编码&#xff0c;建议UTF-8 class name strategy&#xff1a;命名选择 推荐选择camel&#xff1a;驼峰命…

Centos下rpm和yum执行卡住问题(已解决)

问题描述 执行rpm和yum卡住&#xff0c; 没有任何报错信息&#xff0c;且无法 ctrl c 终止&#xff0c;只能通过后台 kill -9 杀死。 问题排查&#xff1a; 查看yum日志&#xff1a;yum -vv 软件包 会发现卡在 loading keyring from rpmdb&#xff0c;即load DB存在问题。 …

MSPM0G3507——MPU6050读取数据显示在OLED上

移植的立创L1306的部分代码&#xff0c;亲测能用&#xff0c;要源码的评论即可&#xff0c;在CCSTHEIA打开

【代码管理的必备工具:Git的基本概念与操作详解】

一、Git 初识 1.提出问题 不知道你工作或学习时&#xff0c;有没有遇到这样的情况&#xff1a;我们在编写各种⽂档时&#xff0c;为了防止⽂档丢失&#xff0c;更改失误&#xff0c;失误后能恢复到原来的版本&#xff0c;不得不复制出⼀个副本&#xff0c;比如&#xff1a; “…

推荐几款漂亮的代码字体

Visual Studio Code 中字体看时间长了就会产生幻觉&#xff0c;于是今天看到有人推荐漂亮的代码字体&#xff0c;于是自己也推荐几款&#xff1a; 需要注意的是&#xff0c;大部分网上的教程都建议使用混合字体&#xff0c;即使用微软雅黑与某种等宽字体混合。但事实上&#x…

(ECCV,2022)Mask-CLIP:从CLIP中提取自由密集标签

文章目录 Extract Free Dense Labels from CLIP相关资料摘要引言方法Mask-CLIPMask-CLIP 实验 Extract Free Dense Labels from CLIP 相关资料 代码&#xff1a;https://github.com/chongzhou96/MaskCLIP 论文&#xff1a;https://arxiv.org/abs/2112.01071 摘要 对比语言-…

接口测试分析、设计以及实现

接口相关理论 ui功能测试和接口测试哪个先执行&#xff1f;–为什么 结论&#xff1a;接口测试先执行 原因&#xff1a;ui功能测试需要等待前端页面开发完成、后台接口开发完后且前端与后端联调完成。ui功能测试与接口测试的区别&#xff1f; ui功能&#xff1a;功能调用&am…

windows上传app store的构建版本简单方法

我们在上传app store上架&#xff0c;或上传到testflight进行ios的app测试的时候&#xff0c;需要mac下的上传工具上传ipa文件到app store的构建版本上。 然而windows电脑这些工具是无法安装的。 因此&#xff0c;假如在windows上开发hbuilderx或uniapp的应用&#xff0c;可以…