Linux多线程服务端编程笔记,Linux多线程服务端编程笔记 第二章

本文解决如下问题:

使用锁时要注意哪些问题。

死锁常见的两个例子以及如何避免死锁的两个简单方法。

条件变量的使用注意问题。

单例模式的问题与写法。

条件变量与锁的使用场景;

条件变量中的虚假唤醒原理是什么?

如何避免把类当做函数调用这种问题?

如何减少锁争用?(锁的延迟的主要占用点)

1. 使用锁时要注意哪些问题。

不直接使用std::mutex的lock和unlock函数,一切交给unique_lock等封装对象来完成;

使用unique_lock对象时,要考虑当前调用栈上是否已经持有了这个锁,避免死锁。

不使用跨进程的mutex,进程间通信只使用TCP sockets;

加锁和解锁必须在同一线程;(RAII自动保证)

不使用递归锁和读写锁、信号量。

对第五点的解释:读写锁在读优先的情况下会阻塞写操作,甚至会造成饥饿现象;写优先的情况下,会阻塞读,对读效率很敏感的程序来说很不友好。而普通锁在设计良好的情况下,临界区很小,效率是很高的,没有这些问题。

信号量的实现完全可以通过互斥器与条件变量来实现。另外信号量有自己的计数值,通常我们的数据结构也会保存一个计数值,两个计数值需要保持一致,增加了出错的可能。

2. 死锁常见的两个例子。

1. 同一线程发生死锁

同一个类中有锁的一个函数辗转调用了另一个有锁的函数。func1和func2调用时都持有锁,然后func2调用了func3,func3反过来调用了func1。调用关系如图1所示,此时存在死锁现象。

06f03bd19826?utm_campaign=maleskine&utm_content=note&utm_medium=seo_notes&utm_source=recommendation

图1

func2直接调用func1,没有func3也会发生死锁。

2. 两个线程发生死锁

两个类中持有锁的两对函数相互调用。类1中包含func1,func2;类2中包含func3,func4,然后func1调用func3,func4调用func2。(反向相互调用)如图2所示。

06f03bd19826?utm_campaign=maleskine&utm_content=note&utm_medium=seo_notes&utm_source=recommendation

图2

func1和func2可以重叠;func3和func4可以重叠。

3. 死锁的检测与预防

预防:

严格控制锁的调用顺序,用锁之前需要想调用栈上都有了哪些锁;

将锁内调用的函数同时定义一个无锁版,锁内调用那个无锁版;

检测:死锁之后,打开core文件或者使用如下gdb命令查看:

thread apply all bt

看到线程阻塞在一个锁上,或者某个非条件变量或者epoll_wait函数上,就是发生了死锁。

3. 条件变量的使用注意问题。

条件变量一般用于等待某个条件成立,即等待某个bool表达式为真。

wait端

(1)必须与mutex一起使用;wait函数和bool表达式的判断需要在mutex的保护下;

(2)把bool表达式的判断和wait放到while循环中。

std::unique_lock<:mutex> lock(mtx);

while(queue.empty()) {

cond.wait();

}

//在锁的保护下,从queue中获取变量。

signal端

(1)notify、notifyAll函数不一定在已上锁的情况下调用;

(2)调用notify之前一般要修改bool表达式;(修改bool表达式要有锁保护)

std::unique_lock<:mutex> lock(mtx);

queue.push_back(x);

cond.notify();

4. 单例模式的问题与写法。

1. double check locking(DCL)实现单例模式的问题

DCL实现方法:

if(instance == null) {

lock();

if(instance == null) {

instance = new Instance();

}

}

在对instance的初始化的时候,编译器会按如下步骤:

(1)分配内存空间;

(2)初始化内存;

(3)将内存的地址赋值给instance。

编译器对(2)(3)步没有顺序限制。结果就是如果先(3),就会instance没有初始化就有值了,外部线程到了第一个if,检测有值,就去使用了,发生了未定义行为。

2. 使用static来完成单例模式

class SingleInstance {

private:

public:

get() {

static instance;

return instance;

}

}

函数内的static对象是懒惰初始化,什么时候用什么时候初始化。

类静态变量的初始化则是饿汉初始化,即使不用类对象,也会在main函数开始执行之前进行初始化,不好。

除了使用函数内static初始化,还可以使用std::thread_once来保证只调用了一次。

5. 条件变量与锁的使用场景

锁是为了访问共享数据;

