【Linux】多线程的相关知识点

news2024/10/5 21:23:22

一、线程安全

1.1 可重入 VS 线程安全

1.1.1 概念

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

1.1.2 常见的线程不安全的情况

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

1.1.3 对函数状态随着被调用,状态发生变化进行解释

class A
{
public:
    void fun()
    {
        std::cout << "fun" << std::endl;
    }
}

class B : public class A
{
    int count = 0;
public:
    void test()
    {
        fun();
        count++;
        std::cout << count << std::endl;
    }
}

1.2 常见的线程安全的情况

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

1.3 常见不可重入的情况

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

1.4 常见的可重入的情况

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

1.5 可重入与线程安全的联系

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

1.6 可重入与线程安全的区别

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

二、常见锁的概念

2.1 死锁的概念:

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

2.2 出现死锁的场景:

  1. 在加锁之后,又进行了一次加锁操作
  2. 现在有两个线程:线程A和线程B。两个线程都要互相申请两个锁才能进行继续访问,但是由于访问的顺序不同,会造成死锁的现象

2.3 死锁的四个必要条件:(?????)

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

2.4 避免死锁:

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

三、Linux线程同步

3.1 条件变量

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

3.2 同步概念与竞态条件

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

四、STL、智能指针和线程安全

4.1 STL中的容器是否是线程安全的

       STL中的容器不是线程安全的,因为STL的设计初衷是将性能挖掘到极致,而一旦涉及到加锁保证线程安全,会对性能造成巨大的影响,而且对于不同的容器,加锁方式的不同,性能也可能不同(例如hash表的锁表和锁桶)

       因此STL默认不是线程安全的,如果需要在多线程环境下使用,往往需要调用者自行保证线程安全。

4.2 智能指针是否是线程安全的

       对于unique_ptr,由于只是在当前代码块范围内生效,因此不涉及线程安全问题

       对于shared_ptr,多个对象需要共用一个引用计数变量,所以会存在线程安全问题,但是标准库实现的时候考虑到这个问题,基于原子操作的方式保证shared_ptr能够足够高效,原子的操作引用计数。

五、线程安全的单例模式(有待学习)

5.1 什么是单例模式

       单例模式,属于创建类型的一种常用的软件设计模式。通过单例模式的方法创建的类在当前进程中只有一个实例。

5.2 单例模式的特点

       某些类,只应该具有一个对象(实例),就称之为单例。例如一个男人只能有一个媳妇。

       在很多服务器开发场景中,经常需要让服务器加载很多的数据到内存中,往往需要用一个单例的类来管理这些数据。

5.3 饿汉实现方式和懒汉实现方式

举个例子:

  • 吃完饭,立刻洗碗,这种就是饿汉方式。因为下一顿吃的时候可以立刻拿着碗就能吃饭
  • 吃完饭,先把碗放下,然后下一顿饭用到了这个碗再洗这个碗,这就是懒汉方式。

懒汉方式最核心的思想是:延时加载,从而能够优化服务器的启动速度。

5.3.1 饿汉方式实现单例模式

template<typename T>
class Singleton{
    static T date;
public:
    static T* GetTnstance()    
    {
        return &date;
    }
}
// 只要通过Singleton这个包装类来使用T对象,则一个进程中只有一个T对象的实例

5.3.2 懒汉方式实现单例模式

template<typename T>
class Singleton
{
    static T* inst;
public:
    static T* GetInstance()
    {
        if(inst == nullptr)
        {
            inst = new T();
        }
        return inst;
    }
};

       存在一个严重的问题,线程不安全。如果在第一次调用GetInstance的时候, 两个线程同时调用,可能会创建出两份T对象的实例,但是后续再次调用,就没有问题了。

5.4 将线程池改为懒汉方式实现单例模式(线程安全版本)

// 添加单例
static ThreadPool<T> *_instance;
static pthread_mutex_t _lock;


template <class T>
ThreadPool<T> *ThreadPool<T>::_instance;

template <class T>
pthread_mutex_t ThreadPool<T>::_lock = PTHREAD_MUTEX_INITIALIZER;
static ThreadPool<T> *GetInstance()
{
    // 如果是多线程调用以下的代码就会有问题
    // 所以我们需要进行加锁
    // 利用双判断的方式,可以有效减少获取单例的加锁成本,而且保证线程安全
    // 保证第二次之后,所有的线程不用在加锁,直接返回
    if (nullptr == _instance)
    {
        LockGuard lockguard(&_lock);
        if (nullptr == _instance)
        {
            _instance = new ThreadPool<T>;
            _instance->InitThreadPool();
            _instance->Start();
            LOG(DEBUG, "创建线程池单例");
            return _instance;
        }
     }

     LOG(DEBUG, "获取线程池单例");
     return _instance;
}
// 赋值拷贝警用
ThreadPool<T> &operator=(const ThreadPool<T> &) = delete;
ThreadPool(const ThreadPool<T> &) = delete;

