文章目录
1. 死锁
1.1 概念
死锁是指在⼀组进程中的各个进程均占有不会释放的资源,但
因互相申请被其他进程所占用且不会释放的资源
而处于的⼀种永久等待状态。
所以,可能会造成死锁的局面。
1.2 死锁形成的四个必要条件
- 互斥条件:⼀个资源每次只能被⼀个执行流使用
- 请求与保持条件:⼀个执行流因请求资源而阻塞时,对已获得的资源保持不放
- 不剥夺条件:⼀个执行流已获得的资源,在未使用完之前,不能强行剥夺
- 循环等待条件:若干执行流之间形成⼀种头尾相接的循环等待资源的关系
1.3 避免死锁
方式:破坏死锁的四个必要条件。
互斥条件:
- 破坏难度:由于互斥性是资源访问的基本特性,因此很难或不应该被破坏。
请求和保持条件:
- 破坏策略:可以采用静态分配策略,即进程在运行前一次性申请完它所需要的全部资源,在资源未满足前,它不启动。这样就不会出现占有资源又等待其他资源的情况,从而破坏请求和保持条件。
不剥夺条件
- 破坏策略:可以采用剥夺式调度策略,即当一个进程申请新资源而得不到满足时,可以释放它所占有的部分资源,以便其他进程使用,从而破坏不剥夺条件。
循环等待条件
- 破坏策略:可以采用顺序资源分配法,即首先给系统中的资源编号,规定每个进程必须按编号递增的顺序请求资源,只能申请编号比之前大的资源。这样可以避免形成循环等待链,从而破坏循环等待条件。
例如:破环请求与保持条件,使资源⼀次性分配。
lock函数可确保所传递的锁对象全部获取成功,本质就是先申请一把锁,在申请的锁种再申请提供的锁对象,因为申请一把锁的操作是原子的。
#include<iostream>#include<mutex>#include<unistd.h>// 定义两个共享资源(整数变量)和两个互斥锁int shared_resource1 =0;int shared_resource2 =0;
std::mutex mtx1, mtx2;// ⼀个函数,同时访问两个共享资源voidaccess_shared_resources(){
std::unique_lock<std::mutex>lock1(mtx1, std::defer_lock);
std::unique_lock<std::mutex>lock2(mtx2, std::defer_lock);// // 使⽤ std::lock 同时锁定两个互斥锁
std::lock(lock1, lock2);// 现在两个互斥锁都已锁定,可以安全地访问共享资源int cnt =10000;while(cnt){++shared_resource1;++shared_resource2;
cnt--;}// 当离开 access_shared_resources 的作⽤域时,lock1 和 lock2 的析构函数会被⾃动调⽤// 这会导致它们各⾃的互斥量被⾃动解锁}
2. 读者写者问题与读写锁
2.1 读者写者问题
读者写者问题其实与生产者消费者问题类似,都是多线程之间互相同步的一种策略。
例如我们在写博客的时候,在我写的时候你是看不到的,直到我发布出去,你才能看到;与此同时在你看的时候,可能还有很多人都在看。
读者写者问题也应该遵循
“321"
原则:3种关系,2种角色,1个交易场所。
三种关系如下:
- 写者与写者之间互斥,即一个写者在修改数据时,其他写者不能访问。
- 读者与写者之间互斥&&同步,即不能同时有一个线程在读,而另一个线程在写。 - 当一个读者申请进行读操作时,如果已有写者在访问共享资源,则该读者必须等到没有写者访问后才能开始读操作。- 当一个写者申请进行写操作时,如果已有读者正在读取数据,写者必须等待所有读者完成读取后才能开始写操作。
- 读者之间可以
并发
(即没关系),即可以有一个或多个读者在读。
读者写者 vs 生成者消费者
二者最大的区别就是:消费者会“取走”共享资源,而读者不会。
伪代码理解读者写者的逻辑:
2.2 读写锁的使用
初始化:
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,const pthread_rwlockattr_t *restrict attr);
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;
销毁:
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
加锁:
- 读者加锁
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
- 写者加锁
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
解锁:
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
#include<iostream>#include<unistd.h>#include<pthread.h>#include<sys/types.h>
pthread_rwlock_t rwlock;// 读写锁staticint g_data =0;// 共享资源
pthread_mutex_t lockScreen;void*Reader(void*args){int id =*(int*)args;while(true){pthread_rwlock_rdlock(&rwlock);// 读者加锁pthread_mutex_lock(&lockScreen);
std::cout <<"读者-"<< id <<" 正在读取数据, 数据是: "<< g_data << std::endl;pthread_mutex_unlock(&lockScreen);sleep(2);pthread_rwlock_unlock(&rwlock);// 读者解锁sleep(1);}delete(int*)args;returnnullptr;}void*Writer(void*args){int id =*(int*)args;while(true){pthread_rwlock_wrlock(&rwlock);// 写者加锁
g_data +=2;// 修改共享数据pthread_mutex_lock(&lockScreen);
std::cout <<"写者- "<< id <<" 正在写入. 新的数据是: "<< g_data << std::endl;pthread_mutex_unlock(&lockScreen);sleep(1);pthread_rwlock_unlock(&rwlock);// 解锁sleep(1);}delete(int*)args;returnNULL;}intmain(){pthread_rwlock_init(&rwlock,nullptr);// 初始化读写锁pthread_mutex_init(&lockScreen,nullptr);constint reader_num =5;// 读者数量constint writer_num =10;// 写者数量constint total = reader_num + writer_num;
pthread_t threads[total];for(int i =0; i < reader_num; i++){int*id =newint(i);pthread_create(&threads[i],0, Reader, id);}for(int i = reader_num; i < total; i++){int*id =newint(i - reader_num);pthread_create(&threads[i],0, Writer, id);}for(int i =0; i < total; i++){pthread_join(threads[i],nullptr);}return0;}
2.3 读写策略
- 读者优先 在这种策略中, 系统会尽可能多地允许多个读者同时访问资源(比如共享文件或数据) , 而不会优先考虑写者。 这意味着当有读者正在读取时, 新到达的读者会立即被允许进入读取区, 而写者则会被阻塞, 直到所有读者都离开读取区。 读者优先策略可能会导致写者饥饿(即写者长时间无法获得写入权限), 特别是当读者频繁到达时。
- 写者优先 在这种策略中, 系统会优先考虑写者。 当写者请求写入权限时, 系统会尽快地让写者进入写入区, 即使此时有读者正在读取。 这通常意味着一旦有写者到达, 所有后续的读者都会被阻塞, 直到写者完成写入并离开写入区。 写者优先策略可以减少写者等待的时间, 但可能会导致读者饥饿(即读者长时间无法获得读取权限) , 特别是当写者频繁到达时。
选择合适的策略时,需要根据具体的应用场景和需求进行权衡。例如,在需要频繁读取而写入较少的应用中,读者优先策略可能更为合适;而在需要频繁写入的应用中,写者优先策略可能更为合适。
3. 自旋锁
3.1 概念
在我们之前讲的信号量或互斥锁,都有一个特点:申请锁失败,申请线程都要阻塞挂起等待。
但是,当一个线程在临界区内执行的时长非常短时,那么等待线程阻塞、挂起、唤醒的代价是比较大的。所以有一种锁在申请临界区的时候,可以不阻塞等待,它会持续自旋(即在一个循环中不断检查锁是否可用),这种状态的锁我们称之为
自旋锁
。
这种机制减少了线程切换的开销, 适用于短时间内锁的竞争情况。
3.2 原理
自旋锁通常使用一个共享的标志位(如一个布尔值)来表示锁的状态。 当标志位为true 时,表示锁已被某个线程占用;当标志位为 false 时,表示锁可用。 当一个线程尝试获取自旋锁时,它会不断检查标志位:
- 如果标志位为 false,表示锁可用, 线程将设置标志位为 true, 表示自己占用了锁, 并进入临界区。
- 如果标志位为 true(即锁已被其他线程占用),线程会在一个循环中不断自旋等待, 直到锁被释放
上面检测标志位的操作一定是原子性的。
伪代码理解原理:
3.3 自旋锁的使用
Linux 提供的自旋锁系统调用
- 初始化:
int pthread_spin_init(pthread_spinlock_t *lock, int pshared);
- 销毁:
int pthread_spin_destroy(pthread_spinlock_t *lock);
- 加锁
int pthread_spin_lock(pthread_spinlock_t *lock);``````int pthread_spin_trylock(pthread_spinlock_t *lock);
申请锁失败就返回,就可以使用适当的退避策略了。 - 解锁:
int pthread_spin_unlock(pthread_spinlock_t *lock);
使用:
#include<iostream>#include<unistd.h>#include<pthread.h>
pthread_spinlock_t spinlock;//自旋锁staticint g_ticket =5000;void*func(void* args){char* name =(char*)args;while(true){pthread_spin_lock(&spinlock);if(g_ticket >0){usleep(1000);printf("%s sells ticket:%d\n", name, g_ticket);
g_ticket--;pthread_spin_unlock(&spinlock);}else{pthread_spin_unlock(&spinlock);break;}}returnnullptr;}intmain(){pthread_spin_init(&spinlock,PTHREAD_PROCESS_PRIVATE);//初始化
pthread_t t1,t2,t3,t4,t5;pthread_create(&t1,nullptr,func,(void*)"thread-1");pthread_create(&t2,nullptr,func,(void*)"thread-2");pthread_create(&t3,nullptr,func,(void*)"thread-3");pthread_create(&t4,nullptr,func,(void*)"thread-4");pthread_create(&t5,nullptr,func,(void*)"thread-5");pthread_join(t1,nullptr);pthread_join(t2,nullptr);pthread_join(t3,nullptr);pthread_join(t4,nullptr);pthread_join(t5,nullptr);pthread_spin_destroy(&spinlock);//销毁return0;}
3.4 优点与缺点
优点
- 低延迟: 自旋锁适用于短时间内的锁竞争情况, 因为它不会让线程进入休眠状态, 从而避免了线程切换的开销, 提高了锁操作的效率。
- 减少系统调度开销: 等待锁的线程不会被阻塞, 不需要上下文切换, 从而减少了系统调度的开销。
缺点
- CPU 资源浪费: 如果锁的持有时间较长,等待获取锁的线程会一直循环等待,导致 CPU 资源的浪费。
- 可能引起活锁: 当多个线程同时自旋等待同一个锁时, 如果没有适当的退避策略, 可能会导致所有线程都在不断检查锁状态而无法进入临界区, 形成活锁。
使用场景
- 短暂等待的情况: 适用于锁被占用时间很短的场景, 如多线程对共享数据进行简单的读写操作。
- 多线程锁使用: 通常用于系统底层, 同步多个 CPU 对共享资源的访问。
版权归原作者 戴墨镜的恐龙 所有, 如有侵权,请联系我们删除。