C++并发编程学习——4.同步并发操作

等待事件或其他条件
(1)条件变量
当一个线程正等待另一个线程完成某一项任务,可以使用条件变量。当某个线程已经确定条件得到满足,他就会唤醒正在条件变量上等待的线程继续执行。
标准C++库提供了二个条件变量的实现: std::condition_variable std::condition_variable_any 。这二个实现都在 <condition_variable>头文件 中说明。两者都需要与一个互斥量一起才能工作(互斥量是为了同步);前者仅限于与std::mutex一起工作,而后者可以和任何满足最低标准的互斥量一起工作,从而加上了*_any*的后缀。通常使用condition_variable。
std::mutex mut;
std::queue<data_chunk> data_queue;  //队列用于二个线程间传递数据
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);  
		data_cond.notify_one();  //唤醒等待的线程
	}
}

void data_processing_thread()
{
	while (true)
	{
		std::unique_lock<std::mutex> lk(mut);  //使用的是unique_lock,因为中间要加锁解锁
		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;
	}
}
在一个线程中,std::condition_variable的实例上调用 notify_one()成员函数 通知一个等待中的线程 (如果有的话)。
另一个线程中,在std::condition_variable的实例上调用 wait()函数 阻塞等待条件变量的就绪。
wait()会去检查这些条件(通过调用所提供的lambda函数),当条件满足(lambda函数返回true)时返回。如果条件不满足(lambda函数返回false),wait()函数将解锁互斥量,并且将这个线程置于阻塞或等待状态。当准备数据的线程调用notify_one()通知条件变量时,处理数据的线程从睡眠状态中苏醒,重新获取互斥锁,并且对条件再次检查,在条件满足的情况下,从wait()返回并继续持有锁。当条件不满足时,线程将对互斥量解锁,并且重新开始等待。这就是为什么用std::unique_lock而不使用std::lock_guard——等待中的线程必须在等待期间解锁互斥量,并在这之后对互斥量再次上锁,而std::lock_guard没有这么灵活。如果互斥量在线程休眠期间保持锁住状态,准备数据的线程将无法锁住互斥量,也无法添加数据到队列中;同样的,等待线程也永远不会知道条件何时满足。

使用future等待一次性时间
如果线程等待其他线程的任务完成只等待一次,那么条件变量就不是最好的同步机制,这种场景下,使用future更加合适。
C++标准库将这种一次性事件成为 future 。C++中,有二种future: 唯一funture (unique funtures, std::future<> )和 共享future (share future, std::shared_future<> )。二个future都声明在 头文件<future> 中。在与数据无关的地方,可以使用std::future<void>与std::shared_future<void>的特化模板。虽然,我希望用于线程间的通讯,但是“future”对象本身并不提供同步访问。当多个线程需要访问一个独立“future”对象时,必须使用互斥量或类似同步机制对访问进行保护。
(1)带返回值的后台任务
std::thread并不提供直接接收返回值的机制。这里就需要 std::async函数模板 (也是在头文<future>中声明的)了。
当你不急需得到任务的结果时,你可以 使用std::async启动一个异步任务 。与std::thread对象等待的方式不同,std::async会返回一个std::future对象,这个对象持有最终计算出来的结果。当你需要这个值时,你只需要调用这个对象的 get()成员函数 并且会阻塞线程直到“future”状态为就绪为止;之后,返回计算结果。
int find_the_answer_to_ltuae();
void do_other_stuff();
int main()
{
	//使用async开启了一个异步任务,返回的future赋值给了the_answer
	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;//通过get()成员函数得到返回值。调用get会阻塞
}
与std::thread 做的方式一样,std::async允许你通过添加额外的调用参数,向函数传递额外的参数。 当第一个参数是一个指向成员函数的指针,第二个参数提供有这个函数成员类的具体对象 (不是直接的,就是通过指针,还可以包装在std::ref中), 剩余的参数可作为成员函数的参数传入 。否则,第二个和随后的参数将作为函数的参数,或作为指定可调用对象的第一个参数。就如std::thread,当参数为右值(rvalues)时,拷贝操作将使用移动的方式转移原始数据。
默认情况下,std::async是否立刻启动线程执行,还是等到get()调用后才执行 取决于具体的实现方式。当时可以在函数调用之前,向std::async传递额外的参数来人为改变std::async的执行方式。
额外的参数可以是 std::launch::defered 用来表明函数调用被延迟到wait()或get()函数调用时才执行。
或者参数可以是 std::launch::async  表明函数必须在其所在的独立线程上执行
std::launch::deferred | std::launch::async 是默认的执行方式,表明实现可以选择这两种方式的一种。
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();  //  调用延迟函数
(2)任务与future
std::packaged_task<> 将一个future绑定到一个函数或者可调用对象上。 当std::packaged_task<>对象被调用时,它就调用相关的函数或可调用对象让future就绪,将返回值作为关联数据存储。
std::packaged_task<>类模板的模板参数为 函数签名 ,例如 int(std::string&, double*),void()这样。 当你构造一个 std::packaged_task实例的时候,你必须传入一个函数或者可调用对象,它可以接受指定的参数并且返回指定的返回类型。类型不需严格匹配,可以有隐式转换。
指定的函数签名的返回类型指定了从 get_future() 成员函数返回的std::funture<>类型 ,而函数签名的参数列表指定了传入任务的函数调用运算符。
std::mutex m;
std::deque<std::packaged_task<void()> > tasks;//packaged_task队列