六、其他常见的各种锁

6.1 悲观锁

       在每次读取数据时,总是担心数据会被其他线程修改,所以会在取数据前先加锁(读锁,写锁,行锁等),当其他线程想要访问数据时,被阻塞挂起

6.2 乐观锁

       每次取数据的时候,总是乐观的认为数据不会被其他线程修改,因此不上锁,但是在更新数据前,会判断其他数据在更新前有没有对数据进行修改,主要采用两种方式:版本号机制和CAS操作。

6.2.1 版本号机制

       一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加一。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。

6.2.2 CAS操作

       当需要更新数据时,判断当前内存值和之前取得的值是否相等,如果相等,则用新值更新;如果不相等,则失败。失败之后,需要进行重试,一般是一个自旋过程,即不断重试。

6.3 自旋锁

       在之前的学习中,我们从来没有讨论过在临界区里线程执行的时长问题:如果时间比较久:推荐其他线程阻塞挂起等待;如果时间比较短:推荐其他线程不要休眠阻塞挂起,而是不断一直抢占锁,直到申请成功(自旋)。

       自旋的过程中,用户会发现自旋锁和之前学习的互斥锁在行为上是相似的,都是阻塞在那里。

七、读者写者问题

7.1 引入读者写者问题

读者写者问题的例子:写文章,打印报纸、杂志,出黑板报

  • 读者总多,写者较少——读者写者问题最常见的情况
  • 有线程向公共资源中写入,其他线程从公共资源中读取数据——读者写者问题

7.1.1 321 原则

  • 3种关系:读者与读者(没有关系),写者与写者(互斥),读者与写者(互斥和同步)
  • 2种角色:读者,写者
  • 1种场景:公共资源

7.1.2 生产者消费者模型与读者写者问题的本质区别

  • 读者和消费者的本质区别:消费者会把数据拿走,而读者不会把数据拿走,只会进行拷贝

7.2 模拟实现一下读者写者的加锁逻辑

        对于公共资源来说,创建一个全局变量,读者锁和写者锁。但是在实际中,只要一个读者锁。

int reader_count = 0;
pthread_mutex_t wlock;
pthread_mutex_t rlock;

对于读者来说:

lock(&rlock); // 先将读者加锁
if(reader_count == 0)
{
    lock(&wlock); // 变量为空,说明第一次读,将写者加锁
        // 这种操作只会进行一次,否则就有死锁
    //如果申请成功,继续运行,不会有任何读者进来
    //如果申请失败,阻塞
}
++ reader_count;
unlock(&rlock);


// 开始进行常规的read

lcok(&rlock);
--read_count;
if(read_count == 0) // 如果读者数量为0,则可以唤醒写者
{
   unlock(&wlock); 
}
unlock(&rlock);

对于写者来说:

lock(&wlock);

// 写入操作

unlock(&wlock);

7.3 了解一下系统中读写锁的接口

7.3.1 初始化读写锁

函数的原型:

#include <pthread.h>

int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, 
                                 const pthread_rwlockattr_t* restrict attr);

函数的功能:

        进行初始化读写锁
函数的参数:

  • rwlock:指向创建的读写锁对象
  • attr:属性,一般置为nullptr

函数的返回值:

  • 成功返回 0, 失败直接返回错误号

7.3.2 销毁读写锁

函数的原型:

#include <pthread.h>

int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);

函数的功能:

       将所创建好的读写锁进行销毁
函数的参数:

  • rwlock:执行所要销毁的读写锁的指针

函数的返回值:

  • 成功返回 0, 失败直接返回错误号

7.3.3 给读者锁加锁

函数的原型:

#include <pthread.h>

int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);

7.3.4 给写者锁加锁

函数的原型:

#include <pthread.h>

int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);

7.3.5 给读者锁和写者锁解锁

函数的原型:

#include <pthread.h>

int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

7.3.6 代码部分

#include <pthread.h>
#include <stdio.h>
#include <iostream>

// 读写锁的概念

int count = 0;           // 共享资源
pthread_rwlock_t rwlock; // 创建一个读写锁

#define NUM 5

// 读者
void *reader(void *arg)
{
    pthread_rwlock_rdlock(&rwlock); // 给读者加锁
    std::cout << "Reader conut:" << count << std::endl;
    pthread_rwlock_unlock(&rwlock); // 进行解锁
    return nullptr;
}

