C++并发编程----并发和同步(《C++ Concurrency in Action》 读书笔记)

文章目录

      • 条件变量
      • 构建线程安全队列
      • std::future< > 、 std::shared_future< >
      • 等待时间

本文为《C++ Concurrency in Action》 读书笔记,对其中的一些知识点进行总结。阅读这本书前建议先阅读《Effective C++》

条件变量

std::condition_variable 和 std::condition_variable_any 。这两个实现都包含 在 < condition_variable > 头文件的声明中。
两者都需要与一个互斥量一起才能工作(互斥量是 为了同步);前者仅限于与 std::mutex 一起工作,而后者可以和任何满足最低标准的互斥量一 起工作,从而加上了_any的后缀。一般使用std::condition_variable_any会产生更多的开销和使用更多的资源,因此,我们一般首选std::condition_variable。

std::mutex mut; 
std::queue<data_chunk> data_queue; // 1 两个线程之间传递数据的队列
std::condition_variable data_cond; 
void data_preparation_thread() 
{ 
	while(more_data_to_prepare()) 
	{ 
		data_chunk const data=prepare_data(); 
		std::lock_guard<std::mutex> lk(mut); 
		data_queue.push(data); // 2 对队列上锁,将准备好的数据压入队列中
		data_cond.notify_one(); // 3 对等待的线程(如果有等待 线程)进行通知
	} 
}
void data_processing_thread() 
{ 
	while(true) 
	{ 
		std::unique_lock<std::mutex> lk(mut); 
		data_cond.wait( 
		     lk,[]{return !data_queue.empty();}); 
		data_chunk data=data_queue.front(); 
		data_queue.pop(); 
		lk.unlock(); 
		process(data); 
		if(is_last_chunk(data)) 
		break; 
	} 
}

data_cond.wait() )会去检查这些条件(通过调用所提供的lambda函数),当条件满足(lambda函数返回true) 时返回。如果条件不满足(lambda函数返回false),wait()函数将解锁互斥量,并且将这个线程 (上段提到的处理数据的线程)置于阻塞或等待状态。当准备数据的线程调用notify_one()通知 条件变量时,处理数据的线程从睡眠状态中苏醒,重新获取互斥锁,并且对条件再次检查, 在条件满足的情况下,从wait()返回并继续持有锁。当条件不满足时,线程将对互斥量解锁, 并且重新开始等待。 使用 std::unique_lock 而不使用 std::lock_guard等待中 的线程必须在等待期间解锁互斥量,并在这之后对互斥量再次上锁,而 std::lock_guard 没有这么灵活。

构建线程安全队列

设计一个安全队列,用于线程之间的通信,安全队列的使用是非常重要的,比如线程池中一般就会使用安全队列用于传递信息。

#include  
#include  
#include  
#include  
template<typename T> 
class threadsafe_queue 
{
private: 
	mutable std::mutex mut; //  互斥量必须是可变的, 以便在const函数中可以对其进行上锁
	std::queue<T> data_queue; 
	std::condition_variable data_cond; 
public: 
	threadsafe_queue() 
	{}
	threadsafe_queue(threadsafe_queue const& other) 
	{ 
		std::lock_guard<std::mutex> lk(other.mut); 
		data_queue=other.data_queue; 
	}
	void push(T new_value) 
	{ 
		std::lock_guard<std::mutex> lk(mut); 
		data_queue.push(new_value); 
		data_cond.notify_one(); 
	}
	void wait_and_pop(T& value) 
	{ 
		std::unique_lock<std::mutex> lk(mut); 
		data_cond.wait(lk,[this]{return !data_queue.empty();}); 
		value=data_queue.front(); 
		data_queue.pop(); 
	}
	std::shared_ptr<T> wait_and_pop() 
	{ 
		std::unique_lock<std::mutex> lk(mut); 
		data_cond.wait(lk,[this]{return !data_queue.empty();}); 
		std::shared_ptr<T> res(std::make_shared<T> 
		(data_queue.front())); 
		data_queue.pop(); 
		return res; 
	}
	bool try_pop(T& value) 
	{ 
		std::lock_guard<std::mutex> lk(mut); 
		if(data_queue.empty()) 
			return false; 
		value=data_queue.front(); 
		data_queue.pop(); 
		return true; 
	}
	std::shared_ptr<T> try_pop() 
	{ 
		std::lock_guard<std::mutex> lk(mut); 
		if(data_queue.empty()) 
			return std::shared_ptr<T>(); 
		std::shared_ptr<T> res(std::make_shared<T> (data_queue.front())); 
		data_queue.pop(); 
		return res; 
	}
	bool empty() const 
	{ 
		std::lock_guard<std::mutex> lk(mut); 
		return data_queue.empty(); 
	} 
}; 

为了防止固有接口间的条件竞争,一般将front()和pop()合并成一个函数调用,也就是在一个函数中即完成出栈队列,又完成对数据的取用。

std::future< > 、 std::shared_future< >

