当前位置: 首页 > article >正文

【Linux】31.Linux 多线程(5)

文章目录

  • 10. 线程安全和重入问题
    • 10.1 概念
    • 10.2 结论
  • 11. STL,智能指针和线程安全
    • 11.1 STL中的容器是否是线程安全的?
    • 11.2 智能指针是否是线程安全的?
  • 12. 其他常见的各种锁
  • 13. 读者写者问题(选学)
    • 13.1 读写锁


10. 线程安全和重入问题

10.1 概念

  • 线程安全:就是多个线程在访问共享资源时,能够正确地执行,不会相互干扰或破坏彼此的执行结果。一般而言,多个线程并发同一段只有局部变量的代码时,不会出现不同的结果。但是对全局变量或者静态变量进行操作,并且没有锁保护的情况下,容易出现该问题。

  • 重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数。

学到现在,其实我们已经能理解重入其实可以分为两种情况

  1. 多线程重入函数

  2. 信号导致一个执行流重复进入函数

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

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

常见不可重入的情况:

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

常见的线程安全的情况:

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

常见可重入的情况:

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

10.2 结论

可重入与线程安全联系:

  • 函数是可重入的,那就是线程安全的(其实知道这一句话就够了)
  • 函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题
  • 如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的。

可重入与线程安全区别:

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

注意:

  • 如果不考虑 信号导致一个执行流重复进入函数 这种重入情况,线程安全和重入在安全角度不做区分
  • 但是线程安全侧重说明线程访问公共资源的安全情况,表现的是并发线程的特点
  • 可重入描述的是一个函数是否能被重复进入,表示的是函数的特点

11. STL,智能指针和线程安全

11.1 STL中的容器是否是线程安全的?

不是。

原因是, STL 的设计初衷是将性能挖掘到极致, 而一旦涉及到加锁保证线程安全, 会对性能造成巨大的影响。

而且对于不同的容器, 加锁方式的不同, 性能可能也不同(例如hash表的锁表和锁桶)。

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


11.2 智能指针是否是线程安全的?

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

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


12. 其他常见的各种锁

  • 悲观锁:在每次取数据时,总是担心数据会被其他线程修改,所以会在取数据前先加锁(读锁,写锁,行锁等),当其他线程想要访问数据时,被阻塞挂起。
  • 乐观锁:每次取数据时候,总是乐观的认为数据不会被其他线程修改,因此不上锁。但是在更新数据前,会判断其他数据在更新前有没有对数据进行修改。主要采用两种方式:版本号机制和CAS操作。
  • CAS操作:当需要更新数据时,判断当前内存值和之前取得的值是否相等。如果相等则用新值更新。若不等则失败,失败则重试,一般是一个自旋的过程,即不断重试。
  • 自旋锁,读写锁

13. 读者写者问题(选学)

13.1 读写锁

在编写多线程的时候,有一种情况是十分常见的。那就是,有些公共数据修改的机会比较少。相比较改写,它们读的机会反而高的多。通常而言,在读的过程中,往往伴随着查找的操作,中间耗时很长。给这种代码段加锁,会极大地降低我们程序的效率。那么有没有一种方法,可以专门处理这种多读少写的情况呢? 有,那就是读写锁。

513a69036fc5d188f32a2dfbedfc9a0f

注意:写独占,读共享,读锁优先级高

读写锁接口

设置读写优先

int pthread_rwlockattr_setkind_np(pthread_rwlockattr_t *attr, int pref); 

参数:
- attr: 读写锁属性对象指针
- pref: 优先级策略,可以是以下值:
  * PTHREAD_RWLOCK_PREFER_READER_NP: 读者优先(默认)
  * PTHREAD_RWLOCK_PREFER_WRITER_NP: 写者优先
  * PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP: 写者优先(非递归)

返回值:
- 成功返回 0
- 失败返回错误码

初始化

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