// 写者
void *writer(void *arg)
{
    pthread_rwlock_wrlock(&rwlock); // 给写者加锁
    count++;
    pthread_rwlock_unlock(&rwlock); // 给读者解锁
    return nullptr;
}

int main()
{
    pthread_t reader_threads[NUM], writer_threads;
    pthread_rwlock_init(&rwlock, nullptr); // 给读写锁进行初始化

    pthread_create(&writer_threads, nullptr, writer, nullptr);
    for (int i = 0; i < NUM; i++)
    {
        pthread_create(&reader_threads[i], nullptr, reader, nullptr);
    }

    pthread_join(writer_threads, nullptr);
    for (int i = 0; i < NUM; i++)
    {
        pthread_join(reader_threads[i], nullptr);
    }

    pthread_rwlock_destroy(&rwlock);

    return 0;
}

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

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

相关文章

Android性能优化-内存优化

&#xff11;、为什么进行内存优化&#xff08;如果不进行内存优化&#xff09; APP运营内存限制&#xff0c;OOM导致APP崩溃 APP性能&#xff0c;流畅性&#xff0c;响应速度和体验 2、Android内存管理方式: Android系统内存分配与回收方式 APP内存限制机制 切换应用时&…

【日常开发之Windows共享文件】Java实现Windows共享文件上传下载

文章目录 Windows 配置代码部分Maven代码 Windows 配置 首先开启服务&#xff0c;打开控制面板点击程序 点击启用或关闭Windows功能 SMB1.0选中红框内的 我这边是专门创建了一个用户 创建一个文件夹然后点击属性界面&#xff0c;点击共享 下拉框选择你选择的用户点击添加…

自建消息推送工具 Gotify 实现消息私有化通知

本文首发于只抄博客,欢迎点击原文链接了解更多内容。 前言 之前分享了如何通过 Webhook 将 VPS 与 NAS 上部署的应用消息推送到钉钉、飞书、企业微信,但是对于部分用户来说,可能因为以下种种原因,不方便使用常见的办公 IM 软件来进行消息推送: 消息涉及隐私敏感信息,不希…

艺术签名生成工具哪个好?5个工具定制个性化签名

在追求个性化的现代社会&#xff0c;艺术签名已经成为一种时尚和趋势&#xff0c;越来越多的人开始关注和尝试学习如何设计自己的艺术签名。 这不仅是一种表达自我的方式&#xff0c;也是一种展现个性和独特性的方式。今天让我们一起探索5款艺术签名在线生成工具&#xff0c;让…

【高性能计算笔记】

第1章 - 高性能计算介绍 1. 概念&#xff1a; 高性能计算(High performance computing&#xff0c;缩写HPC)&#xff1a; 指通常使用很多处理器&#xff08;作为单个机器的一部分&#xff09;或者某一集群中组织的几台计算机&#xff08;作为单个计算资源操作&#xff09;的…

百度Agent初体验(制作步骤+感想)

现在AI Agent很火&#xff0c;最近注册了一个百度Agent体验了一下&#xff0c;并做了个小实验&#xff0c;拿它和零一万物&#xff08;Yi Large&#xff09;和文心一言&#xff08;ERNIE-4.0-8K-latest&#xff09;阅读了相同的一篇网页资讯&#xff0c;输出资讯摘要&#xff0…

shell的正则表达------awk

一、awk&#xff1a;按行取列 1.awk原理&#xff1a;根据指令信息&#xff0c;逐行的读取文本内容&#xff0c;然后按照条件进行格式化输出。 2.awk默认分隔符&#xff1a;空格、tab键&#xff0c;把多个空格自动压缩成一个。 3.awk的选项&#xff1a; awk ‘操作符 {动作}’…

【总线】AXI4第五课时:信号描述

大家好,欢迎来到今天的总线学习时间!如果你对电子设计、特别是FPGA和SoC设计感兴趣&#xff0c;那你绝对不能错过我们今天的主角——AXI4总线。作为ARM公司AMBA总线家族中的佼佼者&#xff0c;AXI4以其高性能和高度可扩展性&#xff0c;成为了现代电子系统中不可或缺的通信桥梁…

不同匿名程度的代理本质区别是什么?

区别主要在于匿名的程度不同&#xff0c;就看你自己对匿名要求高不高了。 有三种主要代理类型&#xff1a; 1、透明代理 透明代理的特点就是不提供匿名性&#xff0c;你用它的时候网站是可以直接读取到你的真实IP地址的&#xff0c;需要提供的就可以直接排除它了。 2、匿名…

JVM专题八:JVM如何判断可回收对象