当一个线程需要等待一个特定的一次性 事件时,在某种程度上来说它就需要知道这个事件在未来的表现形式。这个线程会周 期性(较短的周期)的等待或检查,事件是否触发;在检查期间也会执行其他任务 。在等待任务期间它可以先执行另外一些任务,直到对应的任务触 发,而后等待期望的状态会变为就绪(ready)。
唯一期望(unique futures)( std::future< > )和共享期望(shared futures)( std::shared_future< > )。 std::future 的实例只能与一个指定事件相关联, 而 std::shared_future 的实例就能关联多个事件。当多个线程需要访问一个独 立“期望”对象时,他们必须使用互斥量或类似同步机制对访问进行保护。

带返回值的后台任务

当一个任务的结果你不着急要时,你可以使用 std::async 启动一个异步任务。与 std::thread 对 象等待的方式不同, std::async 会返回一个 std::future 对象,这个对象持有最终计算出来 的结果。当你需要这个值时,你只需要调用这个对象的get()成员函数;

#include  
#include  
int find_the_answer_to_ltuae(); 
void do_other_stuff(); 
int main() 
{ 
	std::future<int> 
	the_answer=std::async(find_the_answer_to_ltuae); 
	do_other_stuff(); 
	std::cout<<"The answer is "<<the_answer.get()<<std::endl; 
} ;

与 std::thread 做的方式一样, std::async 允许你通过添加额外的调用参数,向函数传递额 外的参数。当第一个参数是一个指向成员函数的指针,第二个参数提供有这个函数成员类的 具体对象(不是直接的,就是通过指针,还可以包装在 std::ref 中), 剩余的参数可作为成员 函数的参数传入。

更多用法:

#include  
#include  
struct X 
{ 
	void foo(int,std::string const&); 
	std::string bar(std::string const&); 
};
X x; 
auto f1=std::async(&X::foo,&x,42,"hello"); // 调用p->foo(42, "hello"),p是指向x的指针 
auto f2=std::async(&X::bar,x,"goodbye"); // 调用 tmpx.bar("goodbye"), tmpx是x的拷贝副本 
struct Y 
{ 
	double operator()(double); 
};
Y y; 
auto f3=std::async(Y(),3.141); // 调用tmpy(3.141),tmpy通过Y的移动 构造函数得到 
auto f4=std::async(std::ref(y),2.718); // 调用y(2.718) 
X baz(X&); 
std::async(baz,std::ref(x)); // 调用baz(x) 
class move_only 
{
public: 
move_only(); 
move_only(move_only&&) 
move_only(move_only const&) = delete; 
move_only& operator=(move_only&&); 
move_only& operator=(move_only const&) = delete; 
void operator()(); 
};
auto f5=std::async(move_only()); // 调用tmp(),tmp是通过 std::move(move_only())构造得到 

调用方式选择:

auto f6=std::async(std::launch::async,Y(),1.2); // 在新线程上执行 
auto f7=std::async(std::launch::deferred,baz,std::ref(x)); // 在 wait()或get()调用时执行 
auto f8=std::async( std::launch::deferred | std::launch::async, baz,std::ref(x)); // 实现选择执行方式 
auto f9=std::async(baz,std::ref(x)); 
f7.wait(); // 调用延迟函数 

任务和期望 packaged_task
std::packaged_task<> 对一个函数或可调用对象,绑定一个期望。当 std::packaged_task<> 对象被调用,它就会调用相关函数或可调用对象,将期望状态置为就绪,返回值也会被存储 为相关数据。

template<> 
class packaged_task<std::string(std::vector<char>*,int)> 
{
	public: 
	template<typename Callable> 
	explicit packaged_task(Callable&& f); 
	std::future<std::string> get_future(); 
	void operator()(std::vector<char>*,int); 
}; 

这里的 std::packaged_task 对象是一个可调用对象,并且它可以包含在一个 std::function 对 象中,传递到 std::thread 对象中,就可作为线程函数;传递另一个函数中,就作为可调用对 象,或可以直接进行调用。当 std::packaged_task 作为一个函数调用时,可为函数调用操作 符提供所需的参数,并且返回值作为异步结果存储在 std::future ,可通过get_future()获 取。你可以把一个任务包含入 std::packaged_task ,并且在检索期望之前,需要 将 std::packaged_task 对象传入,以便调用时能及时的找到。

使用std::promises

可以通过get_future()成员函数来获取与一个给定的 std::promise 相关的 std::future 对象。

void fun1(std::promise<int> &p)//线程1执行函数
{
	
	int Val = 15;
	std::cout << "传入数据(int):" << Val <<endl;
	p.set_value(Val);//设置数据
}
 
void fun2(std::future<int> &f)//线程二指向函数
{
	auto Val = f.get();	//会阻塞,知道收到数据	
	cout << iVal <<endl;
}

实际上,可以通过promise传递函数指针,可调用对象,以及typename类型等。
参考:promise、

std::shared_future< >
std::future 模型独享同步结果的所有权,并且通过调用get()函数,一次性 的获取数据,只有一个线程可以获取结果值,因为在第一 次调用get()后,就没有值可以再获取了。
如果想要多个线程等待同一结果,我们就需要使用std::shared_future< >, std::shared_future 实例是可拷贝的,所以多个对象可以引用同一关联“期望”的结果。
C++并发编程----并发和同步(《C++ Concurrency in Action》 读书笔记)_第1张图片也可以将std::future的所有权移动到std::shared_future,如下方式:

