第十八章 并发:多线程基础知识---从入门到入坑

并发

  • 一、高级接口:async()和Future
    • 1.async()和Future的第一个用例
    • 2.等待两个Tast
    • 3.shared future
  • 二、底层接口:thread和promise
    • 1.class std::thread
    • 2.promise
  • 三、细说启动线程
    • 1.细说async()
    • 2.细说Future
    • 3.细说share future
  • 四、线程同步化与并发问题
    • 1.当心并发
    • 2.并发数据处理为什么会造成为题
    • 3.什么情况下可能出错
    • 4.解决问题所需要的性质
    • 5.其他
  • 五、Mutex 和 Lock
    • 1.使用Mutex和Lock
    • 2.细说Mutex和Lock
    • 3.只调用一次
  • 六、Condition Variable(条件变量)
    • 1.Condition Variable(条件变量)的意图
    • 2.条件变量的第一个完整的例子
    • 3.使用Condition Variable 实现多线程queue
    • 4.细说condition Variable
  • 七、Atomic
    • 1.Atomic用例
    • 2.细说Atomic极其高级接口

一、高级接口:async()和Future

1.async()和Future的第一个用例

#include 
#include 
#include 
#include 
#include 
#include

int doSomething(char c)
{
	std::default_random_engine dre(c);
	std::uniform_int_distribution<int>id(10, 1000);

	for (int i = 0;i < 10;i++)
	{
		std::this_thread::sleep_for(std::chrono::milliseconds(id(dre)));
		std::cout.put(c).flush();
	}
	return c;
}

int func1()
{
	return doSomething('.');
}

int func2()
{
	return doSomething('+');
}


int main()
{
	std::cout << "starting func1() int background"
		<< " and func2() in foreground:" << std::endl;
	std::future<int> result1(std::async(func1));

	int result2 = func2();

	int result = result1.get() + result2;
	std::cout<< "\nresult of func1()+func2():" << result << std::endl;
	return 0;
}

(1)分析程序

std::future<int> result1(std::async(func1));

int result2 = func2();

int result = result1.get() + result2;

首先使用std::async()尝试将其所获得的函数立刻异步起动与一个分离的线程内。func1()在这里启动了,不会造成main()停滞

int result = result1.get() + result2;

随和get()的调用,以下三件事之一会发生
a.如果func1()被asyns()启动于一个分离线程中并且已结束,会立刻获得其结果。
b.如果func1()被启动尚未结束,get()会引发停滞等待func1()结束后获得结果。
c.如果func1()尚未启动,会被强迫启动如同一个同步调用;get()会引发停滞直至产生结果
传给asyns()的东西可以是任何类型的callable object:可以是函数、成员函数、函数对象或是lambda
(2)Launch策略

std::future<long>result1 = std::async(std::launch::async,func1);

如果异步调用此代码无法实现,程序会抛出一个std::system_error异常
有了这个asyns发射策略,就不必非得调用get()了,因为如果返回的future声明即将结束,这个程序必将会等待func1结束。因此,如果不调用get(),当离开future object作用域时,程序会等待后台任务结束

std::future<long>f(std::async(std::launch::deferred,fun1));

强制延缓执行,这保证fun1()绝不会再没有get()(或wait())的情况下启动。
(3)处理异常
“对future调用get()也能处理异常”。
当get()被调用,且后台操作已经(或随后由于异常)而终止,该异常不会再此线程内被处理,而是会被再次传播出去。
因此,欲处理后台操作所发生的异常,只需要偕同get()做出“以同步方式调用该操作”所作的相同动作即可。

#include 
#include 
#include 
#include 
void task1()
{
	std::list<int>v;
	while (true)
	{
		for (int i = 0;i < 1000000;i++)
		{
			v.push_back(i);
		}
		std::cout.put('.').flush();
	}

}

int main()
{
	std::cout << "starting 2 tasks" << std::endl;
	auto f1 = std::async(task1);
	std::cin.get();

	try
	{
		f1.get();
	}
	catch (const std::exception & e)
	{
		std::cerr << "eXCEPTION:" << e.what() << std::endl;
	}
	return 0;
}

(4)等待和轮询
一个future<>只能被get()调用一次,在那之后future就处于无效状态。”
只要对某个future调用wait(),就可以强制启动该future象征的线程并等待这一后台操作终止