在JVM专题七&#xff1a;JVM垃圾回收机制中提到JVM的垃圾回收机制是一个自动化的后台进程&#xff0c;它通过周期性地检查和回收不可达的对象&#xff08;垃圾&#xff09;&#xff0c;帮助管理内存资源&#xff0c;确保应用程序的高效运行。今天就让我们来看看JVM到底是怎么定…

Shopee API接口:获取搜索栏生成的商品结果列表

一、引言 此接口可以高效获取搜索栏生成的商品结果列表。本文将详细介绍这一核心功能&#xff0c;并探讨其在实际应用中的价值。 二、核心功能介绍——获取搜索栏生成的商品结果列表 请求API及返回示例 http://api.xxxx.com/sp/ll/search/item?keywordiphone&page1&am…

零门槛用AI,302.AI让人工智能变得简单易用

当下人工智能火爆&#xff0c;提到AI&#xff0c;几乎每个人都能说上几句&#xff0c;但是你真的会使用AI吗&#xff1f; 当涉及到如何实际使用AI时&#xff0c;许多人可能会觉得它太过高深莫测&#xff0c;从而产生一种距离感&#xff0c;不知如何开始。我和大家也一样&#x…

期末考试的成绩怎么发?

随着学期末的临近&#xff0c;我们又迎来了向家长通报学生成绩的关键时刻。下面是一份成绩群发的全新指南&#xff0c;让我们一起高效而温馨地完成这项任务&#xff01; 1.选择沟通渠道&#xff1a; - 邮件与短信各有优势。邮件更适合提供详尽的成绩分析和评语&#xff0c;而短…

云计算【第一阶段(18)】磁盘管理与文件系统 分区格式挂载(一)

目录 一、磁盘基础 二、磁盘结构 2.1、机械硬盘 2.2、固态硬盘 2.3、扩展移动硬盘 2.4、机械磁盘的一些计算&#xff08;了解&#xff09; 2.5、磁盘接口类型 二、Linux 中使用的文件系统类型 2.1、磁盘分区的表示 2.1.1、主引导记录(MBR) 2.1.2、Linux中将硬盘、分…

【UIDynamic-动力学-UIPushBehavior-推行为 Objective-C语言】

一、接下来,我们来说这个,推行为, 1.推行为,首先,它叫做UIPushBehavior, 这个里边呢,又分为持续推力、瞬时推力, 我们新建一个项目,叫做:13-推行为 我们这个里边,还是先来一个redView, UIView *redView = [[UIView alloc] initWithFrame:CGRectMake(100,100,…

二刷算法训练营Day41 (Day40休息) | 动态规划(3/17)

目录 详细布置&#xff1a; 1. 背包问题理论基础 1.1 01背包 2. 46. 携带研究材料&#xff08;第六期模拟笔试&#xff09; 一维dp数组&#xff08;滚动数组&#xff09; 3. 416. 分割等和子集 详细布置&#xff1a; 1. 背包问题理论基础 但说实话&#xff0c;背包九讲…

ONLYOFFICE 8.1全新升级,智能办公体验再升级,引领未来工作新潮流!

&#x1f4dd;个人主页&#x1f339;&#xff1a;Eternity._ &#x1f339;&#x1f339;期待您的关注 &#x1f339;&#x1f339; ❀ONLYOFFICE 8.1 &#x1f4d2;1. ONLYOFFICE简介&#x1f4d9;2. ONLYOFFICE特点&#x1f4d5;3. ONLYOFFICE功能⛰️PDF 文件编辑器&#x1…

win10系统管理员账号怎么切换

1、按住“windowsx”&#xff0c;选择“计算机管理” 2、在页面左侧&#xff0c;找到“计算机管理(本地)”&#xff0c;展开“系统工具”&#xff0c;点击“本地用户和组”下面的“用户”&#xff0c;在右侧找到“Administrator”&#xff0c;双击打开。 3、在打开页面选择常规…

【分布式事务】Seata AT实战

目录 Seata 介绍 Seata 术语 Seata AT 模式 介绍 实战&#xff08;nacos注册中心&#xff0c;db存储&#xff09; 部署 Seata 实现 RM 实现 TM 可能遇到的问题 1. Seata 部署成功&#xff0c;服务启动成功&#xff0c;全局事务不生效 2. 服务启动报错 can not get …

Windows安装jdk配置环境变量(基础)

一、下载安装JDK 下载地址:https://www.oracle.com/java/technologies/downloads/?er=221886#java8-windows 因为JDK8比较稳定,所以建议选择这个。电脑32位的下载jdk-8u411-windows-i586.exe;电脑是64位的下载jdk-8u411-windows-x64.exe 1、根据自己电脑的配置下载相应的…