我在之前一篇博文《漫谈C++11 Thread库之使写多线程程序中,着重介绍了<thread>头文件中的std::thread类以及其上的一些基本操作,至此我们动手写多线程程序已经基本没有问题了。但是,单线程的那些"坑"我们仍还不知道怎么去避免。

多线程存在的问题

多线程最主要的问题就是共享数据带来的问题。如果共享数据都是只读的,那么没问题,因为只读操作不会影响到数据,更不会涉及对数据的修改,所以所有线程都会获得同样的数据。但是,当一个或多个线程要修改共享数据时,就会产生很多潜在的麻烦。

#include <iostream>
#include <thread> long sum = 0L; void fun()
{
for(int i=1;i<100000;++i)
sum += i;
} int main()
{
std::cout << "Before joining,sun = " << sum << std::endl;
std::thread t1(fun);
std::thread t2(fun);
t1.join();
t2.join();
std::cout << "After joining,sun = " << sum << std::endl;
}

  程序结构很简单,启动两个线程分别对变量sum加上 1-99999。其内存结构大致上是这样的。

c++多线程程序中,每个线程都有一个线程栈,它们相互独立,因此在线程栈中的数据,是不会被其他线程影响到的。但是在内存的数据段中的数据,是可以在全局被访问到的。我们在上面这段代码中定义的sum变量正是位于数据段中。

在目前来看,我们期望最后程序退出的时候,打印出sum是 9999900000。但是结果却不尽人意,我们试着编译运行:

[thread]g++ condition.cpp -omain -std=c++11 -lpthread
[thread]main
Before joining,sun = 0
After joining,sun = 5192258282
[thread]main
Before joining,sun = 0
After joining,sun = 8418413412
[thread]main
Before joining,sun = 0
After joining,sun = 5294478585

  显然结果还是比较意外的,运行了三次,都得到了不同的结果,而且没有一次得到我们的期望值,这下我们精准地踩中了多线程的"坑"。试着多运行几遍,看看会不会出现正确的结果。当然手动运行几遍甚至几十遍,还是可以应付得了的。但是要运行几千遍,手动运行下来估计手就得抽筋了。这样的机械般的操作还是交给shell脚本吧,由于我的机器配置不是很牛×,暂且先1000次看看,shell脚本如下,count.sh:

