同步并发操作

条件变量

有时候线程之间是需要同步的,为了使线程同步,我们的方法有

1、设置一个共享标志,一个线程持续检查共享标志,一个线程更新标志,使用一个锁来保护这个标志

2、在方法一的基础上使用std::this_thread::sleep_for()让出CPU时间片,以降低不必要的时间损耗

 

#include <thread>  //std::this_thread::sleep_for
#include<chrono> //std::chrono::milliseconds
bool flag;  
std::mutex m;  
 
void wait_for_flag()  
{  
  std::unique_lock<std::mutex> lk(m);  //加锁保护共享标志
  while(!flag)  	//循环检测标志
  {  
    lk.unlock();  // 解锁互斥量
    std::this_thread::sleep_for(std::chrono::milliseconds(100));  //主动让出时间片
    lk.lock();   // 锁互斥量以便检测标志/标志为真后带锁执行任务
  }  
do_sometihng();
}

3、使用条件变量

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); 	//必须使用std::unique_lock,以使条件变量在wait()时可以解锁,在wait()返回时可以上锁,而std::lock_guard只能在析构时解锁
		data_cond.wait
			(lk,[]{return !data_queue.empty();}); //第二个参数可以是任意可调用对象或函数,此处是lambda表达式
		data_chunk data=data_queue.front();	//返回队头元素的引用
		data_queue.pop();
		lk.unlock(); 	//如果有多个处理数据的线程,这里的unlock()操作可以提高并发性
		process(data);
		if(is_last_chunk(data))
			break;
	}
}


关于为何使用条件变量需要使用一个锁的解释请看这里,关于为什么使用的锁是std::unique_lock而不是std::lock_guard我们需要看下对std::condition_variable::wait()的概述

 

unconditional (1)

void wait (unique_lock<mutex>& lck);

predicate (2)

template <class Predicate>

  void wait (unique_lock<mutex>& lck, Predicate pred);

 

 

The execution of the current thread (which shall have locked lck's mutex) is blocked until notified.

At the moment of blocking the thread, the function automatically calls lck.unlock(), allowing other locked threads to continue.

Once notified (explicitly, by some other thread), the function unblocks and calls lck.lock(), leaving lck in the same state as when the function was called. Then the function returns (notice that this last mutex locking may block again the thread before returning).

Generally, the function is notified to wake up by a call in another thread either to member notify_one or to member notify_all. But certain implementations may produce spurious wake-up calls without any of these functions being called. Therefore, users of this function shall ensure their condition for resumption is met.

 

If pred is specified (2), the function only blocks if pred returns false, and notifications can only unblock the thread when it becomes true (which is specially useful to check against spurious wake-up calls). This version (2) behaves as if implemented as:

while (!pred()) wait(lck);

 

当线程在条件变量wait()时,会调用锁的unlock()函数解锁以使其他线程得以安全完成任务并有机会通知当前线程,当条件变量收到通知wait()返回时又会上锁互斥量以便当前线程能安全完成任务,而利用RAII机制管理锁的两种机制中,只有std::unique_lock含有unlock()lock()成员函数,std::lock_guard仅有构造函数与析构函数

 

wait()的第二个参数可以是任意函数或可调用对象(返回值需转化为bool类型),当第二个参数返回false时,会继续wait()阻塞当前线程,当接收到其他线程的通知且第二个参数为true后才会解除阻塞状态

 

由上图可知,第二个参数返回false时会再次wait,只有返回为true与收到通知同时满足才能真正解除阻塞状态

 

std::condition_variable::notify_one 解除一个因等待条件变量而阻塞的线程,如果没有线程正在等待,什么也不做,如果正在等待的线程超过一个,则哪个线程被解除阻塞是不确定的

 

 

再来一个例子

// condition_variable::wait (with predicate)
#include <iostream>           // std::cout
#include <thread>             // std::thread, std::this_thread::yield
#include <mutex>              // std::mutex, std::unique_lock
#include <condition_variable> // std::condition_variable

std::mutex mtx;
std::condition_variable cv;

int cargo = 0;
bool shipment_available() { return cargo!=0; }

void consume(int n) {
	for (int i=0; i<n; ++i) {
		std::unique_lock<std::mutex> lck(mtx);
		cv.wait(lck, shipment_available);
		// consume:
		std::cout << cargo << '\n';
		cargo=0;
	}
}

int main()
{
	std::thread consumer_thread(consume, 10);

	// produce 10 items when needed:
	for (int i=0; i<10; ++i) {
		while (shipment_available()) std::this_thread::yield();
		std::unique_lock<std::mutex> lck(mtx);
		cargo = i+1;
		cv.notify_one();
	}

	consumer_thread.join();

	return 0;
}

例子中的std::this_thread::yield()请看这里

线程安全队列

#include <mutex>
#include <condition_variable>
#include <queue>
#include <memory>

template<typename T>
class threadsafe_queue  
{  
private:  
    mutable std::mutex mut; 	//必须声明为mutable
    std::queue<T> data_queue;  
    std::condition_variable data_cond;  
public:  
    threadsafe_queue()  
    {}  
    threadsafe_queue(threadsafe_queue const& other)  
    {  
		//锁住other对象中的锁,防止其他线程在当前对象拷贝构造时对other中的数据进行修改
        std::lock_guard<std::mutex> lk(other.mut); //实际上mutable作用于此,由于other是const引用而lock_guard的拷贝构造函数需求非const引用,mutable实际上是作用于other对象中的互斥量成员,以使const引用得以被非const引用所接受,此处还有一个不太理想的做法:使用const_cas将other的const属性去掉 
        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();});  //注意,此处的empty()必须是队列的成员函数,而不是当前类的公有成员函数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();  
    }  

    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();  
    }  
}; 


