1.什么是线程互斥,为什么需要线程互斥
线程互斥是指多个线程对临界资源进行争抢访问时有可能会造成数据二义,因此通过保证同一时间只有一个线程能够访问临界资源的方式实现线程对临界资源的访问安全性
2.mutex简单理解就是一个0/1的计数器,用于标记资源访问状态:
0表示已经有执行流加锁成功,资源处于不可访问,
1表示未加锁,资源可访问。
3.可用来实现线程间通知和唤醒的方式:
条件变量,信号量
4.信号量与条件变量的区别
信号量既可以实现同步又可以实现互斥
条件变量需要搭配互斥锁使用,信号量不用
条件变量提供了一个pcb阻塞队列以及阻塞和唤醒线程的接口用于实现同步,但是什么时候该唤醒以及什么时候该阻塞线程由程序员进行控制,而这个控制通常需要一个共享资源的条件判断完成,因此条件变量还需要搭配互斥锁使用,来保护这个共享资源的条件判断及操作。
信号量提供一个pcb等待队列,以及一个实现了原子操作的对资源进行计数的计数器,通过自身计数器实现同步的条件判断,因此不需要搭配互斥锁使用,而且信号量在初始化计数为1的情况下也可以模拟实现互斥操作。
5.条件变量在等待被唤醒时需要重新对条件进行判断,是否条件满足
条件变量的控制判断需要使用循环进行,避免在多个线程同时被唤醒的情况下,A线程加锁成功访问资源,其他线程卡在锁处,而A线程一旦解锁,其他线程抢到锁在资源访问条件不满足的情况下访问资源。因此被唤醒后加锁成功则需要重新进行判断,条件满足则访问,不满足则需要重新陷入休眠。
6.同步,竞态条件,线程同步
在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效地避免饥饿问题,叫做同步
因为时序问题,而导致程序异常,我们称之为竞态条件
线程同步指的是线程间对数据资源进行获取,有可能在不满足访问资源条件的情况下访问资源而造成程序逻辑混乱,因此通过进行条件判断来决定线程在不能访问资源时休眠等待或满足资源后唤醒等待的线程的方式实现对资源访问的合理性
7.线程安全的概念和实现
线程安全指的是在多线程编程中,多个线程对临界资源进行争抢访问而不会造成数据二义或程序逻辑混乱的情况。(多个线程并发同一段代码的时候,不会出现不同的结果)
线程安全的实现,通过同步与互斥实现
具体同步的实现可以通过互斥锁和信号量实现、而同步可以通过条件变量与信号量实现。
线程安全指的是当前线程中对各项操作时安全的,但不表示内部调用的函数是安全的,两个之间并没有必然关系
8.可重入,可重入与线程安全的区别
重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入
一个函数在重入的情况下,运行结果不会出现任何不同或任何问题,则该函数被称为可重入函数,否则为不可重入函数
可重入函数是线程安全函数的一种
线程安全不一定是可重入的,可重入函数一定是线程安全的
如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生死锁,因此是不可重入的
线程中不仅仅会调用函数,有可能本身内部就进行了临界资源的操作,所以线程内调用的函数可重入只是线程安全的一个要素
9.力扣1114. 按序打印
法一:信号量
class Foo{
private:
sem_t sem_1,sem_2;
public:
Foo(){
sem_init(&sem_1,0,0);
sem_init(&sem_2,0,0);
}
void first(function<void()>printFirst){
printFirst();
sem_post(&sem_1); //即V操作,通过 sem_post(&sem_1) 增加 sem_1 信号量的值,以唤醒等待在 sem_1 上的线程
}
void second(function<void()>printSecond){
sem_wait(&sem_1);
//sem_wait 函数的作用是阻塞当前线程(或进程),直到指定的信号量的值大于0。一旦信号量的值大于0
//sem_wait 函数将减少信号量的值,并立即返回。如果信号量的值已经为0,则调用线程将阻塞,直到信号量的值大于0。
printSecond();
sem_post(&sem_2);//用于等待 sem_1 信号量的值变为非零,然后打印第二个字符串,并通过 sem_post(&sem_2) 增加 sem_2 信号量的值,以唤醒等待在 sem_2 上的线程。
}
void third(function<void()>printThird){
sem_wait(&sem_2);
printThird();
}
};
sem_wait函数:
用来等待信号量的值变为非零
int sem_wait(sem_t *sem);
函数参数 sem 是一个指向要等待的信号量的指针。
sem_wait 函数的作用是阻塞当前线程(或进程),直到指定的信号量的值大于0。一旦信号量的值大于0,sem_wait 函数将减少信号量的值,并立即返回。如果信号量的值已经为0,则调用线程将阻塞,直到信号量的值大于0。
sem_wait 函数成功返回0,失败返回-1,并设置 errno 表示错误原因。
sem_post函数:
用来增加信号量的值
int sem_post(sem_t *sem);
函数参数 sem 是一个指向要增加值的信号量的指针。
sem_post 函数的作用是增加指定信号量的值,并唤醒等待该信号量的线程或进程。一旦信号量的值增加,等待该信号量的线程或进程将被唤醒,可以继续执行。如果有多个线程或进程等待该信号量,则只会唤醒其中一个。
sem_post 函数成功返回0,失败返回-1,并设置 errno 表示错误原因。
sem_init函数:
sem_init 函数是用于初始化 POSIX 信号量的函数
int sem_init(sem_t *sem, int pshared, unsigned int value);
sem:一个指向要初始化的信号量的指针。
pshared:指定信号量的类型。如果为0,则信号量将被进程内的线程共享;如果为非0,则信号量可以在多个进程之间共享。在大多数情况下,可以将此参数设置为0,表示信号量在进程内的线程之间共享。
value:指定信号量的初始值。
class Foo{
condition_variable cv;
mutex mtx;
int k=0;
public:
void first(function<void()>printFirst){
printFirst();
k=1;
cv.notify_all();
}
void second(function<void()>printSecond){
unique_lock<mutex>lock(mtx);
cv.wait(lock,[this](){return k==1;}); //unlock mtx,并阻塞等待唤醒通知,需要满足 k == 1 才能继续运行
printSecond();
printSecond();
k=2;
cv.notify_one(); // 随机通知一个(unspecified)在等待唤醒队列中的线程
}
void third(function<void()>printThird){
unique_lock<mutex>lock(mtx);
cv.wait(lock,[this](){return k==2}); // unlock mtx,并阻塞等待唤醒通知,需要满足 k == 2 才能继续运行
printThird();
}
};
wait函数
void wait(std::unique_lockstd::mutex& lock, Predicate pred);
lock:一个已经锁定了相关互斥锁的 std::unique_lock 对象。调用 wait 时,互斥锁会被释放,并且线程会被阻塞,直到满足条件。
pred:一个可调用对象(例如函数、lambda 表达式等),用于检查条件是否满足。如果条件满足,则 wait 函数立即返回;否则,当前线程将被阻塞,直到条件满足或者超时。
在调用 wait 函数时,会执行以下步骤:
wait 函数首先释放 lock 所持有的互斥锁,使得其他线程可以获取锁。
它将当前线程阻塞,直到满足了条件(即 pred 返回 true)。
一旦条件满足,线程将重新获取 lock 所持有的互斥锁,并继续执行后续代码。
10.力扣1117. H2O 生成
class H2O {
public:
condition_variable cv;
mutex m;
int h,o;
H2O() {
h=0;
o=0;
}
inline void check(){
if(h>=2&&o>=1){
h-=2;
o-=1;
cv.notify_all();
}
}
void hydrogen(function<void()> releaseHydrogen) {
unique_lock<mutex>mtx(m);
cv.wait(mtx,[this](){return h<2;});
releaseHydrogen();
h++;
check();
}
void oxygen(function<void()> releaseOxygen) {
unique_lock<mutex>mtx(m);
cv.wait(mtx,[this](){return o<1;});
releaseOxygen();
o++;
check();
}
};