【Linux】31.Linux 多线程(5)
文章目录
- 10. 线程安全和重入问题
- 10.1 概念
- 10.2 结论
- 11. STL,智能指针和线程安全
- 11.1 STL中的容器是否是线程安全的?
- 11.2 智能指针是否是线程安全的?
- 12. 其他常见的各种锁
- 13. 读者写者问题(选学)
- 13.1 读写锁
10. 线程安全和重入问题
10.1 概念
线程安全:就是多个线程在访问共享资源时,能够正确地执行,不会相互干扰或破坏彼此的执行结果。一般而言,多个线程并发同一段只有局部变量的代码时,不会出现不同的结果。但是对全局变量或者静态变量进行操作,并且没有锁保护的情况下,容易出现该问题。
重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数。
学到现在,其实我们已经能理解重入其实可以分为两种情况
多线程重入函数
信号导致一个执行流重复进入函数
常见的线程不安全的情况:
- 不保护共享变量的函数
- 函数状态随着被调用,状态发生变化的函数
- 返回指向静态变量指针的函数
- 调用线程不安全函数的函数
常见不可重入的情况:
- 调用了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 读写锁
在编写多线程的时候,有一种情况是十分常见的。那就是,有些公共数据修改的机会比较少。相比较改写,它们读的机会反而高的多。通常而言,在读的过程中,往往伴随着查找的操作,中间耗时很长。给这种代码段加锁,会极大地降低我们程序的效率。那么有没有一种方法,可以专门处理这种多读少写的情况呢? 有,那就是读写锁。
注意:写独占,读共享,读锁优先级高
读写锁接口
设置读写优先
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);
}