由上图可知,lock_guard的构造函数需求的是非const _Mutex引用

 

 

期望

期望std::future<>可以用来获取异步任务的结果,也就是说可以将它作为一种线程间同步的手段

有效的期望是与一种共享状态(或者说期望状态)相关联的,通过函数std::async<>()std::promise<>类成员函数get_future()std::packaged_task<>类成员函数get_future()都可以获取到一个有效的期望,使用std::future<>类的默认构造函数构造的期望对象是无效的,除非通过移动操作移动了一个有效期望对象

 

通过在一个有效的std::future<>上调用成员函数get()可以获取异步任务(即一个线程)的执行结果:获取异步任务的返回值或是期望对象存储的异常(如果异步任务产生了异常),但如果期望对象的共享状态不为就绪(ready)函数get()的调用将会导致调用线程发生阻塞,直到对象的共享状态变为ready

 

需要注意的是1、对同一std::future<>对象使用两次get()是不合理的,这将会引发一个异常

2、当期望状态不为ready时,就调用std::promise<>std::packaged_task<>的析构函数,期望对象将会存储一个std::future_error异常(此异常继承自标准异常的logic_error)

 

std::async<>()

联合使用期望与函数模板std::async<>()可以使我们以一种更为简单的方式获取异步任务的执行结果,这是相对于std::thread来说的,因为std::thread并不直接提供返回异步任务结果的机制,虽然我们可以通过引用或指针的方式返回,但使用这两种方式更改麻烦不说,还需要注意被指向或被引用对象的生命周期

 

unspecified policy (1)

template <class Fn, class... Args>

  future<typename result_of<Fn(Args...)>::type>

    async (Fn&& fn, Args&&... args);

specific policy (2)

template <class Fn, class... Args>

  future<typename result_of<Fn(Args...)>::type>

    async (launch policy, Fn&& fn, Args&&... args);

 

 

 

std::async<>()通过Fn开启一次异步任务,它返回一个std::future<>对象,通过返回的期望对象的成员函数get()就可以获取异步任务的执行结果了。当异步任务执行完毕后,期望对象的共享状态变为ready

Fn 可以是函数指针、或者任何拥可移动构造(即可由右值参数构造)的函数对象、可调用对象。Fn被调用后的返回值通过std::async<>()返回的期望通过get()获取,当Fn执行时抛出了异常,异常会被存储在期望当中,并在get()时再次抛出

值得注意的是,即使Fn的返回值类型为voidstd::async<>()的返回值也不能忽略,即应使用std::future<void>去获取std::async<>()的返回值

args 传递给Fn的参数,需要可移动构造,当Fn是成员函数的指针时,第二个参数提供类的对象(可以是直接传递,或是传递指向对象的指针,或是通过std::ref()传递引用)

policy 可以是std::launch::async表示启动一个新线程来调用Fn,可以是std::launch::deferred表示期望对象get()时才创建线程调用Fn,或者是std::launch::deferred | std::launch::async 表示自动选择启动策略

 

栗子

#include <iostream>       // std::cout
#include <future>         // std::async, std::future

// a non-optimized way of checking for prime numbers:
bool is_prime (int x) {
  std::cout <<"Calculating. Please, wait...\n";
  for (int i=2; i<x; ++i) if (x%i==0) return false;
  return true;
}

int main ()
{
  // call is_prime(313222313) asynchronously:
  std::future<bool> fut = std::async (is_prime,313222313);

  std::cout <<"Checking whether 313222313 is prime.\n";
  // ...

  bool ret = fut.get();      // waits for is_prime to return

  if (ret) std::cout <<"It is prime!\n";
  else std::cout <<"It is not prime.\n";

  return 0;
}

