<Linux> 多线程

文章目录

  • 线程
  • 线程互斥
    • 死锁
  • 线程同步
  • 生产者消费者模型
  • POSIX信号量
    • 基于环形队列的生产消费模型
  • 线程池

线程

线程是进程内部可以独立运行的最小单位

进程是资源分配的基本单位,线程是调度器调度的基本单位

线程在进程的地址空间内运行

进程内的大部分资源线程是共享的,但也有属于线程自己的独立资源,主要是寄存器和栈(位于共享区)

为什么引入线程?

创建新线程的工作要比创建新进程的工作少得多

线程的切换要比进程的切换所作的工作少得多

线程异常

单个线程出现崩溃会导致整个进程奔溃,因为线程是在进程内部运行的

Linux下的线程

Linux将线程视为一种特殊类型的进程,称作轻量级进程(Lightweight Process,LWP)

Linux并没有提供系统调用来对线程进行操作,对线程操作使用的是第三方库

来段代码感受一下

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

void *routine(void *args)
{
    while (true)
    {
        printf("sub process running ...  pid:%d\n", getpid());
        sleep(1);
    }
}

int main()
{
    pthread_t t;

    pthread_create(&t, nullptr, routine, nullptr);

    while (true)
    {
        printf("main process running ...  pid:%d\n", getpid());
        sleep(3);
    }

    pthread_join(t, nullptr);
    return 0;
}

运行结果如下

请添加图片描述

程序运行起来之后检测可以看到,有两个线程,其中一个pid和lwp相等,这就是主线程,下面的就是新线程

在这里插入图片描述

每个进程内部至少有一个线程(主线程)

线程互斥

多个线程在对同一份资源进行访问时,很可能会造成数据紊乱的问题,来看个例子

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

int num = 10000;

void *routine(void *args)
{

    while (true)
    {
        if (num > 0)
        {
            usleep(100);
            printf("%d\n", num);
            num--;
        }
        else
        {
            break;
        }
        usleep(1000);
    }

}

int main()
{

    const int n = 100;
    pthread_t t[n];
    for (size_t i = 0; i < n; i++)
    {
        pthread_create(t + i, nullptr, routine, nullptr);
    }

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


    return 0;
}

创建两个新线程,让这两个线程对同一个数字进行争夺,类似于抢票,没有票就退出

只看最后的几个结果

请添加图片描述

这里n==0时明显不能进入if语句,但是后面居然打印出负数
这就是多线程对于同一份资源在进行访问时造成的数据紊乱

临界区

临界区是指一段代码,当一个线程(或进程)进入这段代码并开始执行时,其他线程(或进程)不能同时进入执行该段代码的区域。这是为了确保共享资源在同一时间只能被一个线程访问,避免数据竞争和不一致的状态。

临界资源

临界资源是指在多线程或多进程环境中被共享访问的数据、对象或资源。因为多个线程或进程可能同时访问这些资源,所以需要在访问它们时确保数据的一致性和正确性。

如何对临界资源进行保护?------‘锁’

锁的使用可以确保当一个线程在访问共享资源时,其他线程无法同时访问该资源,从而保证数据的一致性和正确性。

对刚才代码稍作修改

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

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
int num = 10000;

void *routine(void *args)
{

    while (true)
    {
        pthread_mutex_lock(&mutex);
        if (num > 0)
        {
            usleep(100);
            printf("%d\n", num);
            num--;
            pthread_mutex_unlock(&mutex);
        }
        else
        {
            pthread_mutex_unlock(&mutex);
            break;
        }
        usleep(1000);
    }
}

int main()
{

    const int n = 100;
    pthread_t t[n];
    for (size_t i = 0; i < n; i++)
    {
        pthread_create(t + i, nullptr, routine, nullptr);
    }

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

    return 0;
}

这次运行结果就正常了

有了锁就能保证每次只有一个线程能够访问到临界资源

死锁

死锁是指两个或多个线程互相等待对方持有的资源而无法继续执行的情况

死锁的4个必要条件

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

避免死锁

  • 破坏死锁的四个必要条件
  • 加锁顺序一致

线程同步

在多个线程互斥的访问临界资源时,只有一个能获得临界资源,其他线程只能忙等待,什么也做不了

两个问题:

  • 存在多个线程竞争同一个临界资源的情况,每次都是其中一个线程获得临界资源,造成其他线程的饥饿问题
  • 在临界资源没有就绪的时候,线程不停的申请锁—判断临界资源事否就绪—释放锁,一直重复,不合理

如何解决?条件变量
条件变量允许一个或多个线程等待满足某些条件时继续执行。