条件变量是为了等待事件发生。等待事件发生严禁使用sleep函数。

条件变量的notify通常代表资源可用(生产者模式);notifyall通常代表状态变化。(比如倒计时系统,可以开始做事了的那种)

这两种用法都很常见。所以都要会。

6. 条件变量中的虚假唤醒原理是什么?

1. 基本原因

虚假唤醒是因为条件变量的wait函数调用的是futex系统调用,这个系统调用在阻塞状态下,被信号打断的时候会返回-1。即线程被唤醒了。

2. 验证

将一个线程阻塞在cond.wait()函数上,然后调用kill向这个线程发送信号,然后就会发现阻塞的线程被唤醒了。

3. 解决

虚假唤醒代表的是等待条件变量唤醒的线程可能不是因为bool表达式为正而唤醒,所以需要将wait放到一个循环中,而不是一个if中。

C++11提供的条件变量的wait函数可以使用第二个参数来实现自动while,这样就不用放到while循环了。

7. 如何避免把类当做函数调用这种问题?

比如定义了GuardLock类来封装std::mutex的使用。但是使用时不小心这样使用了:

GuardLock(std::mutex);

问题:

定义了一个临时变量,紧接着就析构掉了。锁没有加上。

解决:

#define GuardLock(x) static_assert("false", "hehe");

编译期找到这个错误。

8. 如何减少锁争用?(锁的延迟的主要占用点)

真正影响性能的不是锁,而是锁争用。

上一小节,有介绍如何减少锁争用:

对于拷贝代价比较小的共享变量来说:

读操作,临界区内拷贝出来,临界区外使用副本读取;

写操作,临界区外定义副本,完成要完成的操作,临界区内直接赋值或者swap;

读和写都使用副本,而不是使用共享变量,这样就不会出现竞争。当然,如果共享变量是指针,要拷贝的是指针指向的值。

问题:

多个进程的写操作还是会相互覆盖,且临界区定义副本,这个副本的初始值无法界定。所以这种方法不宜使用。(有缺点就不宜使用)

将无关操作移出临界区。

写操作时析构移除临界区:一般指的是shared_ptr在临界区内析构的问题

为了防止其在临界区内析构,我们可以使用栈上的临时变量来增加引用计数,然后在临界区外析构临时变量。

避免临界区内的循环。

避免出现一边遍历一边决定是否对共享变量进行更改的情况;应该在临界区外遍历,然后记住该改哪些元素,最后在临界区内统一修改。

shared_ptr来实现copy_on_write

有几个条件:

(1)共享变量使用shared_ptr来管理。使用shared_ptr来管理其实和指针是一样的;

(2)读和写都要获取到锁,同一时间只能有一个线程拥有锁。copy_on_write的关键在于减少锁争用。

(3)读操作,放心读,抢到锁就读,但是需要在读之前定义一个临时变量,来增加引用计数;

(4)写操作,无法放心写,抢到锁之后,得先判断引用计数是否有别的线程在读,如果没有就可以直接写,如果有,为了减少写等待,需要深拷贝动作。

如图3所示:

06f03bd19826?utm_campaign=maleskine&utm_content=note&utm_medium=seo_notes&utm_source=recommendation

屏幕快照 2018-09-13 下午11.57.41.png

读操作:

(1)抢到锁之后,先定义出来ptr1的副本ptr2,但是都是指向的共享变量,然后就可以退出临界区了。

(2)临界区外使用ptr2来访问临界区。因为ptr1可能被写。

(3)每个读都会创建一个ptr2副本,所以可能同一时间ptr的副本有超多个。

(4)可以在临界区外访问共享变量是因为只要有读,写不会改变这个共享变量,只会改变共享变量的副本。

写操作:

(1)抢到锁之后,判断有没有读(根据ptr的引用计数值是否为1);

(2)没有读的话,可以直接修改共享变量;

(3)有读的话,二话不说,把共享内存拷贝出来一份,然后在拷贝出来的内存中进行修改,最后把ptr3赋值给ptr。

(4)所有的写操作都要在临界区内部。

(5)如果这里的共享变量仍然是一个指针,拷贝共享变量使用深拷贝就可以。

问题:

由于存在拷贝操作,共享变量无法太大。

共享变量太大也可以,不要频繁写就可以。(比如一段时间更新一次数据)

如果 单线程写 && 写时获取到的是全部数据,(不是共享变量的一部分),则退化到第一点,无需拷贝,直接临界区内swap就可以了。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值