std::future<long>f(std::async(func));
f.wait();

另外还有两个类似函数,但他们并不强制启动线程(如果线程尚未启动的话):
1)使用wait_for()并给予一个时间段,就可以让“异步、运行中”的操作等待一段有限的时间:

std:future<long>(std::async(func));
f.wait_for(std::chrono::seconds(10));

2)使用wait_until(),可以等待直至达到某个特定时间点

std::future<long>f(std::async(func));
f.wait_until(std::systerm_clock::now()+std::chrono::minutes(1));

不论wait_for()或wait_until()都返回以下三种东西之一:
1)std::future_status::derred :如果async()延迟了操作而程序中又完全没有调用wait或get(那会强制启动)。这种情况下上述两个函数都会立刻返回。
2)std::future_status::timeout :如果某个操作被异步起动但尚未结束,为waiting又逾期
3)std::future_status::ready : 如果操作已经完成。

2.等待两个Tast

第十八章 并发:多线程基础知识---从入门到入坑_第1张图片
(1)传递实参方式
1)传递给lambda的是c的拷贝

char c = '@';
auto f = std::async([=]{
doSomething(c);
     });

2)

char c = '@';
auto f = std::async(doSomething,c);

3)可以采用by reference方式传递实参。但是有风险:被传递值甚至可能在后台任务启动前就变得无效。

char c = '@';
        auto f = std::async([&]{
doSomething(c);
     });
 
char c = '@';
auto f = std::async(doSomething,std::ref(c);

如果控制实参寿命,使它超越后台任务的生命,可以这么做:

void doSomething(char & c)char c = '@';
 auto f = std::async([&]{
doSomething(c);
     });
f.get();

4)可以传给async()一个指向“指向成员函数”的pointer或reference,指向某个object,后者调用该成员函数:

class X
{
public:
void mem(int num);
 
 
};
 
X x;
auto a = std::async(&X::mem, x, 42);

(2)分析引用传参危险

char c = '@';
        auto f = std::async([&]{
doSomething(c);
     });
 
c = '_';
f.get();

首先,“这里”以及“在doSomthing()内”对c的处理,次序无法预期。
更糟糕的是:在某一线程中改动c,在另一个线程中读取c,这是对同一对象的异步并发处理,将导致不可预期的行为,除非是用mutex或atomic保护并发处理动作。
如果使用async(),就应该以值传递的方式进行。如果复制成本太高。让object以const reference形式传递,且不使用mutable。

3.shared future

(1)class sd::future提供了“处理并发运算的未来结果”的能力,然而只能处理该结果一次。
(2)class std::shared_future,可以多次调用get(),导致相同结果,或导致抛出同一个异常。

#include 
#include 
#include 
#include 
#include 
#include 

void doSomething(char c, std::shared_future<int> f);
int queryNumber();
int main()
{
	std::cout << "starting 2 operators asynchronously" << std::endl;
	std::cout << std::this_thread::get_id() << std::endl;
	

	try
	{
		
		std::shared_future<int>f = std::async(queryNumber);
		auto f1 = std::async(std::launch::async, doSomething,'.',f);
		auto f2 = std::async(std::launch::async, doSomething, '+', 						     f);
		auto f3 = std::async(std::launch::async, doSomething, '*', f);

		f1.get();
		f2.get();
		f3.get();

	}
	catch (const std::exception & e)
	{
		std::cout << "\nEXCEPTION: " << e.what() << std::endl;
	}
	
	
	std::cout << "\ndone" << std::endl;
	return 0;
}

int queryNumber() {

	std::cout << "read number: ";
	int num;
	std::cin >> num;
	if (!std::cin) {
	
		throw std::runtime_error("no number read");
		
	}
	return num;
}

void doSomething(char c, std::shared_future<int> f) {

	try
	{
		int num = f.get();
		std::cout << std::this_thread::get_id() << std::endl;
		for (int i = 0; i < num; ++i)
		{
			std::this_thread::sleep_for(std::chrono::milliseconds(100));
			std::cout.put(c).flush();
		}
	}
	catch (const std::exception& e)
	{
		std::cerr << "\nEXCEPTION: " << e.what() << std::endl;
	}

}


二、底层接口:thread和promise

1.class std::thread