参数:
- rwlock: 指向读写锁的指针
- attr: 读写锁属性对象指针。如果为 NULL,使用默认属性

返回值:
- 成功返回 0
- 失败返回错误码

销毁

int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);

参数:
- rwlock: 要销毁的读写锁指针

返回值:
- 成功返回 0
- 失败返回错误码

加锁和解锁

// 读加锁
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
// 写加锁
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
// 解锁
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

参数:
- rwlock: 读写锁指针

返回值:
- 成功返回 0
- 失败返回错误码

读写锁案例:

#include <vector>      // 用于存储线程属性
#include <sstream>     // 用于字符串流处理
#include <cstdio>      // 标准输入输出
#include <cstdlib>     // 标准库函数
#include <cstring>     // 字符串处理
#include <unistd.h>    // POSIX操作系统API
#include <pthread.h>   // POSIX线程库

// volatile确保ticket的值不会被编译器优化,每次都从内存读取
// 共享资源:1000张票
volatile int ticket = 1000; 
// 声明读写锁
pthread_rwlock_t rwlock; 

// 读者线程函数
void * reader(void * arg) 
{ 
    char *id = (char *)arg;  // 转换参数为字符串(线程ID)
    while (1) { 
        // 获取读锁(共享锁)
        // 多个读者可以同时获得读锁
        pthread_rwlock_rdlock(&rwlock); 
        
        // 检查票是否卖完
        if (ticket <= 0) { 
            pthread_rwlock_unlock(&rwlock); 
            break; 
        } 
        // 读取票数(不修改)
        printf("%s: %d\n", id, ticket); 
        // 释放读锁
        pthread_rwlock_unlock(&rwlock); 
        // 休眠1微秒,模拟读取时间
        usleep(1); 
    } 

    return nullptr; 
} 

// 写者线程函数
void * writer(void * arg) 
{ 
    char *id = (char *)arg;  // 转换参数为字符串(线程ID)
    while (1) { 
        // 获取写锁(独占锁)
        // 同一时刻只能有一个写者持有写锁
        pthread_rwlock_wrlock(&rwlock); 
        
        // 检查票是否卖完
        if (ticket <= 0) { 
            pthread_rwlock_unlock(&rwlock); 
            break; 
        } 
        // 修改票数(减1)并打印
        printf("%s: %d\n", id, --ticket); 
        // 释放写锁
        pthread_rwlock_unlock(&rwlock); 
        // 休眠1微秒,模拟写入时间
        usleep(1); 
    } 

    return nullptr; 
} 

// 线程属性结构体:存储线程ID和名称
struct ThreadAttr 
{ 
    pthread_t tid;      // 线程ID
    std::string id;     // 线程名称
}; 

// 创建读者线程的ID字符串
std::string create_reader_id(std::size_t i) 
{ 
    // 利用ostringstream进行string拼接 
    // ate表示从末尾开始写入
    std::ostringstream oss("thread reader ", std::ios_base::ate); 
    oss << i;  // 添加编号
    return oss.str(); 
} 

// 创建写者线程的ID字符串
std::string create_writer_id(std::size_t i) 
{ 
    std::ostringstream oss("thread writer ", std::ios_base::ate); 
    oss << i; 
    return oss.str(); 
} 

// 初始化读者线程
void init_readers(std::vector<ThreadAttr>& vec) 
{ 
    for (std::size_t i = 0; i < vec.size(); ++i) { 
        // 设置线程名称
        vec[i].id = create_reader_id(i); 
        // 创建线程,传入reader函数和线程名称
        pthread_create(&vec[i].tid, nullptr, reader, (void *)vec[i].id.c_str()); 
    } 
} 

// 初始化写者线程
void init_writers(std::vector<ThreadAttr>& vec) 
{ 
    for (std::size_t i = 0; i < vec.size(); ++i) { 
        // 设置线程名称
        vec[i].id = create_writer_id(i); 
        // 创建线程,传入writer函数和线程名称
        pthread_create(&vec[i].tid, nullptr, writer, (void *)vec[i].id.c_str()); 
    } 
} 

