这部分我们来看多线程中Lambda函数的用处。
我们计划创建5个线程,把他们放入一个vector容器中。然后使用for_each()调用join()。
#include <iostream>
#include <thread>
#include <vector>
#include <algorithm>
int main()
{
// vector container stores threads
std::vector<std::thread> workers;
for (int i = 0; i < 5; i++) {
workers.push_back(std::thread([]()
{
std::cout << "thread function\n";
}));
}
std::cout << "main thread\n";
// Looping every thread via for_each
// The 3rd argument assigns a task
// It tells the compiler we're using lambda ([])
// The lambda function takes its argument as a reference to a thread, t
// Then, joins one by one, and this works like barrier
std::for_each(workers.begin(), workers.end(), [](std::thread &t)
{
t.join();
});
return 0;
}
输出是这样的
thread function
thread function
thread function
thread function
thread function
main thread
并发编程的不确定性
从上面的输出我们无法判断每一条输出是哪个线程打印的。所以我们改进一下,利用Lambda函数的捕捉列表,传递索引。
for (int i = 0; i < 5; i++) {
workers.push_back(std::thread([i]() {
std::cout << "thread function " << i << "\n";
}));
}
输出
thread function thread function thread function thread function thread function
main thread
4
2
1
0
3
每次运行的输出都是不一样的,体现了并发编程的不确定性。同时我们从输出还能看到,即使在打印语句之间,它也可能是抢占式的。换句话说,调度程序可以随时中断。
共享资源
下面这段代码已经对cout资源产生了竞争条件
#include <iostream>
#include <thread>
void thread_function()
{
std::cout << "thread function\n";
}
int main()
{
std::thread t(&thread_function);
std::cout << "main thread\n";
t.join();
return 0;
}
如果我们对两个cout加上循环,这个结果就更明显。
#include <iostream>
#include <thread>
void thread_function()
{
for (int i = -100; i < 0; i++)
std::cout << "thread function: " << i << "\n";
}
int main()
{
std::thread t(&thread_function);
for (int i = 0; i < 100; i++)
std::cout << "main thread: " << i << "\n";
t.join();
return 0;
}
thread function: main thread: 0
main thread: 1
main thread: 2
main thread: 3
-100
thread function: -99
thread function: -98
thread function: -97
thread function: -96
thread function: -95
thread function: -94
thread function: -93
thread function: -92
thread function: -91
thread function: -90
thread function: -89
thread function: -88
thread function: -87
thread function: -86
thread function: -85
thread function: -84
thread function: -83
thread function: -82main thread: 4
main thread: 5
main thread: 6
可以看出两个线程随意的获取cout资源。为了确定性访问,下面代码使用mutex:在访问cout前锁住(lock),使用完之后释放锁(unlock)。
#include <iostream>
#include <thread>
#include <string>
#include <mutex>
std::mutex mu;
void shared_cout(std::string msg, int id)
{
mu.lock();
std::cout << msg << ":" << id << std::endl;
mu.unlock();
}
void thread_function()
{
for (int i = -100; i < 0; i++)
shared_cout("thread function", i);
}
int main()
{
std::thread t(&thread_function);
for (int i = 100; i > 0; i--)
shared_cout("main thread", i);
t.join();
return 0;
}
..
thread function:-12
main thread:12
thread function:-11
main thread:11
thread function:-10
main thread:10
thread function:-9
main thread:9
thread function:-8
main thread:8
thread function:-7
main thread:7
thread function:-6
main thread:6
thread function:-5
main thread:5
thread function:-4
main thread:4
thread function:-3
main thread:3
thread function:-2
main thread:2
thread function:-1
main thread:1
共享数据问题
线程间共享数据的问题,主要是由于修改数据导致的。
如果共享数据是只读的,那就没问题。因为一个线程读数据不会受其他线程是否正在读相同的数据的影响。如果一个或多个线程修改数据,就会产生问题。
下面3个线程访问同一个list
#include <iostream>
#include <thread>
#include <list>
#include <algorithm>
using namespace std;
// a global variable
std::list<int>myList;
void addToList(int max, int interval)
{
for (int i = 0; i < max; i++) {
if( (i % interval) == 0) myList.push_back(i);
}
}
void printList()
{
for (auto itr = myList.begin(), end_itr = myList.end(); itr != end_itr; ++itr ) {
cout << *itr << ",";
}
}
int main()
{
int max = 100;
std::thread t1(addToList, max, 1);
std::thread t2(addToList, max, 10);
std::thread t3(printList);
t1.join();
t2.join();
t3.join();
return 0;
}
输出可是这样的
0,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,33,34,0,10,20,30,40,50,60,70,80,90,42,43,44,45,46,47,48,49,50,51,52,53,
54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80
,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,
线程t1、t2都向list添加元素。同时t3访问同一个list并打印。从输出来看,list没能按序添加元素。这是因为3个线程同时访问list。我们需要一些保护机制保证正确的顺序,比如用mutex。
虽然mutex可能是在C++数据保护机制中最常使用的,结构化我们的代码来保护正确的数据同时避免接口中固有的竞争条件,还是非常重要的。
mutex也有自己的问题,比如死锁,或者保护了过多(过少)的数据。
在C++中,通过创建std::mutex的实例来生成一个mutex,通过调用成员函数lock()、unlock()来锁住它或释放它。但是,直接调用成员函数不是一个好的做法,因为在每个离开功能的地方都要记得调用unlock(),包括由于异常需要离开的地方。换句话说,在lock()和unlock()之间发生异常时就会出问题。资源会一直处于locked状态。
因此,标准C++库提供了std::lock_guard类模板,对一个mutex实现RAII,在构造时加锁,析构时释放锁,保证一个锁住的mutex总是能正确的被释放。
#include <iostream>
#include <thread>
#include <list>
#include <algorithm>
#include <mutex>
using namespace std;
// a global variable
std::list<int>myList;
// a global instance of std::mutex to protect global variable
std::mutex myMutex;
void addToList(int max, int interval)
{
// the access to this function is mutually exclusive
std::lock_guard<std::mutex> guard(myMutex);
for (int i = 0; i < max; i++) {
if( (i % interval) == 0) myList.push_back(i);
}
}
void printList()
{
// the access to this function is mutually exclusive
std::lock_guard<std::mutex> guard(myMutex);
for (auto itr = myList.begin(), end_itr = myList.end(); itr != end_itr; ++itr ) {
cout << *itr << ",";
}
}
int main()
{
int max = 100;
std::thread t1(addToList, max, 1);
std::thread t2(addToList, max, 10);
std::thread t3(printList);
t1.join();
t2.join();
t3.join();
return 0;
}
输出
0,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,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56
,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,8
3,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,0,10,20,30,40,50,60,70,80,90,