注:除非真的知道自己在做什么,否则面对“处理目标函数所必须”的所有object都应该以 by value方式传递
(1)std::thread的性质
1)class thread 没有所谓的launch策略。C++标准库永远试着将目标函数启动于一个新线程中。如果无法做到就会抛出std::system_error并带着差错码resource_unavailable_try_again
2)没有接口可处理线程结果。唯一可获得的是一个独一无二的线程ID
3)如果发生异常,但未捕获于线程内,程序会立刻终止并调用std::terminate()。若想将异常传播至线程外的某个context,必须使用exception_ptr
4)必须生命是否“想要等待线程结束”或打算“将他自母体卸离使它运行于后台而不受任何控制”。如果在thread object寿命结束之前不这么做,或如果它发生了一次move assignment,程序会终止并调用std::terminate()
5)如果让线程运行于后台而main()结束了,所有线程会被鲁莽而硬性的终止。

#include 
#include 
#include 
#include 
#include 
 
void doSomething(int num,char c);
int main()
{
	try
	{
		std::thread t1(doSomething, 5, '.');
		std::cout << "- started fg thread " << t1.get_id() << std::endl;
 
		for (int i= 0;i < 5;++i)
		{
			std::thread t(doSomething, 10, 'a' + i);
			std::cout << "- detach started bg thread" << t.get_id() << std::endl;
			t.detach();
		}
		std::cin.get();
		std::cout << "- join fg thread " << t1.get_id() << std::endl;
		t1.join();
	}
	catch (const std::exception& e)
	{
		std::cerr << "EXCEPTION: " << e.what() << std::endl;
	}
 
}
 
void doSomething(int num,char c) {
 
try
{
	std::default_random_engine dre(42 * c);
	std::uniform_int_distribution<int>id(10, 1000);
 
	for (int i = 0;i < num;++i)
	{
		std::this_thread::sleep_for(std::chrono::milliseconds(id(dre)));
		std::cout.put(c).flush();
	}
}
catch (std::exception& e)
{
	std::cerr << "THREAD_EXCEPTION ( thread " << std::this_thread::get_id() << " )" 	<< e.what() << std::endl;
}
catch (...)
{
	std::cerr << "THREAD_EXCEPTION ( thread " << std::this_thread::get_id() << " )" << std::endl;
}
 
 
}

2.promise

(1)欲传递值给线程,可以仅仅把它们当作实参传递。
(2)如果需要线程的运行结果,可以用by reference方式传递。
用来传递运行结果和异常的一般性机制是:class std::promise

#include 
#include 
#include 
#include 
#include 
#include 
#include
#include 

void doSomething(std::promise<std::string>& p);
int main()
{
	try
	{
		std::promise<std::string>p;
		std::thread t(doSomething, std::ref(p));
		t.detach();


		std::future<std::string>f(p.get_future());
		std::cout << "result: " << f.get() << std::endl;
	}
	catch (const std::exception& e)
	{
		std::cerr << "EXECEPTION: " << e.what() << std::endl;
	}
	return 0;
}

void doSomething(std::promise<std::string>& p) {

	try
	{
		std::cout << "read char ('x' for exception): ";
		char c = std::cin.get();
		if (c == 'x')
		{
			throw std::runtime_error(std::string("char ") + c + " read");
		}
		std::string s = std::string("char ") + c + " processed";
		p.set_value(std::move(s));
	}
	catch (const std::exception&)
	{
		p.set_exception(std::current_exception());
	}

}

三、细说启动线程

1.细说async()

(1)future async(std::launch::async,F func,args…)
1)尝试启动func并给予实参args,形成一个异步任务。
2)如果以上办不到,就抛出std::system_error异常,带着差错码std::errc::resource_unavailable_try_again
3)被启动的线程保证再程序结束前完成,除非程序中途失败
4)以下程序会结束线程
-对返回的future调用get()或wait()
-如果最后一个指向“返回的future所代表的shared state”的object被销毁。
5)这意味着对async()的调用会造成停滞知道func完成—如果async的返回值未被使用的话。
(2)future async(std::launch::deferred,F func,args…)
1)传递func并夹带实参args,形成一个推迟任务,当我们对返回的future调用wait()或get(),那个推迟任务即被同步调用。
2)如果未曾调用wait()或get(),推迟任务即被同步调用
(3)future async(F func,args…)
1)相当于调用async()并携带“std::launch::deferred和std::launch::async组合而成”的launch策略。系统会根据当前形势选择一个发射策略。也就是说,如果立即发射策略行不通的话,会造成func被推迟调用。
2)即,如果async()可以为func启动一个线程,就那么做。否则func就会被推迟,知道调用get()或wait()。
3)这个调用唯一的保证是,一旦我们返回的future调用get()或wait(),func就一定会被调用并完成。
4)如果没有对返回的future调用get()或wait(),func有可能会永不被调用。
5)注意:如果无法异步调用func,本形式的async()不会抛出system_error异常。