经测试,不管何种启动方式,只要没有对期望对象get()td::async就不会启动线程去创建异步任务(线程函数断点未命中),这个现象只与td::asyncstd::future<>对象有关

 

std::packaged_task<>

std::packaged_task<>可对一个函数、可调用对象进行封装,并可关联一个期望对象(使用成员函数get_future()可获得关联的期望对象),当std::packaged_task<>对象被调用时,它会调用封装的函数/可调用对象来开启一个异步任务,当异步任务完成时,其相关联的期望状态变为ready,调用期望的成员函数get()获取异步任务的执行结果或异步任务执行中发生的异常。

在构造std::packaged_task<>对象时,需要指定一个函数签名,函数签名的返回值类型用于标识期望对象的类型

#include <future>
#include <string>
#include <thread>
#include <iostream>
#include <utility>		//std::move

class Test
{
public:
int operator()(int a, int b)
	{
		return a + b;
	}
};

int main(){
	Test t;
	std::packaged_task<int(int, int)> task(t);

	auto f = task.get_future();					//获取期望必须在创建线程之前,否则运行出错
	std::thread th(std::move(task), 5, 5);		//必须使用std::move()或std::ref() 因为std::packaged_task<>的拷贝构造函数是删除函数  
	std::cout << f.get() << std::endl;

	if( th.joinable() )
		th.join();
	return 0;
}

 

再来一个例子

#include <deque>
#include <mutex>
#include <future>
#include <thread>
#include <utility>

std::mutex m;
std::deque<std::packaged_task<void()>> tasks;
bool gui_shutdown_message_received();
void get_and_process_gui_message();

void gui_thread() 
{
	while(!gui_shutdown_message_received()) 
	{
		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());			
		tasks.pop_front();
		task(); 								//调用std::packaged_task对象
	}
} 

std::thread gui_bg_thread(gui_thread);			//启动gui线程

//post任务的线程如果需要知道std::packaged_task<>的执行情况可以通期望去get,不需要则直接丢弃
template<typename Func>
std::future<void> post_task_for_gui_thread(Func f)	
{
	std::packaged_task<void()> task(f); 
	std::future<void> res=task.get_future(); 
	std::lock_guard<std::mutex> lk(m); 
	tasks.push_back(std::move(task)); 
	return res;
}

 

std::promise<>

一个std::promise<>对象存储了供关联的期望对象(可能在另一线程)读取的某一T类型的值,从而提供同步点,通过std::promise<>的成员函数get_future可以获得与之相关联的对象,当std::promise<>对象使用set_valueset_exception设置一个值或异常时,关联的期望对象变为ready状态并可通过get()获取设置的值或异常。

 

栗子

// promise example
#include <iostream>       // std::cout
#include <functional>	// std::ref
#include <thread>         // std::thread
#include <future>	// std::promise, std::future

void print_int (std::future<int>& fut) {
  int x = fut.get();
  std::cout <<"value: "<< x << '\n';
}

int main ()
{
  std::promise<int> prom;                    // create promise

  std::future<int> fut = prom.get_future();       // engagement with future

  std::thread th1 (print_int, std::ref(fut));        // send future to new thread

  prom.set_value (10);                         // fulfill promise
                                            // (synchronizes with getting the future)
  th1.join();
  return 0;
}

 

再来一个例子

#include <thread>
#include <iostream>
#include <future>

int main()
{
	std::promise<int> p;
	std::future<int> f = p.get_future();

	std::thread t([&p]{
		try {
			// code that may throw
			throw std::runtime_error("Example");
		}
		catch (...) {
			try {
				// store anything thrown in the promise
				p.set_exception(std::current_exception());
			}
			catch (...) {} // set_exception() may throw too
		}
	});

	try {
		std::cout << f.get();
	}
	catch (const std::exception& e) {
		std::cout <<"Exception from the thread: "<< e.what() << '\n';
	}
	t.join();
}

 

std::shared_future<>

std::shared_future<>std::future<>不同,后者是只移动的,虽然期望的共享状态可以在不同的std::future<>中转移,但它总是值属于一个对象,前者是可拷贝的,也就是说多个对象(在不同线程中)可以共享一个期望状态

当需要同步多个线程的时候,应该使用std::shared_future<>,并让每一个线程都有自己的那一份期望的拷贝,而不是传递引用

std::future<>std::shared_future<>之间的转换

可以使用std::move(),将共享状态从std::future<>转移到std::shared_future<>,也可以通过std::shared_future<>的移动构造函数转移

例子

std::promise<int> p;
std::future<int> f(p.get_future());			//移动构造后p无效
assert(f.valid());							// f有效
std::shared_future<int> sf(std::move(f));	//显示移动
//或std::shared_future<int> sf(f);			//移动构造后f无效
assert(!f.valid()); 							// f现在无效
assert(sf.valid()); 							// sf有效

 