#!/bin/bash
#result equal with 9999900000
cnt=0
#result more than 9999900000
cnt_more=0
#result less than 9999900000
cnt_less=0
for((i=0;i<1000;++i))
do
var=$(main|tail -1)
var=${var#After joining,sun = }
if(($var == 9999900000))
then
((cnt++))
fi
if(($var > 9999900000))
then
((cnt_more++))
fi
if(($var < 9999900000))
then
((cnt_less++))
fi
done echo "cnt="$cnt
echo "cnt_more="$cnt_more
echo "cnt_less="$cnt_less

  其中变量cnt来统计1000次运行中总共得到过多少次的正确结果,用cnt_more统计偏大的结果,用cnt_less统计偏小的结果。这是该脚本的运行结果:

[thread]count.sh
cnt=315
cnt_more=0
cnt_less=685

  1000次运行中还是有315次得到了正确答案,有685次的结果是偏小的,却没有一次的结果是偏大的!那么问题出在哪里了?试着想象一下这样一个场景:你和朋友合租在一间房子里边,房子里面只有一间厨房,你们共用一个锅。有一天你准备做一道西红柿炒蛋,当你把西红柿放入锅中的时候,你的电话响了,你离开厨房去接电话。而这时候你的室友也要做饭,他要做一道红烧鱼,于是他把洗好的鱼放入了锅中煮,然后也离开了厨房(由于某种原因他不知道锅里还有你的食材,在程序中线程也不会知道其他线程对共享的数据做了什么)。当你回来的时候继续往里边放入鸡蛋,最后你得到的是一盘西红柿炒鸡蛋鱼。而你的室友回来厨房的时候他要的红烧鱼就会不见了。

在上面的例子里,你和室友就代表着thread1和thread2线程,sum变量就是那个锅。多线程中共享数据的问题,就是上面场景中你们共用一口锅造成的问题。

原子操作

要解决上面场景的问题,其中有一中可行的方案就是:你们做菜的步骤很短,短到什么程度呢,短到这个步骤不可被分割。例如你做的这道菜只有一个步骤,就是让食材(对应于下面提到的原子数据类型)碰一下锅(当然现实场景中基本没有这样的菜),这样你们的做菜过程就不会被其他室友打断、干扰,即使你们共同在使用一口锅。

而上面的代码中的 sum += i 在CPU指令的层面上是可以被分割的,我用g++的-S选项生成其汇编的指令看到了一段这样的代码:

movl	$0, -4(%ebp)     // sum = 0
movl $0, -8(%ebp) // i =0
......
movl -8(%ebp), %eax //将i送入寄存器eax
addl %eax, -4(%ebp) //将i的值加上sum的值,将结果保存到 sum中。
movl $0, %eax

  汇编指令还是描述的比较清楚的,可以清楚的看到 sum += i;操作被分割成了两条cpu指令,先是将i的值保存在eax寄存器中,然后将eax的值加上sum的值并保存在sum中。

而在c++中原子操作就是这样的一种『小到不可分割的』操作。要使用原子操作我们需要引用c++11的一个新的头文件<atomic>。在这个头文件中定义了一个类模板struct atomic表示原子数据类型,在GNU的实现(/usr/include/c++/4.8.3/atomic)上如下:

template<typename _Tp>
struct atomic
{
private:
_Tp _M_i;
public:
atomic() noexcept = default;
~atomic() noexcept = default;
atomic(const atomic&) = delete; //删除了拷贝构造
atomic& operator=(const atomic&) = delete;
atomic& operator=(const atomic&) volatile = delete; //删除了 operator=
constexpr atomic(_Tp __i) noexcept : _M_i(__i) { }
operator _Tp() const noexcept
{
return load();
} operator _Tp() const volatile noexcept
{
return load();
} _Tp operator=(_Tp __i) noexcept
{
store(__i);
return __i;
} ...
};

  atomic模板中还实现了操作符的重载(由于篇幅,查看完整的类结构请参阅atomic头文件),因此你可以像使用内置的数据类型那样使用原子数据类型(c++保证这些操作是原子操作)。对应于内置的数据类型,原子数据类型都有一份对应的类型,归纳出来如下:

std::atomic_char std::atomic<char>
std::atomic_schar std::atomic<signed char>
std::atomic_uchar std::atomic<unsigned char>
std::atomic_short std::atomic<short>
std::atomic_ushort std::atomic<unsigned short>
std::atomic_int std::atomic<int>
std::atomic_uint std::atomic<unsigned int>
std::atomic_long std::atomic<long>
std::atomic_ulong std::atomic<unsigned long>
std::atomic_llong std::atomic<long long>
std::atomic_ullong std::atomic<unsigned long long>
 更多的请见:http://en.cppreference.com/w/cpp/atomic/atomic

我们之前的sum变量是long类型的,对应的原子数据类型是std::atomic_long,下面我们就简单的修改一下开篇的代码:

#include <iostream>
#include <thread>
#include <atomic> // modified std::atomic_long sum = {0L};   // modified void fun()
{
for(int i=0;i<100000;++i)
sum += i;
} int main()
{
std::cout << "Before joining,sun = " << sum << std::endl;
std::thread t1(fun);
std::thread t2(fun);
t1.join();
t2.join();
std::cout << "After joining,sun = " << sum << std::endl;
}

  我们只增加了一个<atomic>头文件,并且将 long sum = 0L; 修改成了 std::atomic_long sum {0L}; 注意不要写成『std::atomic_long sum = 0L』的形式,因为long类型是不可以隐式转换为std::atomic_long类型的。

为了证明不是偶然性,我们仍用上面的count.sh这个脚本运行1000次上面的修改过的程序:

[thread]g++ atomic.cpp -o main -std=c++11 -lpthread
[thread]count.sh
cnt=1000
cnt_more=0
cnt_less=0

可以看到原子操作还是有明显的效果的,这1000次的运行我们都得到了正确的结果。事实证明原子操作的确可以作为解决共享数据引起的问题的一种有效的手段。

"自旋锁"——atomic_flag

和其他的原子数据类型(包括atomic_bool)不同的是,他是锁无关(lock-free)的一种类型,即线程对它的访问是不需要加锁的,因此他也没有其他的原子类型的读写操作(load(),store())、运算符操作等。取而代之的是另外两个原子操作的函数test_and_set()clear()。atomic_flag类的结构在GNU上是这样的:

#if __GCC_ATOMIC_TEST_AND_SET_TRUEVAL == 1
typedef bool __atomic_flag_data_type;
#else
typedef unsigned char __atomic_flag_data_type;
#endif struct __atomic_flag_base
{
__atomic_flag_data_type _M_i;
}; struct atomic_flag : public __atomic_flag_base
{
...
bool test_and_set(memory_order __m = memory_order_seq_cst) noexcept; bool test_and_set(memory_order __m = memory_order_seq_cst) volatile noexcept; void clear(memory_order __m = memory_order_seq_cst) noexcept; void clear(memory_order __m = memory_order_seq_cst) volatile noexcept;

...
private:
static constexpr __atomic_flag_data_type
_S_init(bool __i)
{
return __i ? __GCC_ATOMIC_TEST_AND_SET_TRUEVAL : 0;
}
};

atomic_flag::test_and_set()和其名字一样,大致上是这样工作的:首先检查这atomic_flag类中的bool成员_M_i是否被设置成true,如果没有就先设置成true,并返回之前的值(flase),如果atomic_flag中的bool成员已经是true,则直接返回true

相比较而言atomic_flag::clear()更加简单粗暴,它直接将atomic_flag的bool值得标志成员_M_i设置成flase,没有返回值

既然小标题是『自旋锁——atomic_flag』,那么我们看看这把自旋锁(spin lock)是怎么用的:

#include <iostream>
#include <atomic>
#include <unistd.h>
#include <thread> std::atomic_flag lock = ATOMIC_FLAG_INIT; //初始化 void f(int n)
{
while(lock.test_and_set()) //获取锁的状态
std::cout << "Waiting ... " << std::endl;
std::cout << "Thread " << n << " is starting working." << std::endl;
} void g(int n)
{
sleep(3);
std::cout << "Thread " << n << " is going to clear the flag." << std::endl;
lock.clear(); // 解锁
} int main()
{
lock.test_and_set();
std::thread t1(f,1);
std::thread t2(g,2); t1.join();
t2.join();
}

  进入main函数后我们就先设置好atomic_flag,然后启动了两个线程t1和t2,其中t1中我们一直循环获取atomic_flag的状态,知道t2睡眠3秒后,clear()掉lock的锁定状态。其运行结果:

[thread]g++ atomic_flag.cpp -o main -std=c++11 -lpthread
[thread]main
Waiting ...
Waiting ...
Waiting ...
Waiting ...
Waiting ...
// omit lager of "waiting..."
thread 2 is going to clear the flag.
Thread 1 is starting working.

这样的结果正合我们的期望,实际上我们就是通过自旋锁实现了让t1线程一直在等待t2线程。

更进一步地我们还可以通过简单的封装,来实现一把锁。MyLock.h(为了直观我就都写到一个文件中了):

#ifndef __MYLOCK_H_
#define __MYLOCK_H_
#include <iostream>
#include <atomic>
#include <thread> class MyLock
{
private:
std::atomic_flag m_flag;
public:
MyLock();
void lock();
void unlock();
}; MyLock::MyLock()
{
m_flag.clear(); //if not do this,m_flag will be unspecified
} void MyLock::lock()
{
while(m_flag.test_and_set())
;
} void MyLock::unlock()
{
m_flag.clear();
}
#endif

  现在我们就试着使用这把锁,来改写开篇的那个程序:

#include <iostream>
#include <thread>
#include "MyLock.h" //code above MyLock lk; long sum = 0; void add()
{
for(int i=0;i<100000;++i)
{
lk.lock();
sum += i;
lk.unlock();
}
} int main()
{
std::thread t1(add);
std::thread t2(add); t1.join();
t2.join(); std::cout << "sum = " << sum << std::endl;
}

  运行后没有问题,正确打印出结果sum=9999900000。

内存顺序语义

如果你点过开过上边的atomic_flag::test_and_set()的链接,你会发现其实它是有参数的,其原型是这样的:

bool test_and_set(std::memory_order order = std::memory_order_seq_cst) volatile;
bool test_and_set(std::memory_order order = std::memory_order_seq_cst);
void clear( std::memory_order order = std::memory_order_seq_cst ) volatile;
void clear( std::memory_order order = std::memory_order_seq_cst );

这两个函数原型包含了一个新的数据类型std::memory_order,这是一个枚举类型,其具体的定义在<bits/atomic_base.h>头文件中(/usr/include/c++/4.8.3/bits/atomic_base.h)。所有的枚举值得具体意义,我都查阅资料,注释在后边,如下:

typedef enum memory_order
{
memory_order_relaxed,   //不对执行顺序做任何保证
memory_order_consume, //本线程中所有有关本原子类型的操作,必须等到本条原子操作完成之后进行
memory_order_acquire, //本线程中,后续的读操作必须在本条原子操作完成后进行
memory_order_release, // 本线程中,之前的写操作完成后才执行本条原子操作
memory_order_acq_rel, //memory_order_acquire和memory_order_release 效果的合并
memory_order_seq_cst //顺序一致
} memory_order;

test_and_set()和clear()的默认参数都是使用的memory_order_seq_cst这个枚举值,其语义上是顺序一致性(sequential consistent)。顺序一致性是指线程执行的顺序和我们程序员所写代码的顺序是一致的。我们首次接触这个概念的时候,可能会感到疑惑,一直以来我们都理所当然的以为我们写的是什么,程序就怎么干。其实不然。当编译器在编译我们的源码的时候会权衡我们的代码做出适当的优化,如果编译器认为执行顺序和程序输出结果无直接影响,那么就可能会重排序(reorder)指令以提高性能。而memory_order_seq_cst则保证了顺序执行程序。如上边memory_order定义的那样,在C++11,并不是只支持顺序一致的内存模型,因为顺序一致意味着最低效。

关于内存顺序个人以为这和硬件的关系跟大一些,在此不再用过多篇幅讨论。了解一下应该就够了。

最后谢谢你的阅读,如果你能给我一点建议的话,那就更好了。

漫谈C++11 Thread库之原子操作的更多相关文章

  1. 漫谈c++11 Thread库之使写多线程程序

    c++11中最重要的特性之一就是对多线程的支持了,然而<c++ primer>5th却没有这部分内容的介绍,着实人有点遗憾.在网上了解到了一些关于thread库的内容.这是几个比较不错的学 ...

  2. c++11 Thread库写多线程程序

    一个简单的使用线程的Demo c++11提供了一个新的头文件<thread>提供了对线程函数的支持的声明(其他数据保护相关的声明放在其他的头文件中,暂时先从thread头文件入手吧),写一 ...

  3. Boost::thread库的使用

    阅读对象 本文假设读者有几下Skills [1]在C++中至少使用过一种多线程开发库,有Mutex和Lock的概念. [2]熟悉C++开发,在开发工具中,能够编译.设置boost::thread库. ...

  4. C++11 标准库也有坑(time-chrono)

    恰巧今天调试程序遇到时间戳问题, 于是又搜了搜关于取时间戳,以及时间戳转字符串的问题, 因为 time()   只能取到秒(win和linux) 想试试看能不能找到 至少可以取到毫秒的, 于是, 就找 ...

  5. c++11 thread的学习

    http://www.cnblogs.com/wxquare/p/6736202.html 还没开始 留个链接 使用c++11 thread支持实现  一个生产者消费者模型 下面是一个生产者消费者问题 ...

  6. 关于c++11中的thread库

    c++11中新支持了thread这个库,常见的创建线程.join.detach都能支持. join是在main函数中等待线程执行完才继续执行main函数,detach则是把该线程分离出来,不管这个线程 ...

  7. c++11 thread (目前我使用的ZThread库)

    目前为止(2014-11-30),GCC其实已经基本上完全支持C++11的所有功能了,事实上从GCC4.7之后,就支持了-std=c++11选项,在4.7版本之前,也开始支持-std=c++0x的选项 ...

  8. 11. 标准库浏览 – Part II

    第二部分包含了支持专业编程工作所需的更高级的模块,这些模块很少出现在小脚本中. 11.1. 输出格式 reprlib 模块为大型的或深度嵌套的容器缩写显示提供了 :repr() 函数的一个定制版本: ...

  9. C++11并发编程:原子操作atomic

    一:概述 项目中经常用遇到多线程操作共享数据问题,常用的处理方式是对共享数据进行加锁,如果多线程操作共享变量也同样采用这种方式. 为什么要对共享变量加锁或使用原子操作?如两个线程操作同一变量过程中,一 ...

随机推荐

  1. C# 热敏打印机 Socket 网络链接 打印 图片 (一)

    using System; using System.Collections.Generic; using System.Drawing; using System.Linq; using Syste ...

  2. android Can't bind to local 86XX for debugger

    For some reason eclipse DDMS always gives the error 'Can't bind to local 86XX for debugger' every ti ...

  3. 如何在ASP.NET的web.config配置文件中添加MIME类型

    常常有一些特殊的MIME类型是IIS中没有的,一般来说要我们自己手动添加.如果网站经常更换服务器或者网站代码是提供给多个用户使用,那么会造成网站中用到的特殊的MIME类型要经常性的在IIS上配置.这里 ...

  4. Jsonp跨域访问

    很早之前看过好几篇跨域访问的文章,然后做项目的时候基本没有遇到跨域访问的问题.不过该来的还是会来,前些天终于让我遇到了.于是重温了一下原理这些,再进行实战.于是现在也敢通过实战后的一些理解来和大家分享 ...

  5. EF Core1.0 CodeFirst为Modell设置默认值!

    当我们使用CodeFirst时,有时候需要设置默认值! 如下 ; public string AdminName {get; set;} = "admin"; public boo ...

  6. 利用节点更改table内容

    <html xmlns="http://www.w3.org/1999/xhtml"> <head> <title> new document ...

  7. border:0与border:none区别与联系

    联系:前台效果均实现了无边框 区别: 要解释区别,首先得先介绍一下border这个属性. border是一个简写属性.可以设置如下属性 border-width border-style border ...

  8. java Byte[] to String(hex)

    1. 字节数组转换成16进制字符展示 2.代码 package com.goodfan; public class ByteArrayToString { private static char[] ...

  9. [注意]SerialPort操作PCI-1621D多串口卡,出现异常"参数不正确"

    开发LED大屏显示.40-20mA模拟量输出的时候,经常要与串口打交道.但是Windows自带的SerialPort串口操作组件貌似兼容性 不是太好,或是SerialPort本身有BUG,在操作PCI ...

  10. jquery和css3实现滑动导航菜单

    效果预览:http://keleyi.com/keleyi/phtml/html5/15/ 有都中颜色可供选择,请使用支持HTML5/CSS3的浏览器访问. HTML源代码: <!DOCTYPE ...