std::promise<int> p; 
std::future<int> f(p.get_future()); 
assert(f.valid()); // 1 "期望" f 是合法的 
std::shared_future<int> sf(std::move(f)); 
assert(!f.valid()); // 2 "期望" f 现在是不合法的 
assert(sf.valid()); // 3 sf 现在是合法的 

std::promise<std::string> p;
std::shared_future<std::string> sf(p.get_future()); // 1 隐式转移 所有权

也可以采用std::future的share()函数直接将实例化的对象转换为std::shared::future.

std::promise< std::map< SomeIndexType, SomeDataType, 
SomeComparator, 
SomeAllocator>::iterator> p; 
auto sf=p.get_future().share(); //sf的类型为shared_future

等待时间

有两种方式设定线程的等待时间:一种是“时延”的超时方式,另一种是“绝对”超时方 式。第一种方式,需要指定一段时间(例如,30毫秒);第二种方式,就是指定一个时间点(例 如,协调世界时[UTC]17:30:15.045987023,2011年11月30日)。
一般处理持续时间的变量以“_for”作为后缀,处理绝对时间的变量 以"_until"作为后缀。std::condition_variable 的两个成员函数wait_for()和wait_until()成员函数分别有两 个负载,这两个负载都与wait()成员函数的负载相关——其中一个负载只是等待信号触发,或 时间超期,亦或是一个虚假的唤醒,并且醒来时,会检查锁提供的谓词,并且只有在检查为 true时才会返回(这时条件变量的条件达成),或直接而超时。

时钟
时钟的当前时间可以通过调用静态成员函数now()从时钟类中获取, std::chrono::system_clock::now() 是将返回系统时钟的当前时间。特定的时间点类型可 以通过time_point的数据typedef成员来指定,所以some_clock::now()的类型就是 some_clock::time_point

时延
std::chrono::duration<> 函数模板能够对时延进行处理(线程库使 用到的所有C++时间处理工具,都在 std::chrono 命名空间内)。第一个模板参数是一个类型 表示(比如,int,long或double),第二个模板参数是制定部分,表示每一个单元所用秒数。例 如,当几分钟的时间要存在short类型中时,可以写成 std::chrono::duration> ,因为60秒是才是1分钟,所以第二个参数写成 std::ratio<60, 1> 。另一 方面,当需要将毫秒级计数存在double类型中时,可以写成 std::chrono::duration> ,因为1秒等于1000毫秒。

标准库在 std::chrono 命名空间内,为延时变量提供一系列预定义类型:nanoseconds[纳秒] , microseconds[微秒] , milliseconds[毫秒] , seconds[秒] , minutes[分]和hours[时]。而且他们之间可以相互转换。

std::chrono::milliseconds ms(54802); 
std::chrono::seconds s= 
std::chrono::duration_cast<std::chrono::seconds>(ms); 

这里的结果就是截断的,而不是进行了舍入,所以s最后的值将为54。

时延可以进行计算,例如,5*seconds(1)与seconds(5)或minutes(1)- seconds(55)一样。在时延中可以通过count()成员函数获得单位时间的数量。例 如, std::chrono::milliseconds(1234).count() 就是1234。

使用方式:

std::future<int> f=std::async(some_task); 
if(f.wait_for(std::chrono::milliseconds(35))==std::future_status 
::ready) 
do_something_with(f.get());

等待函数会返回一个状态值,来表示等待是超时,还是继续等待。在这种情况下,你可以等 待一个“期望”,所以当函数等待超时时,会返回 std::future_status::timeout ;当“期望”状态 改变,函数会返回 std::future_status::ready ;当“期望”的任务延迟了,函数会返 回 std::future_status::deferred 。

时间点
时钟的时间点可以用 std::chrono::time_point< > 的类型模板实例来表示,实例的第一个参数 用来指定所要使用的时钟,第二个函数参数用来表示时间的计量单位(特化 的 std::chrono::duration< > )。你可以通过 std::chrono::time_point<> 实例来加/减时延,来获得一个新的时间点,所 以 std::chrono::hight_resolution_clock::now() + std::chrono::nanoseconds(500) 将得到500纳 秒后的时间。
可以利用时间点计算某个代码块的运行时间。

auto start=std::chrono::high_resolution_clock::now(); 
do_something(); 
auto stop=std::chrono::high_resolution_clock::now(); 
std::cout<<do_something() took “ 
<<std::chrono::duration<double,std::chrono::seconds>(stop- 
start).count() 
<<” seconds”<<std::endl; 

具有超时功能函数

表4.1展示 了C++标准库中支持超时的函数。参数列表为“延时”(duration)必须是 std::duration<> 的实 例,并且列出为时间点(time_point)必须是 std::time_point<> 的实例。
C++并发编程----并发和同步(《C++ Concurrency in Action》 读书笔记)_第2张图片

你可能感兴趣的:(C++,并发,多线程,c++)