2.细说Future

(1)如果future是被async()返回且其相关的task受到推迟,对它调用get()或wait()会同步启动task。注意,wait_for()和wait_until()都不会令一个被推迟的任务启动。
(2)成果只能被提取一次。因此future可能处于有效或无效状态:有效状态意味着“某一操作的成果或爆发的异常”尚未被取出。
(3)注意:get()的返回值取决于future<>特化类型
1)如果他是void,get()获得的就是void,也就是“无物”。
2)如果future的template参数是个reference类型,get()便返回reference指向返回值。
3)否则get()返回返回值一份copy,或是对返回值进行move assign动作—取决于返回类型是否支持move assignment语义。
(4)只调用get()一次,因为get()会令future处于无效状态。
(5)future既不提供copy构造函数也不提供copy assignment操作符,确保不会有两个object共享某一后台操作的状态。“将某个future object状态搬移至另一个”的唯一办法是:调用move构造函数或assignment操作符。

3.细说share future

(1)允许多次调用get()。因此get()不必令其状态失效
(2)支持copy语义(copy构造函数、copy assignment操作符)
(3)get()是个const成员函数,返回一个const reference指向“存储于share state“的值(这意味着必须确定”被返回的reference“的寿命短于shared state)。但class std::future的get()却是个non-const成员函数,返回一个move-assigned拷贝,除非这个class”被一个reference类型实现特化“。
(4)不提供share()
(5)get()返回值不是拷贝这一事实造成若干风险:除了生命周期需要考虑,还可能形成数据竞争。
4.细说class std::promise
(1)promise可持有一个share state。如果那个share state持有一个值或一个异常,他的状态为ready。
(2)只能调用get_future()一次
5.细说class std::thread
(1)thread ID有自己的类型std::thread::id。其default构造函数会产出一个独一无二的ID用以表示“非线程”。

四、线程同步化与并发问题

1.当心并发

(1)多个线程并发处理相同的数据而又不曾同步化,唯一安全的情况就是:所有线程只读数据
(2)data race:不同线程中的两个互相冲突的动作,其中至少一个动作不是atomic,而且无一个动作发在另一个动作之前

2.并发数据处理为什么会造成为题

(1)一个编程语言如C++,总是个抽象层,用以支持不同的平台和硬件,后者根据其体系结构和目的提供不同的能力和接口。因此,一个想C++这样的标准具体描述了语句和操作的影响,但并非等同于其所产生的汇编码。标准描述的是what而非how
(2)函数调用其实参核值次序没有具体的说明。如果程序对此有特定的期待,会导致不可预期的行为

3.什么情况下可能出错

在c++中我们可能会遇到以下问题:
(1)未同步化数据访问:并行运行两个线程读和写同一笔数据,不知道那个语句先来
1)除非另有说明,C++标准库提供的函数通常不支持“读或写”动作与另一个“写”动作(写至同步一笔数据)并发执行
即,除非另有说明,来自多线程“对用同一object的多次调用”会导致不可预期的行为。
2)C++标准库对线程安全提供了以下保证
-并发处理同一容器内的不同数据是可以的。
-并发处理string stream、file stream或stream buffer会导致不可预期的行为。
(2)写至半途的数据:某个线程正在读数据,另一个线程改动他,因此读取中的线程甚至可能读到改了一半的数据,读到一个半新半旧的值。
(3)重新安排语句:语句和操作有可能重新安排次序,也许对每一个单线程正确,但对于多线程的组合却破坏了预期的行为。
例:将“某线程中对data的设定”和“另一线程中对data的消费”同步化。
供应端:
Data = 42;
readyFlag = ture;
消费端:
while(!readyFlag)
{
。。。
}
Foo(data);
分析:事实上,第二线程的输出可能是data“在第一线程赋值42之前”的旧值(甚至任何值,因为42的赋值动作有可能只做到一半)
也就是说:编译器或硬件有可能重新安排语句:
readyFlag = ture;
Data = 42;
注意:默认情况下C++编译器应该有能力生成高度优化的代码,而某些优化行为可能需要重新安排语句。