void push(const T &val)
{
    pthread_mutex_lock(&_mtx);
    while (is_full())
    {
        pthread_cond_wait(&_full, &_mtx);
    }

    _q.push(val);
    cout << "Producer Thread [ " << pthread_self() << " ]  Produced " << val << endl;
    pthread_mutex_unlock(&_mtx);
    pthread_cond_signal(&_empty);
}

截取一段代码举例

要点

  • 等待时线程自动挂起,并且释放手中的锁,如果收到唤醒信号,再在被阻塞的位置唤醒,重新去申请锁
  • 为什么使用while循环,不用if。要避免伪唤醒,if出现伪唤醒的情况时会直接向下运行,而while循环会再次检查资源事否就绪,防止伪唤醒

生产者消费者模型

请添加图片描述

核心点

  • 生产者在阻塞队列已满时不能继续放入数据
  • 消费者在阻塞队列为空时不能继续取出数据
  • 生产者与生产者之间互斥
  • 消费者与消费者之间互斥

阻塞队列

为了代码的健壮性,使用泛型编程

字段

阻塞队列是个队列,所以封装STL的队列

阻塞队列属于临界区,对临界区访问应该似乎互斥的,所以需要一把锁来控制生产者和消费者的互斥访问

条件变量,不让临界区外的资源忙等待

队列容量

函数

构造、析构

基本的入队列和出队列

判断是否空,是否满

#pragma once

#include <iostream>
#include <queue>
#include <pthread.h>
#include <unistd.h>

using namespace std;
#define BLOCK_SIZE 10

template <class T>
class BlockQueue
{
public:
    BlockQueue(size_t capacity = BLOCK_SIZE)
        : _capacity(capacity)

    {
        pthread_mutex_init(&_mtx, nullptr);
        pthread_cond_init(&_full, nullptr);
        pthread_cond_init(&_empty, nullptr);
    }

    void push(const T &val)
    {
        pthread_mutex_lock(&_mtx);
        while (is_full())
        {
            pthread_cond_wait(&_full, &_mtx);
        }

        _q.push(val);
        cout << "Producer Thread [ " << pthread_self() << " ]  Produced " << val << endl;
        pthread_mutex_unlock(&_mtx);
        pthread_cond_signal(&_empty);
    }

    void pop()
    {
        pthread_mutex_lock(&_mtx);
        while (isEmpty())
        {
            pthread_cond_wait(&_empty, &_mtx);
        }

        T s = _q.front();
        _q.pop();
        cout << "Consumer Thread [ " << pthread_self() << " ]  Consumed " << s << endl;

        pthread_mutex_unlock(&_mtx);
        pthread_cond_signal(&_full);
    }

    ~BlockQueue()
    {
        pthread_cond_destroy(&_full);
        pthread_cond_destroy(&_empty);
        pthread_mutex_destroy(&_mtx);
    }

private:
    bool isEmpty()
    {
        return _q.empty();
    }
    bool is_full()
    {
        return _capacity == _q.size();
    }

private:
    queue<T> _q;
    size_t _capacity;
    pthread_mutex_t _mtx;
    pthread_cond_t _full;
    pthread_cond_t _empty;
};

POSIX信号量

信号量的两个操作

  • p操作:申请资源
  • v操作:释放资源

基于环形队列的生产消费模型

允许生产者和消费者在同一个数据结构上进行操作

请添加图片描述

核心点

  • 生产者不能套圈消费者
  • 消费者不能超过生产者
  • 生产者与生产者之间互斥
  • 消费者与消费者之间互斥

环形队列

字段

封装vector

容量

生产者在队列的位置

消费者在队列的位置

消费信号

生产信号

消费锁

生产锁

函数

构造(这里没有实现析构,因为封装的锁和信号量各自的析构已经实现,析构时会自动调用)

入队列和出队列

#include <iostream>
#include <vector>
#include "Sem.hpp"
#include "Mutex.hpp"
#define SIZE 10

template <class T>
class RingQueue
{
public:
    RingQueue(size_t size = SIZE)
        : _size(size),
          _rq(size),
          _p_sem(size),
          _c_sem(0),
          _c_pos(0),
          _p_pos(0)
    {
    }

    void push(const T &val)
    {
        _p_sem.p();
        _p_mtx.lock();
        _rq[_p_pos++] = val;

        _p_pos %= _size;
        _p_mtx.unlock();

        _c_sem.v();
    }
    void pop(T *pv)
    {
        _c_sem.p();
        _c_mtx.lock();
        *pv = _rq[_c_pos++];
        _c_pos %= _size;
        _c_mtx.unlock();
        _p_sem.v();
    }

private:
    std::vector<T> _rq;
    size_t _size;
    size_t _c_pos;
    size_t _p_pos;
    Sem _c_sem;
    Sem _p_sem;
    Mutex _c_mtx;
    Mutex _p_mtx;
};

