c++11中的多线程编程

我觉得c++ 11标准库中最重要的新特性就是多线程支持。为了更安全更有效率的编写c++多线程代码,自己写篇博客总结一下。

thread

以上仅仅几行代码就可以创建一个线程,运行线程函数中的逻辑。thread的join方法就是同步的等待线程执行完成。

如果我们在代码块里面创建thread对象,如果不使用join同步等待,那么thread对象超出作用域销毁,这将导致thread对象对应的尚未执行完的线程出错,比如这样:

如果我们不关心线程对象的执行,我们可以使用detach来分离thread对象和线程。这样thread对象的生命周期就跟线程无关了。

传递给thread对象执行的代码不仅可以是函数,还可以是仿函数,lambda表达式,std::bind
比如:

c++ 11还新增了一个非常好的特性,可变参数模板。thread的构造函数就是这样的实现的,因此我们可以传递给线程函数任意数量和类型的参数,只要跟线程函数的参数对应起来就行。

我们还可以给线程函数传递指针,获得执行的结果。

但是我们不能直接传递参数的引用,需要使用std::ref包装一下。

事实上thread的线程执行函数是可以有返回值的,但是其返回值会被忽略。

线程对象还可以赋值,传递,其内部是实现了move语义。

此外,thread头文件中this_thread中还提供了几个帮助函数。

  • get_id:获取线程ID
  • yield:线程一直在空转获取到的执行时间片
  • sleep_until:线程睡眠到某个时间点唤醒
  • sleep_for线程睡眠到某个时间片唤醒

atomic

单线程的代码指令都是串行执行的,但是多线程里面线程的切换是不可预知的,一行代码生成了多条指令,这样就无法保证代码的正确结果,比如下面这个最简单整数自增操作代码:

结果是没有做原子保护的g_int最终的结果是错误的。标准库中提供一个非常简单有用的类atomic,它可以保证被它保证的数据修改是原子性的。

如果只是简单的想在线程间进行数据同步的话,原子类型已经足够了。不过正确的结果是建立在一个假设之上:顺序一致性的内存模型,比如下面的例子:

因为无法保证线程t2和t1之间的执行次序,这样的输出结果可能有3中:

  • a = 0   b = 0
  • a = 1   b = 0
  • a = 1   b = 2

我们改进一下程序:

ShowValue线程一开始总在自旋等待SetValue线程b被正确赋值。这样以为就可以打印出期待的数值a = 1  b = 2。但是还是有可能打印出a = 0 b = 2的结果。因为你是原子类型,编译器(处理器)会做优化乱序执行,改变SetValue里面设置a,b值的代码的执行顺序。

默认情况下,原子类型的变量在线程中总是保持着顺序执行的特性。而顺序一致的模型在多线程里面往往意味着效率最低的模型。有时候乱序执行不影响程序最终的结果,还能提高程序的执行效率。到底如何取舍,c++ 11中给出的解决方式是程序员可以手动指定内存顺序。

mutex

除了atomic,c++标准库中还提供了一个更强大的线程同步对象mutex,它可以保证临界区代码的独占访问。

但是手动的对mutex加锁和解锁是不安全的,比如加锁之后忘记了调用解锁或者代码提前返回了执行不到解锁逻辑。于是还有个lock_guard对象帮助我们在对象作用域自动加锁和解锁,此外还有unique_lock  ,shared_lock,有点像智能指针。

有了锁,也就意味着可能发生死锁:

当我们调用fun2,程序运行就会抛出异常。原因是fun2自动对g_mutex加了锁期间又调用fun1,fun1也对g_mutex加锁,形成了死锁。

解决办法是使用recursive_mutex。它运行同一个线程对一个mutex多次加锁和解锁。

对于多个mutex对象的加锁管理,也是很容易发生死锁的。

以上代码会发生thread1拿到了mt1,thread2拿到了mt2,从而发生了死锁。
我们可以把不同线程获取锁的顺序保持一致就可以解决这个问题,如下:

更好的做法是使用标准库中的std::lock和std::try_lock函数来对多个Lockable对象加锁。std::lock(或std::try_lock)会使用一种避免死锁的算法对多个待加锁对象进行lock操作(std::try_lock进行try_lock操作),当待加锁的对象中有不可用对象时std::lock会阻塞当前线程知道所有对象都可用(std::try_lock不会阻塞线程当有对象不可用时会释放已经加锁的其他对象并立即返回)。

future

thread线程函数执行完是忽略返回值的。但是我们可以使用future对象异步的执行函数并返回结果。

以上代码是判断一个数是否为质数,异步的执行is_prime函数,最后获得is_prime执行的结果。

我们还可以通过promise对象给线程函数异步的传递数据。

线程等待prom值之后才会继续运行。

更复杂的功能是packaged_task,future,thread三者配合起来一起使用。

packaged_task把一个函数打包成task,从这个task中获取一个future,然后task传递给thread执行,future再获取task执行完成的结果。

condition_variable

这个condition_variable需要配合着unique_lock来使用。线程用condition_variable wait去等待别的线程唤醒它。

当我们输入go的时候,等待中的10个线程收到通知,就会继续运行。

总结

这篇博客从准备到总结成文字,经历三个多星期,到最后发现自己还是没有把握说明白,说清楚。倒不如把自己知道的先总结出来,以后再慢慢完善吧。

发表评论

电子邮件地址不会被公开。 必填项已用*标注