4.解决问题所需要的性质

(1)atomic:读或写一个变量,或是写一连串的变量,其行为是独占的、排他的,无任何打断。因此,一个线程不可能读到“因另一个线程而造成的”中间状态。
(2)Order:需要一些方法保证“具体制定的语句”次序
(3)解决之道:
1)使用future和promise。均保证了atomic和Order:一定是在形成成果之后才设定shared state,这意味着读和写不会同时发生。
2)可以使用mutex和lock来处理critical section和protected zone
3)可以使用condition variable有效的令某些线程等待若干“被另一个线程处理其他一个或多个线程所提供的数据或状态”
4)可以使用“atomic data type”确保对变量或对象的访问动作都是不可切割的。
5)可以使用“atomic data type”的底层接口

5.其他

volitile是个C++关键字,用来阻塞过度优化。

五、Mutex 和 Lock

Mutex全名mutual exclusion(互斥体),是个object,用来协助采取独占排他方式控制“对资源的并发访问”
为了获取独占式的资源访问能留,相应的线程必须锁定(lock)mutex,这样可以防止其他线程也锁定mutex,直到第一个线程解锁(unlock)mutex。

1.使用Mutex和Lock

(1)例:
某一个线程:

valMutex.lock()if(val >= 0)
	F(val);
else
F(-val);
	valMutex.unlock()

另一个线程:

valMutex.lock();
val++;
valMutex.unlock()

1)凡是可能发生concurrent access的地方都该使用一个mutex,不论读或写都因该如此。
2)deadlock情景:两个线程在释放他们自己的lock之前等待对方的lock

int val;
std::mutex valMutex;
 
std::lock_guard<std::mutex>lg(valMutex);
if(val >= 0)
	F(val);
else
	F(val);

3)lock应该被限制在可能最短的周期内,因为他们会阻塞其他线程代码的并行运行机会。

#include
#include
#include
#include
 
std::mutex printMutex;
void print(const std::string & s);
 
int main()
{
	auto f1 = std::async(std::launch::async, print, "Hello from a first thread");
	auto f2 = std::async(std::launch::async, print, "Hello from a second thread");
	print("Hello from the main thread");
	return 0;
}
 
void print(const std::string & s) {
 
std::lock_guard<std::mutex>l(printMutex);
for (char c : s)
{
std::cout.put(c);
}
std::cout << std::endl;
}

运行结果:
第十八章 并发:多线程基础知识---从入门到入坑_第2张图片
或者:

第十八章 并发:多线程基础知识---从入门到入坑_第3张图片
不加互斥锁的结果:

第十八章 并发:多线程基础知识---从入门到入坑_第4张图片
(2)递归Lock
1)在每个public函数内放一个mutex并取得其lock,用以防止data race腐蚀对象的内部状态

class DatabaseAccess{
private:
	std::mutex dbMutex;
public:
	void createTable(){
	std::lock_guard<std::mutex>lg(dbMutex);
	}
	void insertData(){
	std::lock_guard(std::mutex)lg(dbMutex);
	}
};

引入一个public成员函数而它可能调用public成员函数

void createTableAndInsertData()
{
	std::lock_guard<std::mutex>lg(dbMutex);
	createTable();//错误!死锁 原因:dbMutex已经锁住一次了
}

调用createTableAndInsertData()回造成死锁,因为它锁住dbMutex之后调用createTable(),造成后者尝试再次lock dbMutex ,这将造成阻塞直到dbMutex变为可用,而这绝对不可发生。因为:createTableAndInsertData()会block(阻塞)之直到createTable()完成.
2)解决:recursive_mutex:允许同一线程多次锁定,并在最近一次相应的unlock()时释放lock

class DatabaseAccess{
private:
	std::recursive_mutex dbMutex;
public:
	void createTable(){
		std::lock_guard<std::recursive_mutex>lg(dbMutex);
	}
	void insertData(){
	std::lock_guard(std::recursive_mutex)lg(dbMutex);
	}
	void createTableAndInsertData()
	{
		std::lock_guard<std::recursive_mutex>lg(dbMutex);
		createTable();
	}
 
};