要点

  • 为什么是先申请资源(p操作),再加锁?申请信号量实际上一种“预定”,先买票后入座,这样可以确保每个进入临界区的线程要访问的资源已经就绪,可以提升效率。

对锁进行封装

#pragma once
#include <pthread.h>
class Mutex
{

public:
    Mutex()
    {
        pthread_mutex_init(&_mtx, nullptr);
    }

    ~Mutex()
    {
        pthread_mutex_destroy(&_mtx);
    }

    void lock()
    {
        pthread_mutex_lock(&_mtx);
    }

    void unlock()
    {
        pthread_mutex_unlock(&_mtx);
    }

private:
    pthread_mutex_t _mtx;
};

对信号量进行封装

#pragma once

#include <semaphore.h>
class Sem
{
public:
    Sem(int value)
    {
        sem_init(&_sem, 0, value);
    }

    ~Sem()
    {
        sem_destroy(&_sem);
    }

    void p()
    {
        sem_wait(&_sem);
    }

    void v()
    {
        sem_post(&_sem);
    }

private:
    sem_t _sem;
};

线程池

详情代码见:实现简易线程池

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

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

相关文章

LeetCode-刷题记录-滑动窗口合集(本篇blog会持续更新哦~)

一、滑动窗口概述 滑动窗口&#xff08;Sliding Window&#xff09;是一种用于解决数组&#xff08;或字符串&#xff09;中子数组&#xff08;或子串&#xff09;问题的有效算法。 Sliding Window核心思想&#xff1a; 滑动窗口技术的基本思想是维护一个窗口&#xff08;一般…

RPC远程过程调用--Thrift

RPC远程过程调用–Thrift 简介 Thrift是一个由Facebook开发的轻量级、跨语言的远程服务调用框架&#xff0c;后进入Apache开源项目。支持通过自身接口定义语言IDL定义RPC接口和数据类型&#xff0c;然后通过编译器生成不同语言代码&#xff0c;用于构建抽象易用、可互操作的R…

JAVA+SSM+VUE《教学视频点播系统》

1管理员登录 管理员登录&#xff0c;通过填写用户名、密码、角色等信息&#xff0c;输入完成后选择登录即可进入视频点播系统&#xff0c;如图1所示。 图1管理员登录界面图 2管理员功能实现 2.1 修改密码 管理员对修改密码进行填写原密码、新密码、确认密码并进行删除、修改…

【Python机器学习】算法链与管道——在网格搜索中使用管道

在网格搜索中使用管道的工作原理与使用任何其他估计器都相同。 我们定义一个需要搜索的参数网络&#xff0c;并利用管道和参数网格构建一个GridSearchCV。不过在指定参数网格时存在一处细微的变化。我们需要为每个参数指定它在管道中所属的步骤。我们要调节的两个参数C和gamma…

监控与安全服务

kali 系统 nmap扫描 网段的扫描 使用脚本扫描 使用john破解密码 哈希算法是一种单向加密的算法&#xff0c;也就是将原始数据生成一串“乱码”只能通过原始数据&#xff0c;生成这串“乱码”&#xff0c;但是不能通过“乱码”回推出原始数据相同的原始数据&#xff0c;生成的乱…

红酒与时尚秀场:品味潮流新风尚

在时尚与品味的交汇点上&#xff0c;红酒总是以其不同的方式&#xff0c;为每一次的时尚盛宴增添一抹诱人的色彩。当红酒遇上时尚秀场&#xff0c;不仅是一场视觉的盛宴&#xff0c;更是一次心灵的触动。今天&#xff0c;就让我们一起走进红酒与时尚秀场的世界&#xff0c;感受…

Elasticsearch:结合稀疏、密集和地理字段

作者&#xff1a;来自 Elastic Madhusudhan Konda 如何以自定义方式组合多个稀疏、密集和地理字段 Elasticsearch 是一款强大的工具&#xff0c;可用于近乎实时地搜索和分析数据。作为开发人员&#xff0c;我们经常会遇到包含各种不同字段的数据集。有些字段是必填字段&#x…

算法力扣刷题记录 二十八【225. 用队列实现栈】

前言 栈和队列篇。 记录 二十八【225. 用队列实现栈】 一、题目阅读 请你仅使用两个队列实现一个后入先出&#xff08;LIFO&#xff09;的栈&#xff0c;并支持普通栈的全部四种操作&#xff08;push、top、pop 和 empty&#xff09;。 实现 MyStack 类&#xff1a; void p…

数据库安全审计系统:满足数据安全治理合规要求

伴随着数据库信息价值以及可访问性提升&#xff0c;使得数据库面对来自内部和外部的安全风险大大增加&#xff0c;如违规越权操作、恶意入侵导致机密信息窃取泄漏&#xff0c;但事后却无法有效追溯和审计。 国内专注于保密与非密领域的分级保护、等级保护、业务连续性安全和大数…

