互斥
在单核 CPU 系统里,为了实现多个程序同时运行的假象,操作系统通常以时间片调度的方式让每个进程执行每次执行一个时间片,时间片用完了,就切换下一个进程运行,由于这个时间片的时间很短,于是就造成了「并发」的现象。线程是调度的基本单位,进程则是资源分配的基本单位。
在多线程运行时必然产生竞争与协作,互斥与同步的问题,需要使用锁给予管控
#include <iostream>
#include <thread>
static int num = 0;
void test() {
for (int i = 0; i < 100000; i++) num++;
}
int main() {
std::thread t1(test);
std::thread t2(test);
t1.join();
t2.join();
std::cout << num << std::endl;
return 0;
}
// 146180
对共享变量的同时写操作一定会产生并发问题
上面展示的情况称为竞争条件(race condition) ,当多线程相互竞争操作共享变量时,在执行过程中发生了上下文切换,得到了错误的结果,事实上,每次运行都可能得到不同的结果,因此输出的结果存在不确定性(indeterminate) 。
由于多线程执行操作共享变量的这段代码可能会导致竞争状态,因此将此段代码称为临界区(critical section),它是访问共享资源的代码片段,一定不能给多线程同时执行。
我们希望这段代码是互斥(mutualexclusion)的,也就说保证一个线程在临界区执行时,其他线程应该被阻止进入临界区,也就是这段代码执行过程中,最多只能出现一个线程。
同步
互斥解决了并发进程/线程对临界区的使用问题。这种基于临界区控制的交互作用是比较简单的,只要一个进程/线程进入了临界区,其他试图想进入临界区的进程/线程都会被阻塞着,直到第一个进程/线程离开了临界区。
在多线程里,每个线程并不一定是顺序执行的,它们基本是以各自独立的、不可预知的速度向前推进,但有时候又希望多个线程能密切合作,以实现一个共同的任务。
所谓同步,就是并发进程/线程在一些关键点上可能需要互相等待与互通消息,这种相互制约的等待与互通信息称为进程/线程同步。同步与互斥是两种不同的概念:
- 同步:「操作 A 应在操作 B 之前执行」,「操作 C 必须在操作 A 和操作 B 都完成之后才能执行」等;
- 互斥:「操作 A 和操作 B 不能在同一时刻执行」;
为了实现进程/线程间正确的协作,操作系统必须提供实现进程协作的措施和方法,主要方法有两种:
- 锁:加锁、解锁操作;
- 信号量:P、V 操作;
信号量比锁的功能更强一些,它还可以方便地实现进程/线程同步。
锁
使用加锁操作和解锁操作可以解决并发线程/进程的互斥问题。任何想进入临界区的线程,必须先执行加锁操作。若加锁操作顺利通过,则线程可进入临界区;在完成对临界资源的访问后再执行解锁操作,以释放该临界资源。
死锁
当两个线程为了保护两个不同的共享资源而使用了两个互斥锁,那么这两个互斥锁应用不当的时候,可能会造成两个线程都在等待对方释放锁,在没有外力的作用下,这些线程会一直相互等待,就没办法继续运行,这种情况就是发生了死锁。
死锁只有同时满足以下四个条件才会发生:互斥条件;持有并等待条件;不可剥夺条件;环路等待条件;
1) 互斥条件
互斥条件是指多个线程不能同时使用同一个资源。如果线程 A 已经持有的资源,不能再同时被线程 B 持有,如果线程 B 请求获取线程 A 已经占用的资源,那线程 B 只能等待,直到线程 A 释放了资源。
2)持有并等待条件
持有并等待条件是指,当线程 A 已经持有了资源 1,又想申请资源 2,而资源 2 已经被线程 C 持有了,所以线程 A 就会处于等待状态,但是线程 A 在等待资源 2 的同时并不会释放自己已经持有的资源 1。
3 )不可剥夺条件
不可剥夺条件是指,当线程已经持有了资源 ,在自己使用完之前不能被其他线程获取,线程 B 如果也想使用此资源,则只能在线程 A 使用完并释放后才能获取。
4) 环路等待条件
环路等待条件指的是,在死锁发生的时候,两个线程获取资源的顺序构成了环形链。如,线程 A 已经持有资源 2,而想请求资源 1, 线程 B 已经获取了资源 1,而想请求资源 2,这就形成资源请求等待的环形图。
最简单的死锁问题就是线程A先后获取互斥锁C,D;线程B先后获取互斥锁D,C,在执行期间发生死锁
#include <thread>
#include <mutex>
#include <cstdio>
std::mutex lock_a;
std::mutex lock_b;
void f1() {
lock_a.lock();
printf("f1获取锁A\n");
std::this_thread::sleep_for(std::chrono::seconds(1));
lock_b.lock();
printf("f1获取锁B\n");
std::this_thread::sleep_for(std::chrono::seconds(1));
lock_b.unlock();
lock_a.unlock();
}
void f2() {
lock_b.lock();
printf("f2获取锁B\n");
std::this_thread::sleep_for(std::chrono::seconds(1));
lock_a.lock();
printf("f2获取锁A\n");
std::this_thread::sleep_for(std::chrono::seconds(1));
lock_a.unlock();
lock_b.unlock();
}
int main(int argc, char *argv[]) {
std::thread t1(f1);
std::thread t2(f2);
t1.join();
t2.join();
return 0;
}
/**
f1获取锁A
f2获取锁B
*/
避免死锁
产生死锁的四个必要条件是:互斥条件、持有并等待条件、不可剥夺条件、环路等待条件。
那么避免死锁问题就只需要破环其中一个条件就可以,最常见的并且可行的就是使用资源有序分配法,来破环环路等待条件。
线程 A 和 线程 B 获取资源的顺序要一样,当线程 A 是先尝试获取资源 A,然后尝试获取资源 B 的时候,线程 B 同样也是先尝试获取资源 A,然后尝试获取资源 B。也就是说,线程 A 和 线程 B 总是以相同的顺序申请自己想要的资源。
Reference
转载自小林coding:xiaolincoding.com/os/4_proces…