(3)尝试性的lock以及带实践性的lock
1)mutex 提供成员函数try_lock(),它试图取得一个lock,成功就返回true,否则false

std::mutex m;
While(m.try_lock() == false)
{
	doSomeOtherStuff();
}
std::lock_guard<std::mutex>lg(m,std::adopt_lock);

try_lock有可能假设性失败,也就是说即使lock并未被他人拿走也有可能失败(返回false)。
2)为了等待特定长度的时间,可以选用(带时间性的)timed mutex
Mutex class std::timed_mutexh和std::recursive_timed_mutex额外允许调用try_lock_for(0或try_lock_until(),用以等待某个时间段,或直至抵达某个时间点。

std::timed_mutex m;
if(m.try_lock_for(std::chrono::seconds(1)))
{
	std::lock_guard<std::timed_mutex>lg(m,std::adopt_lock);
}
else
	couldNoteGetTheLock();

try_lock有可能假设性失败,也就是说即使lock并未被他人拿走也有可能失败(返回false)。

2)为了等待特定长度的时间,可以选用(带时间性的)timed mutex
Mutex class std::timed_mutexh和std::recursive_timed_mutex额外允许调用try_lock_for(0或try_lock_until(),用以等待某个时间段,或直至抵达某个时间点。

std::timed_mutex m;
if(m.try_lock_for(std::chrono::seconds(1)))
{
	std::lock_guard<std::timed_mutex>lg(m,std::adopt_lock);
}
else
	couldNoteGetTheLock();

(4)处理多个Lock

std::mutex m1;
std::mutex m2;{
	std::lock(m1,m2);
	std::lock_guard<std::mutex>lockM1(m1,std::adopt_lock);
	std::lock_guard<std::mutex>lockM2(m2,std::adopt_lock);
}

1)全局函数std::lock()会锁柱它收到的所有mutex,而且会阻塞直到所有的mutex都被锁定或直到抛出异常。如果是后者,已被成功锁定的mutex都会被解锁
2)adopt_lock确保任何情况下这些mutex在离开作用域时会被解锁。
3)注意这个lock()提供了一个deadlock回避机制,但那也意味着多个lock的锁定次序并不明确。
4)全局函数std::try_lock()会在取得多有lock情况下返回-1,否则返回第一个失败的lock的索引(从0开始),且如果这样的话,所有成功的lock会被unlock

std::mutex m1;
std::mutex m2;
 
int idx = std::try_lock(m1,m2);
if(idx < 0)
{
	std::lock_guard<std::mutex>lockM1(m1,std::adopt_lock);
	std::lock_guard<std::mutex>lockM2(m2,std::adopt_lock);
}
else
	std::cerr<<"could not lock mutex m"<<idx + 1<<Std::endl;

(5)class unique_lock
1) 对unique_lock可以调用owns_lock()或bool()来查询其mutex目前是否被锁住。
2)这个class的主要优点是:如果析构时mutex仍被锁住,其析构函数会被自动调用unlock()。
①传递try_to_lock,表示企图锁定mutex但不希望阻塞;

Std::unique_lock<std::mutex>lock(mutex,std::try_to_lock);
If(lock)
{.
}

②可以传递一个时间段或时间点给构造函数,表示尝试在一个明确的时间周期内锁定:

std::unique_lock<std::timed_mutex>lock(mutex,std::chrono::seconds(1));

③传递defer_lock,表示初始化这一lock object但尚未打算锁住mutex:

std::unique_lock<std::mutex>lock(mutex.std::defer_lock);
lock.lock();

defer_lock flag可以用来建立一个或多个lock用于稍后锁住他们:

std::mutex m1;
std::mutex m2;
std::unique_lock<std::mutex>lockM1(m1,std::defer_lock);
std::unique_lock<std::mutex>lockM2(m2,std::defer_lock);
std::lock(m1,m2);

(3)class unique_lock提供release()用来释放mutex,或是将其mutex拥有权转给第一个lock。

2.细说Mutex和Lock

