[TOC]
个人认为C++11最明智的举动之一就是加入了线程库,在多核CPU越来越普及,在多线程编程日益,我们可以说是”疯狂“的时代,一个不支持多线程(我是指本身不支持,在编程过程中需要依赖线程库)的语言,如何能普遍适用?
在C++11之前在C/C++中使用多线程编程并非鲜见,这样的代码主要是使用POSIX线程Pthread和OpenMP编译器指令两种编程模型来完成程序的线程化。其中POSIX线程是POSIX标准中关于线程的部分,程序员可以通过一些Pthread线程的API来完成线程的创建、数据的共享、同步等功能。Pthread主要用于C语言,在类UNIX系统上,如FreeBSD、NetBSD、OpenBSD、GNU/Linux、Mac OS X,甚至在Windows上也都有实现,不过Windows上Pthread的实现并非”原生“,主要还是包装为Windows的线程库。不过在使用的便利性上,Pthread不如后来者OpenMP。
原子操作与C++11原子类型
// 原子类型.cpp : 定义控制台应用程序的入口点。
//
#include "stdafx.h"
#include <iostream>
#include <thread>
#include <atomic>
using namespace std;
atomic_llong total{ 0 };
void func(int){
for (long long i = 0; i < 100000000LL; ++i){
total += i;
}
}
int _tmain(int argc, _TCHAR* argv[])
{
thread t1(func, 0);
thread t2(func, 0);
t1.join();
t2.join();
cout << total << endl;
system("pause");
return 0;
}
相比于基于C语言以及过程编程的pthread“原子操作API”而言,C++11对于“原子操作”概念的抽象遵从了面向对象的思想——C++11标准定义的都是所谓的“原子类型”。编译器可以保证原子类型在线程间被互斥的访问,这样设计从并行编程的角度看,是由于需要同步的总是数据而不是代码,因此C++11对数据进行了抽象,会有利于产生行为更为良好的并行代码。而进一步地,一些琐碎的概念,比如互斥锁、临界区则可被C++11的抽象所掩盖,因此并行代码的编写也会变得更加简单。我们可以在看到内置类型的原子类型的定义:
atomic_bool abool; //对应bool
atomic_char achar; //char
atomic_schar aschar; //signed char
atomic_uchar auchar; //unsigned char
atomic_int aint; //int
atomic_uint auint; //unsigned int
atomic_short ashort; //short
atomic_ushort aushort; //unsigned short
atomic_long along; //long
atomic_ulong aulong; //unsigned long
atomic_llong allong; //long long
atomic_ullong aullong; //unsigned long long
atomic_char16_t achar16_t; //char16_t
atomic_char32_t achar32_t; //char32_t;
atomic_wchar_t awchar_t; //wchar_t
不过更为普遍的应该是使用atomic类模板。通过该模板,可以定义出任意需要的原子类型:
std::atomic t;
如上,声明可一个类型为T的原子类型变量t。编译器会保证产生并行情况下行为良好的代码,以避免线程之间对于数据t的竞争。对于线程而言,原子类型通常属于“资源型”的数据,这意味着多个线程通常只能访问的原子类型的拷贝。因此在C++11中,原子类型只能从其模板参数类型中进行构造,标准不允许原子类型经行拷贝构造、移动构造,以及operator=等,防止以外发生如下面:
atomic<float> af{ 1.2f };
//atomic<float> af1{ af }; //这里无法编译
从上面可以看到,af1{ af }的构造方式在C++11中是不允许的,我们可以通过以前的经验轻松知道如何在类的代码中禁止这些行为,事实上,atomic模板类的拷贝构造函数、移动构造函数、operator=等总是默认被删除的。不过从atomic类型的变量来构造其他模板参数类型T的变量则是可以的,比如:
atomic<float> af{ 1.2f };
//atomic<float> af1{ af }; //这里无法编译
//下面的都是正确的
float af2 = af;
vector<float> vfl{ af };
stack<float> sfl;
sfl.push(af);
这是由于atomic类模板总是定义了从atomic到T的类型转换函数的缘故,在需要的时候,编译器会隐式地完成完成原子类型到其对应的类型的转化。能够实现在线程间保持原子性的原因是编译器能够保证针对原子类型的操作都是原子操作。正如之前所说,原子操作都是平台相关的,因此有必要为常见的原子操作进行抽象,定义统一的结构,并根据编译选项,并根据编译选项(或环境)产生其平台相关的实现。在C++11中,标准将原子操作定义为atomic模板类的成员函数,这囊括了绝大多数典型的操作,如读、写、交换等,当然,对于内置类型而言,主要是通过重载一些全局操作符来完成的,在编译的时候,会产生一条特殊的lock前缀的x86指令,lock能够控制总线及实现x86平台上的原子性。下面是atomic类型及相关的操作:
操作 | atomic_flag | atomic_bool | atomic_integral-type | atomic | atomic | atomic | atomic |
---|---|---|---|---|---|---|---|
test_and_set | Y | ||||||
clear | Y | ||||||
is_lock_free | y | y | y | y | y | y | |
load | y | y | y | y | y | y | |
store | y | y | y | y | y | y | |
exchange | y | y | y | y | y | y | |
compare_exchange_weak+strong | y | y | y | y | y | y | |
fetch_add,+= | y | y | y | ||||
fetch_sub,-= | y | y | y | ||||
fetch_or,|= | y | y | |||||
fetch_and,&= | y | y | |||||
fetch_xor,^= | y | y | |||||
++,– | y | y | y | y |
这里的atomic-integral-type和integraltype指的是前面提到的所有的原子类型的整型,而class-type则是指自定义类型。可以看到,对于大多数的原子类型而言,都可以执行读(load)、写(store)、交换(exchange)、比较并交换(compare_exchange_weak/compare_exchange_stronge)等操作。通常情况下,这些原子操作已经足够使用了。如下:
atomic<int> a;
a = 1; //a.store(1);
int b = a; //b = a.load();
这里的赋值语句b=a其实就等价b=a.load()。同样,a=1也相当于a.store(1),由于这些操作都是原子的,所以原来的从