并发编程
通俗地说,“并发”是指在一个时间段里有多个操作在同时进行,而“多线程”是实现并发的一种方式。
多线程
线程概念
C++
而言,线程就是一个能够独立运行的函数:
1
2
3
4
5
6
auto f = []() // 定义一个lambda表达式
{
cout << "thread id:" << this_thread::get_id() << endl;
};
thread t(f); // 启动一个线程:运行函数f
多线程开发
基于基础的thread
线程类的简单例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
mutex out_lock;
void func(const char* name)
{
thid_thread::sleep_for(100ms);
lock_guard<mutex> guard{
out_lock;
};
cout << "Thread: " << name << endl;
}
int main(){
thread t1{func, "A"};
thread t2{func, "B"};
t1.join();
t2.join();
}
可以看到几个细节:
thread
要求析构之前需要join
,要么detach
(放弃线程管理),否则程序会异常退出;- 使用互斥量(
mutex
)锁定cout
,否则输出会交织在一起;互斥量特性:单个互斥量只能被一个线程锁定。mutex只可默认构造,不可拷贝(或移动),不可赋值,提供的方法:lock
:锁定,当锁被其他线程获得时则堵塞执行;try_lock
:尝试锁定,获得锁则返回true
,否则返回false
;unlock
:解除锁定(获取锁的时候调用)。
上述例子没有返回数据,如果需要在某个线程执行后取回结果,则需要使用信号量/条件变量。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
void work(condition_variable& cv,
mutex& cv_mut,
bool& result_ready,
int& result)
{
this_thread::sleep_for(2s);
result = 66;
{
unique_lock lock{cv_mut};
result_ready = true;
}
cv.notify_one();
}
int main()
{
condition_variable cv; // 条件变量
mutex cv_mut; // 互斥量
bool result_ready = false; // 结果状态变量
int result; // 结果变量
thread t1{work, ref(cv), ref(cv_mut),
ref(result_ready), ref(result)}; // ref声明引用输入
cout << "Waiting for something." << endl;
unique_lock lock{cv_mut}; // 单一锁
cv.wait(lock, [&] {
return result_ready;
});
cout << "Answer: " << result << endl;
t1.join();
}
可以看到,为了返回函数结果,需要额外定义:条件变量
、单一锁
、结果变量状态
、结果变量
,相对使用复杂,可以用async
来简化(后文重点介绍)。
编程技巧
C++多线程编程读取const变量是安全的:多用const关键字,“读而不写”就不会有数据竞争。
保持“仅调用一次”:防止初始化函数多次运行,可以通过声明once_flag类型变量,最好是静态、全局的(所以线程可见),作为初始化的标志:
线程局部存储(thread local storage):通过
thread_local
实现,标记的变量在每个线程中都会有一个独立的副本,即“线程独占”:1 2 3 4 5 6 7 8 9 10 11
thread_local int n = 0; // 线程局部存储变量 auto f = [&](int x) // 线程函数,捕获引用变量 { n += x; cout << n; } int main(){ thread t1{f, 10}; thread t2{f, 20}; // 最终输出为10/20,互不干扰 }
原子变量:原子变量在多线程的含义是:“不可分”,即操作要么完成,要么不可完成,不可被其他操作打断,所以不存在竞争读写的问题。但是不是所有的操作都可以原子化的,只存在一些基本的类型原子化,如:atomic_int、atomic_long等:
1 2 3
using atomic_bool = std::atomic<bool>; using atomic_int = std::atomic<int>; using atomic_long = std::atomic<long>;
原子变量本身是通过模板类包装了原始类型,接口都是一致的,但是禁用了拷贝构造函数,即不可用“=”赋值,只能用圆括号/花括号。
原子变量的最基本用法是:作为线程安全的全局计数器/标志位:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
static atomic_flag flag {false}; static atomic_int n; auto f = [&](){ auto value = flag.test_and_set(); // TAS检查原子标志量 if (value) cout << "flag has been set." << endl; else cout << "set flag by" << this_thread::get_id() << endl; n += 100; // 原子变量加法运算 this_thread::sleep_for(n.load() * 10ms); // 使用时间字面量 }; thread t1(f); thread t2(f); t1.join(); // 等待线程结束 t2.join();
Async
直接调用thread
是相对“原始”的使用方式,使用也更加复杂,可以使用async()
函数,如上文例子:
1
2
3
4
5
6
7
8
9
10
11
int work()
{
this_thread::sleep_for(2s);
return 66;
}
int main(){
auto fut = async(launch::async, work); // launch调用运行策略:新线程调用
cout << "Waiting for something." << endl;
cout << "Answer: " << fut.get() << endl; //get方法只能调用一次,采用移动方法
}
注意:如果你不显式获取 async()
的返回值(即future
对象),它就会同步阻塞直至任务完成(由于临时对象的析构函数),于是async
就变成了sync
。如果不关心返回值,则可以用auto
来避免:
1
2
std::async(task, ...); // 没有显式获取future,被同步阻塞
auto f = std::async(task, ...); // 只有上一个任务完成后才能被执行
Promise
Promise
是另一种使用方式,称之为“承诺量”。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
void work(promise<int> prom)
{
this_thread::sleep_for(2s);
prom.set_value(66);
}
int main()
{
promise<int> prom;
auto fut = prom.get_future();
thread t1{work, move(prom)};
cout << "Waiting for something." << endl;
cout << "Answer: " << fut.get() << endl;
}
promise
和future
需要成对出现:相当于promise
负责数据在线程的移动,future
负责数据获取,不需要考虑返回数据的生命周期管理。
一组promise
和future
只能使用一次,既不能重复设,也不能重复取。