(1)细说Mutex
1)Class std::mutex: 同一时间可被一个线程锁定。如果线程被锁定,任何其他lock()都会阻塞(block),直到这个mutex再次可用,且try_lock()会失败。
2)Class std::recursive_mutex:允许在同一时间多次被同一线程获得其lock。其典型应用是:函数捕获一个lock并调用另一个函数而后者再次捕获相同的lock
3)Class std::timed_mutex额外允许传递一个时间段或时间点,用来定义多长时间内它可以尝试捕获一个lock。为此它提供了一个try_lock_for()和try_lock_until()
4)Class std::recursive_timed_mutex允许同一个线程多次取得其lock,可指定限期。

(2)细说Class lock_guard
1)用以确保一个locked mutex在离开作用域时总会被释放。它的整个生命周期总是与一个lock相关联

(3)细说Class unique_lock
1)为一个不一定得锁定(或拥有)的mutex提供一个lock guard。
2)如果它在析构期间仍旧被锁定(或拥有)mutex,他会调用unlock

3.只调用一次

单线程环境中:

bool initialized = false;
if(!initialized)
{
	Initialize();
	Initialized = true;
}

static std::vector<std::string>staticData;
void foo()
{
	if(staticData.empty())
	staticData = initializeStaticData();
}

多线程中:

std::once_flag oc;
std::call_once(oc,initialize);

static std::vector<std::string>staticData;
void foo()
{
	static std::once_flag oc;
	std::call_once(oc,[]{
	staticData = initializeStaticData();
});
}

多线程环境中缓式初始化

Class X{
private:
	Mutable std::once_flag initDataFlag;
	void initData();
public:
	Data getData()const{
	std::call_once(initDataFlag,&X::initData,this);
	}
};

六、Condition Variable(条件变量)

future:允许你停下来直到另一个线程提供某笔数据或直到另一个线程结束。Future 从某线程传递数据到另一个线程只能一次。事实上,future的主要目的是处理线程的返回值或异常。

1.Condition Variable(条件变量)的意图

(1)操作流程
1)同时包含和,并声明一个mutex
和一个condition variable:

#include
#include
std::mutex readyMutex;
std::condition_variable readyCondVar;
``
`
2)那个激发“条件终于满足”的线程(或多线程之一)必须调用

```cpp
readyCondVar.notify_once();

readyCondVar.notify_all();

3)那个“等待条件被满足”的线程必须调用

std::unique_lock<std::mutex>l(readyMutex);
readyMutex.wait(1);

(2)注意:
首先:为了等待这个condition variable,需要一个mutex和unque_lock (lock_guard是不够的,因为等待中函数有可能锁定或解除mutex)
此外,condition variable也许有所谓假醒。也就是某个condition variable 的wait动作有可能在该condition variable 尚未被notified时便返回。
因此,发生wakeup不一定意味着线程所需要的条件已经掌握了。更确切的说,在wakeup之后仍然需要代码去验证“条件实际以达成”。
因此必须检查数据是否正真备妥,或是仍需要诸如ready flag之类的东西。为了设立和查询它端供应的数据或ready flag ,可使用同一个mutex。

2.条件变量的第一个完整的例子

#include
#include
#include
#include
 
bool readyFlag;
std::mutex readyMutex;
std::condition_variable readyCondVar;
 
void thread1();
void thread2();
 
int main()
{
auto f1 = std::async(std::launch::async, thread1);
auto f2 = std::async(std::launch::async, thread2);
return 0;
}
 
void thread1() {
 
std::cout << "" << std::endl;
std::cin.get();
 
{
std::lock_guard<std::mutex>lg(readyMutex);
readyFlag = true;
}
readyCondVar.notify_one();
 
}
 
void thread2() {
 
{
std::unique_lock<std::mutex>ul(readyMutex);
readyCondVar.wait(ul, [] {
 
return readyFlag;
 
});
}
 
std::cout << "done" << std::endl;
 
}

Condition variable的wait()成员函数使用:把mutex readyMutex的lock ul当作第一个实参,把一个lambda当作当作第二个参数,用来二次检测条件是否真的满足。其效果时wait()=内部会不断调用该第二实参,直到它返回true。

{
std::unique_lock<std::mutex>ul(readyMutex);
readyCondVar.wait(ul, [] {
 
return readyFlag;
 
});
 
}

等于

{
std::unique_lock<std::mutex>ul(readyMutex);
While(!readyFlag)
readyCondVar.wait(ul);
}

