同步原语问题汇总--芒果TV进击面试

前言

芒果TV中台部门位于长沙风水宝地马栏山,有同学面试给到了17k*18(四年小厂工作经验),也算长沙大厂了。线程同步是芒果tv研发岗一面必问的问题。

262f2b1e48282dd49d16bc2616d61e43.png

线程同步

多线程访问共享数据时,可能发生冲突。因此需要通过特定的方式来控制线程之间执行的顺序。举个例子,两个线程操作同一个变量,分为三个步骤:

从内存读变量到寄存器、寄存器中赋值、将寄存器值写回内存。

产生冲突的原因就在于这三个步骤不是一个原子操作,中间某一过程被中断,切换线程再对同一变量或共享资源操作,就会出现问题。

linux系统下有四种基本的线程同步方式:互斥锁、读写锁、条件变量、信号量。

互斥锁

采用加锁方法来控制对共享资源的原子操作,同一时刻只允许一个线程执行

读写锁

用于读多写少的场景,可以参考阿姨打扫男厕所时的场景

条件变量

事件通知机制,需要和互斥锁配合使用

信号量

一个可计数变量,既能用于互斥又能用于同步

几种同步方式的特点

1、互斥锁是为了上锁而设计的,条件变量是为了等待而设计的,信号量可用于上锁互斥,也可用于等待同步。

2、互斥锁必须由给它上锁的线程(持有者)解锁,信号量的释放不必由执行过它的等待操作的同一线程执行。一个线程可以等待某个给定信号量,而另一个线程可以释放该信号量。

3、实际应用通常采用互斥量+条件变量的方式,是因为其加锁的位置和等待的位置可以是两个位置,但使用信号量就不行。

4、信号量等待和释放的可能不是一个线程,可能导致更多的开销和更高的复杂性,编码不当会出现死锁问题,通常需要实现优先级继承和优先级反转。

条件变量中的唤醒丢失问题

下面是知乎的一个例子

Thread A 使用了互斥锁对操作条件变量的代码区域(临界区,包括2-4共3行代码):

1: pthread_mutex_lock(&mutex);
2: while (false == ready) {
3: pthread_cond_wait(&cond, &mutex);
4: }
5: pthread_mutex_unlock(&mutex);

Thread B 没有使用互斥锁对操作条件变量的代码区域(临界区,下面两行都属于临界区)进行保护。

1: ready = true;
2: pthread_cond_signal(&cond);

执行序列

Thread A

Thread B

1

1: pthread_mutex_lock(&mutex);


2

2: while (false == ready) {


3


1: ready = true;

4


2: pthread_cond_signal(&cond);

5

3:  pthread_cond_wait(&cond, &mutex);


6

4: }


按上面的执行序列,条件变量唤醒 signal 先于 wait,那么相当于 Thread A 还没有被加入唤醒队列(thread A进入pthread_cond_wait()之后,才会加入到待唤醒队列中),这个时候,你已经 signal 唤醒了,那么这次唤醒自然就丢失了(后续如果还调用了唤醒,则能唤醒thread A了,如果不再调用pthread_cond_signal()则thread A就不会再被唤醒,只能永远休眠),执行序列的第 5 行,也就是 Thread 的第 3 行pthread_cond_wait(&cond, &mutex) 就会一直等在那里了。

这里需要保证 ready == false 判断成立和 Thread A 调用 wait 进入唤醒队列的原子性

Thread b需要改写:

1: pthread_mutex_lock(&mutex);
2: ready = true;
3: pthread_mutex_unlock(&mutex);
4: pthread_cond_signal(&cond);

Thread b中2行与3行交换也是可以的:

2: ready = true;
3: pthread_cond_signal(&cond);

现在有一种wait morphing优化,可以减少唤醒-睡眠等待mutex-唤醒的上下文切换。

解锁互斥量mutex和发出唤醒信号condition_signal是两个单独的操作,那么就存在一个顺序的问题。谁先随后可能会产生不同的结果。如下:

(1) 按照 unlock(mutex); condition_signal()顺序, 当等待的线程被唤醒时,因为mutex已经解锁,因此被唤醒的线程很容易就锁住了mutex然后从conditon_wait()中返回了。

(2) 按照 condition_signal(); unlock(mutext)顺序,当等待线程被唤醒时,它试图锁住mutex,但是如果此时mutex还未解锁,则线程又进入睡眠,mutex成功解锁后,此线程在再次被唤醒并锁住mutex,从而从condition_wait()中返回。这种现象称为惊群效应。  

按照(2)的顺序,对等待线程可能会发生2次的上下文切换,严重影响性能。因此在后来的实现中,对(2)的情况,如果线程被唤醒但是不能锁住mutex,则线程被转移(morphing)到互斥锁mutex的等待队列中而不是转移到可调度队列争抢cpu时间片,避免了上下文的切换造成的开销。 

虚假唤醒(spurious wakeup)

pthread_cond_wait为什么需要使用while循环检查?

因为在多核处理器下,pthread_cond_signal可能会激活多于一个线程(阻塞在条件变量上的线程)。结果是,当一个线程调用pthread_cond_signal()后,多个调用pthread_cond_wait()或pthread_cond_timedwait()的线程返回。

pthread_mutex_lock(&mtx);
while (msg_list.empty()) { //用while而不是if
  pthread_cond_wait(&cond, &mtx);
}
msg = msg_list.pop();
pthread_mutex_unlock(&mtx);

调用pthread_cond_broadcast会唤醒所有等待线程,所以也会出现虚拟唤醒情况。

惊群效应

前面讲过的无效唤醒的现象就是惊群效应,就是多进程(多线程)在同时阻塞等待同一个事件的时候(休眠状态),如果等待的这个事件发生,那么他就会唤醒等待的所有进程(或者线程),但是最终却只可能有一个进程(线程)获得这个时间的“控制权”,对该事件进行处理,而其他进程(线程)获取“控制权”失败,只能重新进入休眠状态,造成性能浪费。

具体的惊群现象有具体的解决办法。比如web服务器里面,主线程创建listenfd之后,各个子线程创建并管理各自的epollfd。如果子线程把listenfd加入自己的epollfd,那么新连接到来时就会产生惊群。合理的做法是采用reactor模式,由主线程负责accept(或者放入某一个epollfd),将新连接分发给负载最低的子线程,然后子线程再把connfd加入到epoll中。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值