bool gui_shutdown_message_received();
void get_and_process_gui_message();

//任务执行线程
void gui_thread()  
{
	while (!gui_shutdown_message_received())  // 2
	{
		get_and_process_gui_message();  //
		std::packaged_task<void()> task;
		{
			std::lock_guard<std::mutex> lk(m);
			if (tasks.empty())  // 无任务,继续等待
				continue;
			task = std::move(tasks.front());  // 取出packaged_task任务
			tasks.pop_front();
		}
		task();  // 执行packaged_task的任务
	}
}

std::thread gui_bg_thread(gui_thread);
//发布任务,传出等待任务就绪的future,任务函数为f
template<typename Func>
std::future<void> post_task_for_gui_thread(Func f)
{
	std::packaged_task<void()> task(f);  //创建packaged_task实例
	std::future<void> res = task.get_future();  //将packaged_task实例的等待条件返回
	std::lock_guard<std::mutex> lk(m);  // 9
	tasks.push_back(std::move(task));  // 传入packaged_task队列
	return res;
}
这个例子中使用了std::packaged_task<void()>,它封装了一个接受零参数且返回void的函数或可调用对象(如果它返回了别的东西,则返回值会被丢弃),这是最简单的任务模板参数。
(3)使用std::promise
std::promise<T> 提供一种设置值(类型T)的方式,它可以在这之后通过相关联的std::future<T>对象进行读取。 一对std::promise/std::future使得等待中的线程可以阻塞future,同时提供数据的线程可以使用配对中的promise项,来设置相关的值使得future就绪。
可以通过 get_future()成员函数 来获取与一个给定的std::promise相关的std::future对象,就像是与std::packaged_task相关。 当“promise”的值已经设置完毕(使用 set_value() 成员函数),对应“future”的状态变为“就绪”,并且可用于检索已存储的值。当你在设置值之前销毁std::promise,将会存储一个异常。
//在这个例子中,你可以使用一对std::promise<bool>/std::future<bool>找出一块传出成功的数据块
void process_connections(connection_set& connections)
{
	while (!done(connections))  // 1
	{
		for (connection_iterator  //遍历每一个连接
			connection = connections.begin(), end = connections.end();
			connection != end;
		++connection)
		{
			if (connection->has_incoming_data())  // 有数据传来
			{	
				data_packet data = connection->incoming();
				std::promise<payload_type>& p =
					connection->get_promise(data.id);  //赋值给std::promise
				p.set_value(data.payload);	//设置std::promise的值,使得对应的future就绪
			}
			if (connection->has_outgoing_data())  // 有数据发送
			{
				outgoing_packet data =
					connection->top_of_outgoing_queue();
				connection->send(data.payload);
				data.promise.set_value(true);  //表示成功传输
			}
		}
	}
}
(4)为future保存异常
考虑下面的代码。square_root()函数可能产生一个异常。
double square_root(double x)
{
  if(x<0)
  {
    throw std::out_of_range(“x<0”);
  }
  return sqrt(x);
}
通过异步调用的方式运行:
std::future<double> f=std::async(square_root,-1);
double y=f.get();
在这样的情况下,即 如果作为std::async一部分的函数调用引发了异常,该异常会存储在future中,代替所存储的值,并且对get()的调用会重新引发所存储的异常。 同样对std::packaged_task中的异常也会发生在调用get()后。
当然,通过函数的显式调用,std::promise也能提供同样的功能。当你希望存入的是一个异常而非一个数值时,你就需要调用 set_exception()成员函数 ,而非set_value()。这通常是用在一个catch块中,并作为算法的一部分,为了捕获异常,使用异常填充promise:
extern std::promise<double> some_promise;
try
{
  some_promise.set_value(calculate_value());
}
catch(...)
{
  some_promise.set_exception(std::current_exception());
}
这里使用了 std::current_exception() 来检索抛出的异常;可用 std::copy_exception() 作为一个替换方案,std::copy_exception()会直接存储一个新的异常而不抛出:
some_promise.set_exception(std::copy_exception(std::logic_error("foo ")));
(5)多个线程的等待
如果需要多个线程等待同一个事件,则需要使用 std::shared_future
std::future模型独享同步结果的所有权,因此future只是可移动的(move),不可复制,在一个线程get()后,就没有值可以获取了。
而std::shared_future实例是可拷贝的,所以多个对象可以引用同一关联“期望”的结果。
因为std::shared_future是可以拷贝的,所以在多个线程等待同一个事件的情境下,可以让每一个线程拥有自己的std::shared_future实例,等待同一个事件。
std::shared_future实例可以通过std::future实例来构造,但是std::future是无法共享的,所以要通过move()转移future的所有权。
std::promise<int> p;
std::future<int> f(p.get_future());
assert(f.valid());  // “future” f 是合法的
std::shared_future<int> sf(std::move(f));
assert(!f.valid());  // “future”f 现在是不合法的,f为空future
assert(sf.valid());  // sf 现在是合法的
因为右值转换是隐式的,所以可以直接传入get_future()。
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();

