多线程学习总结(1):https://blog.youkuaiyun.com/hansionz/article/details/84665815
多线程学习总结(2):https://blog.youkuaiyun.com/hansionz/article/details/84675536
多线程学习总结(3):https://blog.youkuaiyun.com/hansionz/article/details/84766601
Linux多线程学习
一.读写锁
1.什么是读写锁
学习多线程
的时候,有一种情况是十分常见的。那就是有些公共数据修改
的机会比较少。相比较改写,它们读的机会反而高
的多。通常而言,对临界资源
的修改,一定要加上互斥量去保护临界资源
,但是在读的过程
中,往往伴随着查找
的操作,中间耗时
很长。如果给这种代码段加互斥锁
,会极大地降低程序的效率
。读写锁就是解决多读少写
问题。
读写锁支持当没有线程去写入
的时候,可以存在多个读者线程同时去共享的访问临界资源
,而当临界区没有读者线程去访问或者没有写者线程去写的时候
才允许该线程去写
。这种用于共享访问给定资源的读写锁
,也叫共享-独占锁
,获取一个读写锁用于读称为共享锁
,获取一个读写锁用于写称为独占锁
。
2.读者和写者的关系
- 读者和读者:
共享
关系。可以允许多个线程同时读 - 写者和写者:
互斥
关系。当有一个线程在写,其他线程不能写入 - 读者和写者:
步与互斥
。当有读者在读或者写者在写的时候,不能存在其他线程写;当当有线程在读的时候,不能有其他线程写入。
读写锁分配规则:
- 只要没有线程拿着
读写锁用于写
,任意数目
的线程可以拿到读写锁用来读
- 如果没有线程拿着读写锁用来
读或写
的时候,才可以存在线程用来写
读写锁的行为:
总结:写独占、读共享、写锁优先级高
3.初始化读写锁
读写锁的数据类型为pthread_rwlock_t
。它存在两种初始化方式:
- 静态方法:
//直接给读写锁变量赋值PTHREAD_RWLOCK_INITALIZER
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITALIZER
- 动态分配
//调用函数动态初始化
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,const pthread_rwlockattr_t *restrict attr);
参数:
rwlock为读写锁变量的地址
attr为读写锁的属性,一般不使用可以设置为NULL
返回值:成功返回0,失败返回错误码
4.销毁读写锁
当所有的线程不在持
有也不在去申请读写锁
的时候,该读写锁应该被销毁
。
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
参数:
rwlock为读写锁变量的地址
返回值:成功返回0,失败返回错误码
5.获取和释放读写锁
pthread_rwlock_rlock
函数用来获取一个读锁,如果对于的读写锁
被某个写者线程拥有,则该函数会阻塞调用线程
。pthread_rwlock_wrlock
用来获取一个写锁,如果对应的读写锁由另一个写入者
或者一个或多个读者
所有,那么阻塞调用线程。pthread_rwlock_unlock
函数用来释放一个读锁或写锁
。
//获取读锁
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
//获取写锁
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
//释放读写锁
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
返回值:成功返回0,出错返回错误码
下面两个函数用来尝试获取读写锁
,但是如果锁不能立即获得
,就返回EBUSY
错误,而不是调用阻塞
线程。
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwptr);
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwptr);
返回值:成功返回0,出错返回错误码
注:以上的函数均位于头文件<pthread.h>
中
6.读写锁的实例
#include <iostream>
#include <unistd.h>
using namespace std;
int book = 0;
pthread_rwlock_t rwlock;
//可以存在任意多个线程同时读
void *read_routine(void *arg)
{
while(1)
{
pthread_rwlock_rdlock(&rwlock);
cout << "my tid is:" << pthread_self() << ".read book data is:" << book << endl;
pthread_rwlock_unlock(&rwlock);
sleep(1);
}
}
//任意时刻只能有一个写者线程在写(在写的时候不能有读者读)
void *write_routine(void *arg)
{
while(1)
{
pthread_rwlock_wrlock(&rwlock);
++book;
cout << "my tid is:"<< pthread_self() << ".write book data is:" << book << endl;
pthread_rwlock_unlock(&rwlock);
sleep(2);
}
}
int main()
{
//初始化读写锁变量
pthread_rwlock_init(&rwlock, NULL);
pthread_t r1,r2,w1,w2;//创建两个读者和写者线程
pthread_create(&r1, NULL, read_routine, NULL);
pthread_create(&r2, NULL, read_routine, NULL);
pthread_create(&w1, NULL, write_routine, NULL);
pthread_create(&w2, NULL, write_routine, NULL);
pthread_join(r1, NULL);
pthread_join(r2, NULL);
pthread_join(w1, NULL);
pthread_join(w2, NULL);
//销毁读写锁
pthread_rwlock_destroy(&rwlock);
return 0;
}
以上程序的运行结果应该是任意一个时刻,只存在一个写者在修改book
变量,但是当没有写者在写的时候,可以允许两个读者同时在读book
变量的值。以下为运行结果:
二.其他常见的各种锁
1.乐观锁和悲观锁
- 悲观锁::在每次取数据时,总是
担心数据
会被其他线程修改,所以会在取数据前先加锁(读锁,写锁, 行锁等)
,当其他线程想要访问数据时,被阻塞
挂起。悲观锁适用于多写的场景,因为多写的情况会经常产生冲突。 - 乐观锁:每次取数据时候,总是
乐观
的认为数据不会被其他线程
修改,因此不上锁
。但是在更新数据前
,会判断
其他线程在更新前有没有
对数据进行修改。乐观锁适用于多读
的应用类型,这样可以提高吞吐量
。主要采用两种方式:版本号机制和CAS操作。 - CAS操作:当需要
更新数据
时,判断当前内存值
和之前取得的值
是否相等。如果相等则用新值更新
。若不等则失败
,失败则重试。 即compare and swap(比较与交换)
,是一种有名的无锁算法
。无锁编程,即不使用锁的情况下实现多线程
之间的变量同步
,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)
。CAS算法涉及到三个操作数:需要读写的内存值 V
、进行比较的值 A
、拟写入的新值 B
。当且仅当 V 的值等于A时,CAS通过原子方式用新值B来更新V的值,否则不会执行任何操作(比较和替换是一个原子操作)。一般情况下是一个自旋操作,即不断的重试。 - 版本号机制:一般是在
数据表
中加上一个数据版本号version
字段,表示数据被修改的次数
,当数据被修改时,version
值会加1。当线程A要更新数据
值时,在读取数据的同时也会读取version
值,在提交更新
时,若刚才读取到的version
值为当前数据库中的version
值相等时才更新,否则重试
更新操作,直到更新成功。
推荐阅读:https://blog.youkuaiyun.com/qq_34337272/article/details/81072874
2.自旋锁
自旋锁(spinlock)
:是指当一个线程在获取锁
的时候,如果锁已经被其它线程获取
,那么该线程将循环等待
,然后不断的判断锁
是否能够被成功获取,直到获取到锁才会退出循环
。
获取锁的线程一直处于活跃状态
,但是并没有执行任何有效的任务
,使用这种锁会造成busy-waiting
。
它是为实现保护共享资源
而提出一种锁机制
。其实,自旋锁
与互斥锁
比较类似,它们都是为了解决对某项资源
的互斥
使用。无论是互斥锁
,还是自旋锁
,在任何时刻,最多只能有一个保持者
,也就说,在任何时刻最多只能有一个执行单元
获得锁。但是两者在调度机制
上略有不同。对于互斥锁
,如果资源已经被占用,资源申请者只能进入睡眠状态
。但是自旋锁
不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持
,调用者就一直循环在那里
看是否该自旋锁的保持者已经释放
了锁,”自旋”
一词就是因此而得名。
推荐阅读:https://blog.youkuaiyun.com/qq_34337272/article/details/81252853
3.公平锁和非公平锁
- 公平锁:按照线程
加锁的顺序
来分配,即先来先得FIFO
- 非公平锁:一种
获取锁
的抢占机制,是随机的获得锁
的,这样可能会有些线程一直会拿不到锁
,结果也就是不公平
的
三.线程安全
1.什么是线程安全
线程安全是多线程编程
中的一个概念。在拥有共享数据
的多个线程
并行执行的程序中,线程安全
的代码会通过同步机制
保证各个线程都可以正常且正确
的执行,不会出现数据污染
等错误情况。
2.线程安全版本的单例模式
对于线程安全版本的单例模式单独总结于我的另外一边博客:https://blog.youkuaiyun.com/hansionz/article/details/83752531
主要问题:
- 加锁解锁的
位置
- 双重
if
判定, 避免不必要的锁竞争 volatile
关键字防止过度优化
3.STL中的容器是否是线程安全的
答:不是
。原因是STL
的设计初衷是将性能挖掘到极致
, 而一旦涉及到加锁保证
线程安全, 会对性能
造成巨大的影响。且对于不同的容器
加锁方式的不同,性能可能也不同(例如hash表的锁表和锁桶)
。 因此 STL
默认不是线程安全, 如果需要在多线程环境
下使用,往往需要调用者自行保证
线程安全。
4.智能指针是否是线程安全的
- 对于
unique_ptr
, 由于只是在当前代码块范围
内生效, 因此不涉及线程安全
问题。 - 对于
shared_ptr
, 多个对象需要共用一个引用计数变量
, 所以会存在线程安全
问题.。但是标准库实现的时候考虑到了这个问题
,基于原子操作(CAS)
的方式保证shared_ptr
能够高效,原子的操作引用计数。