通过std::future<>的成员函数shar()可以创建std::shared_future<>对象同时转移所有权

std::promise<int> p;
auto sf=p.get_future().share();

 

时间

C++库提供了两种时钟类,稳定时钟std::chrono::steady_clock类与不稳定时钟类std::chrono::system_clock

从字面意思上看,前者为稳定时钟,后者是系统时钟,一个时钟是否是稳定的要看时钟节拍是否均匀分布,且是否可调整,检测方法是根据类的静态成员变量is_steady,如果该变量为true,说明是稳定时钟

线程库使用到的所有C++时间工具据来自std::chrono命名空间内

 

 

时延

std::chrono::duration<>表示时延(时间段)

template<class _Rep, class _Period = ratio<1> > class duration;

其第一个参数表示表示一种数值类型,第二个参数为std::ratio类型,表示秒表示的时间单位

ratio<3600, 1>             hours
ratio<60, 1>               minutes
ratio<1, 1>                seconds
ratio<1, 1000>             microseconds
ratio<1, 1000000>          microseconds
ratio<1, 1000000000>       nanosecons

将几分钟存储在short中,写成std::chrono::duration<short,

std::ratio<60, 1>>将毫秒级存在double中,写成std::chrono::duration<double, std::ratio<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); //发生截断,结果为54s

std::chrono::duration<>对象是支持运算的,对象之间可以加减,且可以乘除一个整数以得到一个新的时间段

在时延中通过count()成员函数可以获得单位时间的数量,如std::chrono::milliseconds(1234).count()得到1234

 

支持时延处理的某些类的成员函数,他们以_for结尾

std::future<>的成员函数wait_for(),调用此函数的线程可能在一段指定的时间内等待(阻塞)直到期望对象的状态变为ready(以指定时间与变为ready中先者为准),函数返回值:

如果状态在指定的时间结束之前没有变为ready(任务完成或异常),返回std::future_status::timeout

如果状态在指定时间内变为ready,返回std::future_status::ready

如果期望任务被延迟,返回std::future_status::deferred

例子

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::chrono::time_point<>对象来表示

template<class _Clock, class _Duration = typename _Clock::duration> class time_point;

第一个参数需要指定一个时钟类型(std::chrono::steady_clockstd::chrono::system_clock),第二个参数表示时间的计量单位(特化的std::chrono::duration<>)

std::chrono::time_point<>对象支持运算,可以通过加/减时延对象来得到一个新的时间点

std::chrono::hight_resolution_clock::now() + std::chrono::nanoseconds(500)会得到500纳秒后的时间

std::chrono::time_point<>对象之间也可以相减(运算的对象间需要共享同一时钟类型)以得到两个时间点的时间差,也就是得到一个时间段

:

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;


支持时间点处理的某些类的成员函数,他们以_until结尾。

例子

#include <condition_variable>
#include <mutex>
#include <chrono>

std::condition_variable cv;
bool done;
std::mutex m;

bool wait_loop()
{
	auto const timeout= std::chrono::steady_clock::now()+
		std::chrono::milliseconds(500);
	std::unique_lock<std::mutex> lk(m);
	while(!done)
	{
			if(cv.wait_until(lk,timeout)==std::cv_status::timeout)
			break;
	} 
	return done;
}

 

支持时间处理方式函数

参数为duration必须为std::duration<> 对象,参数为time_point必须是std::time_point<> 对象

Class/Namespace

Functions

Return values

std::this_thread

sleep_for(duration)
sleep_until
(time_point)

N/A

std::condition_
variable or
std::condition_
variable_any

wait_for(lock,
duration
)
wait_until(lock,
time_point
)

std::cv_status::
timeout or
std::cv_status::
no_timeout

wait_for(lock,
duration,
predicate
)
wait_until(lock,
time_point,
predicate
)

bool—the return value of
the predicate when
awakened

 

std::timed_mutex or
std::recursive_
timed_mutex

try_lock_for
(duration)
try_lock_until
(time_point)

bool—true if the lock was
acquired, false otherwise

std::unique_
lock<TimedLockable>

unique_lock(lockable,
duration
)
unique_lock(lockable,
time_point
)

N/A—owns_lock() on the
newly constructed object;
returns true if the lock was
acquired, false otherwise

try_lock_for(duration)
try_lock_until
(time_point)

bool—true if the lock was
acquired, false otherwise

 

std::future<ValueType> or
std::shared_
future<ValueType>

wait_for(duration)
wait_until
(time_point)

std::future_status::
timeout if the wait timed
out, std::future_
status::ready if the
future is ready, or
std::future_status::
deferred if the future holds
a deferred function that
hasn’t yet started


 

 参考:

C++并发编程实战

 http://www.cplusplus.com/

 http://en.cppreference.com/w/

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值