限制等待时间
(1)时钟
对于C++标准库来说,时钟就是时间信息源。特别是,时钟是一个类,提供了四种不同的信息:
1.现在时间 2.时间类型 3.时钟节拍 4.通过时钟节拍的分布,判断时钟是否稳定
时钟的当前时间可以通过调用 静态成员函数now() 从时钟类中获取;例如, std::chrono::system_clock::now() 是将返回系统时钟的当前时间。特定的时间点类型可以通过time_point的数据typedef成员来指定,所以some_clock::now()的类型就是some_clock::time_point。
时钟节拍被指定为1/x(x在不同硬件上有不同的值)秒,这是由时间周期所决定——一个时钟一秒有25个节拍,因此一个周期为 std::ratio<1, 25> ,当一个时钟的时钟节拍每2.5秒一次,周期就可以表示为std::ratio<5, 2>。
如果一个时钟以均匀速率计时且不能被调整,则该时钟被称为匀速时钟。如果时钟是匀速的,则 时钟类的is_steady静态数据成员为true,反之为false 。 通常情况下,td::chrono::system_clock是不匀速的。 但匀速时钟对于计算超时是非常重要的,因此C++标准库提供形式为 std::chrono::steady_clock
C++标准库提供的其他时钟可表示为std::chrono::system_clock(在上面已经提到过),它代表了系统时钟的“实际时间”,并且提供了函数可将时间点转化为time_t类型的值; std::chrono::high_resolution_clock  可能是标准库中提供的具有最小节拍周期(因此具有最高的精度[分辨率])的时钟。它实际上是typedef的另一种时钟,这些时钟和其他与时间相关的工具,都被定义在 <chrono>库头文件 中。
(2)时间段
std::chrono::duration<>函数模板 能够对时间段进行处理(线程库使用到的所有C++时间处理工具,都在std::chrono命名空间内)。 第一个模板参数是一个类型表示(比如,int,long或double),第二个模板参数是制定部分,表示每一个单元所用秒数 。例如,当几分钟的时间要存在short类型中时,可以写成std::chrono::duration<short, std::ratio<60, 1>>
标准库在std::chrono命名空间内,为时间段变量提供一系列预定义类型:nanoseconds[纳秒] , microseconds[微秒] , milliseconds[毫秒] , seconds[秒] , minutes[分]和hours[时]。
当不要求截断值的情况下(时转换成秒是没问题,但是秒转换成时就不行)时间段的转换是隐式的。显示转换可以由 std::chrono::duration_cast<> 来完成。
时间段支持计算,所以你能够对两个时延变量进行加减,或者是对一个时延变量乘除一个常数(模板的第一个参数)来获得一个新延迟变量。
基于时间段的等待可由std::chrono::duration<>来完成。例如,你等待一个“future”状态变为就绪已经35毫秒:
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());
(3)时间点
时钟的时间点是通过 std::chrono::time_point<>类模板 的实例来表示的, 它以第一个模板参数表示其参看的时钟,并且以第二个模板参数表示时间的计量单位 (std::chrono::duration<>的特化)。时间点是基于unix时间戳的一个时间的长度,通常unix时间戳表示1970年1月1日。
你可以通过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;
如你期望的那样,后缀为_unitl的(等待函数的)变量会使用时间点。通常是使用某些时钟的::now()(程序中一个固定的时间点)作为偏移,虽然时间点与系统时钟有关,可以使用std::chrono::system_clock::to_time_point() 静态成员函数,在用户可视时间点上进行调度操作。例如,当你有一个对多等待500毫秒的,且与条件变量相关的事件,你可以参考如下代码:
std::condition_variable cv;
bool done;
std::mutex m;