3.使用Condition Variable 实现多线程queue

#include
#include
#include
#include
#include
 
std::queue<int>queue;
std::mutex queueMutex;
std::condition_variable queueCondVar;
 
void provider(int val);
void consumer(int num);
 
int main()
{
auto p1 = std::async(std::launch::async, provider, 100);
auto p2 = std::async(std::launch::async, provider, 300);
auto p3 = std::async(std::launch::async, provider, 500);
 
auto c1 = std::async(std::launch::async, consumer, 1);
auto c2 = std::async(std::launch::async, consumer, 2);
 
return 0;
}
 
void provider(int val) {
 
std::cout << "provider : "<<std::this_thread::get_id() << std::endl;
for (int i = 0; i < 6; i++)
{
std::lock_guard<std::mutex>lg(queueMutex);
queue.push(val + i);
}
queueCondVar.notify_one();
std::this_thread::sleep_for(std::chrono::milliseconds(val));
}
 
void consumer(int num) {
 
std::cout << "consumer : " << std::this_thread::get_id() << std::endl;
while (true)
{
int val;
{
std::unique_lock<std::mutex>ul(queueMutex);
queueCondVar.wait(ul,[] {
return !queue.empty();
});
val = queue.front();
queue.pop();
}
std::cout << "consumer " << num << " : " << val << std::endl;
}
}

程序解读:
第十八章 并发:多线程基础知识---从入门到入坑_第5张图片
就这种情况而言:程序内部解读为:
首先consumer、provider线程同时开启,但是很明显由于consumer的访问需要provider的唤醒。由输出可以看出,虽然consumer线程开启但是抑制没有进入,直到provider开启权限。
对于provider而言,三个线程开启并执行,此次实验效果是:auto p2 = std::async(std::launch::async, provider, 300);线程执行比较快,也就有了consumer先能够访问300这个队列的结果,以此类推即可!

4.细说condition Variable

(1)class condition_variable
1)用来唤醒一个或多个等待某特定条件(意指某些必须由他人提供或执行的东西)获得满足的线程。
2)多个线程可等待同一个条件发生,一旦条件满足,线程就可以通知所有(或某个)等待者(线程)。
3)由于可能发生假醒,当条件满足时,仅仅通知是不够的,等待着(线程)必须在苏醒之后来两次检查该条件。
4)所有等待某个条件变量的线程都必须使用相同的mutex
5)当wait()家族的某个成员被调用时,该mutex必须使用unique_lock锁定,否则会发生不明确行为。
5)条件变量的消费者总是在“被锁住的mutex”基础上操作的。只有等待函数会执行以下三个原子操作步骤暂时解除mutex:
①解除mutex 进入等待状态
②解除因等待而造成的阻塞
③再次锁住mutex
这意味着传给wait函数的那个判别式总是在lock情况下被调用,所以他们可以安全的处理mutex保护的对象。

七、Atomic

注意:
(1)一般而言,即使面对基本数据类型,读和写也不是不可切割的。
(2)编译器生成的代码有可能改变操作次序。
顺序一致性:在线程之中原子操作保证一定“像代码出现次序”那样发生。

1.Atomic用例

(1)使用原子操作的流程
1)必须使用包含头文件,其内声明了atomic
2)然后使用std::atomic<>class template声明一个atomic object
注意:总是应该将atomic object初始化,因为其默认构造函数并不完全初始化它。
改写:

{
std::lock_guard<std::mutex>lg(readyMutex);
readyFlag = true;
}

改为:

readyFlag.store(true);
{
std::unique_lock<std::mutex>ul(readyMutex);
readyCondVar.wait(ul, [] {
 
return readyFlag;
 
});
}

改为:

while(!readyFlag.load)
{
std::this_thread::sleep_for(std::chrono::millisenconds(100));
}

2.细说Atomic极其高级接口

(1)一般而言,原子操作获得的是copy而不是reference
(2)默认构造函数并未能完全能够将object初始化。默认构造函数之后唯一合法的操作就是调用atomic_init()完成初始化。
(3)接受相关类型值的那个构造函数并不是原子操作
(4)所有函数,除了构造函数,都被重载为volatile和non-volatile。

你可能感兴趣的:(C++11标准程序库,多线程,future,并发编程)