// 等待所有线程结束
void join_threads(std::vector<ThreadAttr> const& vec) 
{ 
    // 按创建的逆序来回收线程
    // 使用反向迭代器遍历
    for (std::vector<ThreadAttr>::const_reverse_iterator it = vec.rbegin(); 
         it != vec.rend(); ++it) { 
        pthread_t const& tid = it->tid; 
        pthread_join(tid, nullptr); 
    } 
} 

// 初始化读写锁
void init_rwlock() 
{ 
    #if 0 // 写优先模式(当前未启用)
    pthread_rwlockattr_t attr;  // 读写锁属性
    pthread_rwlockattr_init(&attr);  // 初始化属性
    // 设置为写者优先
    pthread_rwlockattr_setkind_np(&attr, PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP); 
    pthread_rwlock_init(&rwlock, &attr);  // 用属性初始化读写锁
    pthread_rwlockattr_destroy(&attr);    // 销毁属性
    #else // 读优先模式(默认)
    pthread_rwlock_init(&rwlock, nullptr); 
    #endif 
} 

int main() 
{ 
    // 设置读者和写者的数量
    // reader_nr不能太大,否则系统可能无法调度主线程
    const std::size_t reader_nr = 1000;  // 1000个读者
    const std::size_t writer_nr = 2;     // 2个写者

    // 创建存储线程属性的容器
    std::vector<ThreadAttr> readers(reader_nr); 
    std::vector<ThreadAttr> writers(writer_nr); 

    // 初始化读写锁
    init_rwlock(); 

    // 创建读者和写者线程
    init_readers(readers); 
    init_writers(writers); 

    // 等待所有线程结束
    join_threads(writers); 
    join_threads(readers); 

    // 销毁读写锁
    pthread_rwlock_destroy(&rwlock); 
}

http://www.kler.cn/a/540139.html

相关文章:

  • 【信息系统项目管理师-案例真题】2017下半年案例分析答案和详解
  • 【虚幻引擎UE】AOI算法介绍与实现案例
  • 【电机控制器】STC8H1K芯片——低功耗
  • 33. 搜索旋转排序数组
  • 人工智能A*算法与CNN结合- CNN 增加卷积层的数量,并对卷积核大小进行调整
  • redis高级数据结构布隆过滤器
  • Python+Flask搭建属于自己的B站,管理自己电脑里面的视频文件。支持对文件分类、重命名、删除等操作。
  • 日志统计(acWing,蓝桥杯)
  • PLSQL: 存储过程,用户自定义函数[oracle]
  • python-leetcode-组合总和
  • win10 llamafactory模型微调相关① || Ollama运行微调模型
  • 【论文阅读】Comment on the Security of “VOSA“
  • 并查集知识整理、蓝桥杯修改数组
  • 【vue】高德地图AMap.Polyline动态更新画折线,逐步绘制
  • 深度学习-神经机器翻译模型
  • 【1.05版】wordpressAI插件批量生成文章、图片、长尾关键词、文章采集、AI对话等
  • 软件工程 项目管理
  • 使用 mkcert 本地部署启动了 TLS/SSL 加密通讯的 MongoDB 副本集和分片集群
  • mysql 学习12 存储引擎,mysql体系结构
  • 技术栈选择:Vue 还是 React
  • gptme - 终端中的个人 AI 助手
  • 《一》深入了解软件测试工具 JMeter-自我介绍
  • 基于lstm+gru+transformer的电池寿命预测健康状态预测-完整数据代码
  • iOS Swift算法之KDF2
  • 【1】深入解析 SD-WAN:从思科 SD-WAN 视角看现代网络发展
  • 题解:P1005 [NOIP 2007 提高组] 矩阵取数游戏