bool wait_loop()
{
  auto const timeout= std::chrono::steady_clock::now()+
      std::chrono::milliseconds(500);	//实例化一个时间点,通过now()的当前时间
  std::unique_lock<std::mutex> lk(m);
  while(!done)
  {
    if(cv.wait_until(lk,timeout)==std::cv_status::timeout)	//wait_until中有了等待时间点
      break;
  }
  return done;
}
(4)超时函数
使用超时的最简单方式就是,对一个特定线程添加一个延迟处理;当这个线程无所事事时,就不会占用可供其他线程处理的时间。你在4.1节中看过一个例子,你循环检查“done”标志。两个处理函数分别是 std::this_thread::sleep_for()和std::this_thread::sleep_until() 。他们的工作就像一个简单的闹钟:当线程因为指定时延而进入睡眠时,可使用sleep_for()唤醒;或因指定时间点睡眠的,可使用sleep_until唤醒。
超时甚至可以在尝试获取一个互斥锁时(当互斥量支持超时时)使用。std::mutex和std::recursive_mutex都不支持超时锁,但是std::timed_mutex和std::recursive_timed_mutex支持。这两种类型也有try_lock_for()和try_lock_until()成员函数,可以在一段时期内尝试,或在指定时间点前获取互斥锁。表4.1展示了C++标准库中支持超时的函数。 参数列表为“延时”(duration)必须是std::duration<>的实例,并且列出为时间点(time_point)必须是std::time_point<>的实例。


  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值