浅谈渗透测试实战

很多时候&#xff0c;在看白帽子们的漏洞的时候总有一种感觉就是把web渗透简单地理解成了发现web系统漏洞进而获取webshell。其实&#xff0c;个人感觉一个完整的渗透&#xff08;从黑客的角度去思考问题&#xff09;应该是以尽一切可能获取目标的系统或者服务器的最高权限&…

TCL中环可转债缩水近90亿:业绩持续承压,百亿自有资金购买理财

《港湾商业观察》廖紫雯 日前&#xff0c;TCL中环新能源科技股份有限公司&#xff08;以下简称&#xff1a;TCL中环&#xff0c;002129.SZ&#xff09;可转债总额缩水近90亿&#xff0c;引发市场关注。可转债大幅缩水的另一面&#xff0c;公司此前发布公告披露将使用百亿自有资…

深入详解RocketMQ源码安装与调试

1.源码下载 http://rocketmq.apache.org/dowloading/releases/ 2. 环境要求 64位系统JDK1.8(64位)Maven 3.2.x

[笔记] 卷积03 - 运算的对称性 时域构建高通滤波器的失败尝试

1.卷积运算具备足够好的对称性 1.在计算卷积时&#xff0c;两个函数的位置是可以颠倒的&#xff0c;对吧&#xff1f; 在卷积运算中&#xff0c;确实可以对参与卷积的两个函数进行颠倒。这是因为卷积的定义是通过一个函数与另一个函数的翻转后的形式进行积分运算。具体来说&a…

【系统架构设计师】计算机组成与体系结构 ⑨ ( 磁盘管理 | “ 磁盘 “ 单缓冲区 与 双缓冲区 | “ 磁盘 “ 单缓冲区 与 双缓冲区案例 )

文章目录 一、" 磁盘 " 单缓冲区 与 双缓冲区1、" 磁盘 " 单缓冲区2、" 磁盘 " 双缓冲区 二、" 磁盘 " 单缓冲区 与 双缓冲区案例1、案例描述2、磁盘单缓冲区 - 流水线分析3、磁盘双缓冲区 - 流水线分析 一、" 磁盘 " 单缓冲…

Avalonia应用在基于Linux的国产操作deepin上运行

deepin系统介绍 deepin(原名Linux Deepin)致力于为全球用户提供美观易用&#xff0c;安全可靠的 Linux发行版。deepin项目于2008年发起&#xff0c;并在2009年发布了以 linux deepin为名称的第一个版本。2014年4月更名为 deepin&#xff0c;在中国常被称为“深度操作系统”。 …

matlab 干涉图仿真

目录 一、算法概述1、干涉图2、生成步骤 二、代码实现三、结果展示 本文由CSDN点云侠原创&#xff0c;原文链接。如果你不是在点云侠的博客中看到该文章&#xff0c;那么此处便是不要脸的爬虫。 一、算法概述 1、干涉图 干涉图是两束或多束相干光波相遇时&#xff0c;它们的振…

大模型学习笔记3【大模型】LLaMA学习笔记

文章目录 学习内容LLaMALLaMA模型结构LLaMA下载和使用好用的开源项目[Chinese-Alpaca](https://github.com/ymcui/Chinese-LLaMA-Alpaca)Chinese-Alpaca使用量化评估 学习内容 完整学习LLaMA LLaMA 2023年2月&#xff0c;由FaceBook公开了LLaMA&#xff0c;包含7B&#xff0…

echarts柱状选中shadow阴影背景宽度设置

使用line&#xff0c;宽度增大到所需要的宽度&#xff0c;设置下颜色透明度就行 tooltip: {trigger: axis,//把阴影的层级往下降z:-15,axisPointer: {type: line,lineStyle: {color: rgba(150,150,150,0.3),width: 44,type: solid,},}, }, series: [{type: bar,barWidth:20,//…

探究Executors创建的线程池(如newFixedThreadPool)其核心线程数等参数的可调整性

java中提供Executors类来创建一些固定模板参数的线程池&#xff0c;如下图&#xff08;newWorkStealingPool除外&#xff0c;这个是创建ForkJoinPool的&#xff0c;这里忽略&#xff09;&#xff1a; 拿newFixedThreadPool方法创建线程池为例&#xff0c;newFixedThreadPool是…

24位DAC转换的FPGA设计及将其封装成自定义IP核的方法

在vivado设计中,为了方便的使用Block Desgin进行设计,可以使用vivado软件把自己编写的代码封装成IP核,封装后的IP核和原来的代码具有相同的功能。本文以实现24位DA转换(含并串转换,使用的数模转换器为CL4660)为例,介绍VIVADO封装IP核的方法及调用方法,以及DAC转换的详细…