• 第4章 同步并发操作


    4.1 等待事件或其他条件

    我们在配合做事情的时候,总是应该在前提条件完成的情况下,才会开始进行下一步操作,所以,我们可以选择以下几种方式:

    1.实时查看前提条件的完成情况。这样可以及时的进行下一步任务,但是,这样的操作是一件十分浪费时间的事情,在这期间本来还可以做其他事情,现在由于实时关注前提条件的完成情况,不得不一直专注于这件事情。

    2.每隔一定时间查看前提条件的完成情况。这样虽然我们可以在这些间断的时间用来做一些其他的事情,但是,这种查询方式不利于前提条件完成后及时进行下一步的任务。

    3.让完成前条件的人,在完成该任务后及时通知你。这种方法可以避免中间的时间浪费,也可以及时的进行下一步的任务,是一件十分高效的策略。

    故C++标注库中的条件变量等待条件正是第三种情况。

    4.1.1 用条件变量等待条件

    C++标准库中提供了两个条件变量的实现:std::condition_variable和std::condition_variable_any。前者std::condition_variable只能与std::mutex一起配合使用,但是后者std::condition_variable_any则可以与复核成为类似互斥元的最低标准的任何东西一起工作,但同时在大小、性能和操作系统资源方面有着额外的代价。下面展示std::condition_variable的使用。

    #include<iostream>
    #include<condition_variable>
    #include<mutex>
    #include<chrono>
    #include<thread>
    
    using namespace std;
    
    mutex m;
    condition_variable cv;
    bool flg;
    
    
    void pre_print(){
    	
    	cout<<"pre_print"<<endl;
    	flg = true;
    	cv.notify_one();
    }
    
    void after_print(){
    	unique_lock<mutex> lg(m);
    	cv.wait(lg, []{return flg;});
    	cout<<"after_print"<<endl;
    }
    
    
    int main(){
    	flg = false;
    	thread t1(after_print), t2(pre_print);
    	t1.detach();
    	
    	//std::this_thread::sleep_for(std::chrono::seconds(3));
    
    	t2.join();
    	cout<<"finish_process"<<endl;
    	return 0;
    }

    4.1.2 使用条件变量建立一个线程安全队列

    为queue书写一个安全的支持多线程并发的数据结构--safe_queue。

    相关代码有时间再补

    4.2 使用future等待一次事件

    比如调用函数得到返回时,有时候这种事情只发生一次,在单线程中很容易解决,在多线程中我们要借助C++标准库中的future来解决,虽然future支持多线程通讯,但这种工具不支持同步访问,如果多线程需要同步访问同一个future应该使用互斥元等来解决。

    在C++中关于future有两种:

    1.future :future实例时仅有的一个指向其关联事件的实例。

    2.shared_future :多个shared_future的实例可以指向同一个事件。

    4.2.1 从后台任务中返回值

    在使用多线程时,我们会想到thread,但是这种机制没有提供接收返回值的特性,所以我们现在要学习另一个用于多线程的函数模板,std::async,这个函数的使用方式和thread相似,但是结果返回一个std::future的对象,而不是一个thread对象让你等待,这个future对象最终持有该多线程函数的返回值,最终只需要在future对象上使用get函数即可得到函数返回值。例如

    #include<thread>
    #include<future>
    #include<iostream>
    #include<vector>
    using namespace std;
    
    vector<int> add(){
    	vector<int> ans;
    	for(int i = 0; i < 10; ++i)
    		ans.push_back(i);
    	return ans;
    }
    
    
    int main(){
    	future<vector<int>> res = async(add);
    	vector<int> ans = res.get();
    	for(auto i: ans)
    		cout<<i<<' ';
    	cout<<endl;
    	return 0;
    }

    虽然上面的例子用单线程也能解决,但是上面的例子只是为说明该函数的使用方法。async也可以指定多线程调用的方法,async(std::launch::deferred表明在调用中一直延迟到使用future.wait()或者future.get()为止,但是async(std::launch::async)则表明必须在他自己的线程上运行。(注:根据课本中的几个字在wait()或者get()中运行,我感觉应该是在this_thread中运行,只是在wait或者get函数中再次启动该线程,如有不对希望高手指正)。

    4.2.2 将任务与future相关联

    C++标准库中有个类std::package_task<>可以将future与绑定到一个函数或者可调用对象上,当std::package_task<>被调用时,绑定的函数或者可调用对象也被调用,并且使future进入就绪状态,将返回值作为关联数据存储。当std::package_task<>作为可调用对象时被调用时,提供给函数调用运算符的参数被传给所包含的函数,并且将结果作为异步结果返回,存储在由get_future()获得的std::future中。使用书中示例如下

    #include<deque>
    #include<mutex>
    #include<future>
    #include<thread>
    #include<utility>
    
    using namespace std;
    
    mutex m;
    deque<package_task<void()>> tasks;
    
    bool gui_shutdown_message_received();
    void get_and_process_gui_message();
    
    void gui_thread(){
    	while(!gui_shutdown_message_received()){
    		std::package_task<void()> task;
    		{
    			lock_guard<mutex> lg(m);
    			if(tasks.empty())
    				continue;
    			task = std::move(task.front());
    			task.pop_front();
    		}
    		task();
    	}		
    }	
    
    std::thread gui_bg_thread(gui_thread);
    template<typename Func>
    std::future<void> post_task_for_gui_thread(Func f){
    		package_task<void()> task(f);
    		future<void> res = task.get_future();
    		lock_guard<mutex> lg(m);
    		tasks.push_back(std::move(task));
    		return res;
    } 

    4.2.3 生成std::promise

    在我看来std::promise是一种用于线程间通讯的工具,std::promise<T>提供一种设置值(类型T)的方式,他可以在这之后通过相关联的std::future<T>对象进行读取。一对std::promise/std::future为这一设施提供了一种可能的机制,等待中线程可以阻塞future,同时提供数据的线程可以使用配对中的promise向,来设置相关的值并使future就绪。

    同时你可以使用get_future()成员函数来获取与给定的std::promise相关的std::future对象。当设置完promise的值(使用set_value()成员函数),,future就会变为就绪,,并且可以用来获取锁存储的值。如果销毁std::promise时未设置值,则将存入一个异常。示例如下

    #include<iostream>
    #include<future>
    #include<condition_variable>
    #include<chrono>
    #include<thread>
    #include<mutex>
    
    using namespace std;
    
    promise<int> ps;
    mutex m;
    condition_variable cv;
    bool flg;
    
    void get_and_show_val(){
    	int t;
    	
    	unique_lock<mutex> ul(m);
    	cv.wait(ul, [](){return flg;});
    	flg = false;
    	future<int> ans = ps.get_future();
    	t = ans.get();
    	cout<<t<<endl;
    
    }
    
    void set_val(){
    
    
    	unique_lock<mutex> ul(m);
    	ps.set_value(3);
    	flg = true;
    	cv.notify_one();
    	ul.unlock();
    	this_thread::sleep_for(chrono::seconds(3));
    		
    
    }
    
    int main(){
    	flg = false;
    	thread t1(get_and_show_val), t2(set_val);
    	t1.detach();
    	t2.join();
    	return 0;
    }		 

    根据https://zh.cppreference.com/w/cpp/thread/promise 说明该promise只应该使用一次。

    4.2.4 为future保存异常

    当然也可以使用promise,但是在设置值的时候使用set_exception(),而不是set_value()。

    std::promise<double> some_promise;
    try{
    	some_promise.set_val(calc_val());
    }
    catch{
    	some_promise.set_exception(std::current_exception());
    }

    4.2.5 等待自多个线程

    当多个线程访问std::furture对象而不进行额外同步时,就会出现数据竞争和未定义的行为,这是有意为之的,future模型统一异步结果的所有权,同时get()的单发性质是的这样的并发访问没有意义——只有一个线程可以获取值,因为在首次调用get()之后,就没有任何可获得的值留下了,但是这时我们可以使用std::shared_future使std::future交出所有权达到可以复制的效果,有几种方式可以使用shared_future

    future<int> f;
    share_future<int> sf(f);
    //或者
    // share_future<int> sf = f.share();

    4.3 有时间的等待

    关于超时有两种:

    1.基于时间段:等待一个指定的时间长度。函数形式为_for

    2.基于时间点(绝对超时):等到指定的时间点。函数形式为_until

    4.3.1 时钟

    时钟是时间信息的来源,具体来说,时钟提供以下四个不同部分信息的类。

    1.现在时间

    2.用来表示从时钟获取到的时间值的类型

    3.时钟的节拍周期

    4.时钟是否以均匀的速率进行计时,决定其是否为匀速(steady)时钟

    对于具体的某个时间点类型是time_point成员的typedef

    4.3.2 时间段

    时间段是时间支持中的最简单部分,他们是由std::chrono::duration<>来进行处理的,第一个模板代表类型,第二个模板是分数,代表每个时间段表示多少秒,short存储的几分钟表示为std::chrono::duration<short, std::radio<60,1>>,以double存储的毫秒数则表示为std::chrono::duration<double, std::radio<1,1000>>表示1毫秒为1/1000秒

    在无需截断的场合下,时间段之间的转换时隐式的(因此将小时转换为秒是可以的,但将秒转换为小时则不然),显式转换可以通过std::chrono::duration_cast<>来转换。

    4.3.3 时间点

    时间点是通过std::chrono::time_point<>类模板来管理的。

    4.3.4 接收超时的函数

    类/命名空间 函数 返回值
    std::this_thread命名空间

     sleep_for(duration)

    sleep_until(time_point)

    不可用

    std::condition_variable或者

    std::condition_variable_any

    wait_for(lock,duration)

    wait_until(lock, time_point) 

    std::cv_status::timeout或

    std::cv_status::no_timeout

     

    wait_for(lock, duration, predication)

    wait_unitl(lock, time_point, predition) 

    bool——当唤醒时predicate的返回值

    std::time_mutex或者

    std::recursize_time_mutex

    try_lock_for(duration)

    try_lock_until(time_point) 

    bool——true如果获得了锁,否则false
    std::unique_lock<TimedLockable>

    unique_lock(lockable, duration)

    unique_lokc(lockable, time_point) 

    不可用——owns_lock()在新构造的对象上,如或获得了锁返回true,否则false
     

    try_lock_for(duration)

    try_lock_until(time_point) 

    bool——如果获得了锁,否则返回false

    std::future<ValueType>或者

    std::shared_future<ValueType>

    wait_for(duration)

    wait_until(time_point) 

    std::future_status::timeout如果等待超时,

    std::future_status::ready如果future就绪或

    std::future_status::deferred如果future持有的延迟函数还没有开始

    4.4 使用操作同步简化代码

    4.4.1 带有future的函数式编程

    4.4.2 具有消息传递的同步操作

  • 相关阅读:
    JavaWeb——Servlet开发3
    8.3.3
    8.3.2
    8.3
    8.2
    8.1
    7.3.9
    7.3.8.3
    7.3.8.2
    7.3.8.1
  • 原文地址:https://www.cnblogs.com/hebust-fengyu/p/12087822.html
Copyright © 2020-2023  润新知