目录
std::timed_mutex 、std::recursive_timed_mutex
使用unique_lock和condition_variable 实现的同步队列:
std::future、std:promise和 std::package_task之间的关系
创建线程:
使用std::thread创建线程,只需要提供线程函数或者线程对象。并且可以同时指定线程函数的参数。
void func() { } int main() { std::thread t(func); t.join(); return 0; }
join函数会阻塞线程,直到线程函数执行结束,如果线程函数有返回值,返回值将被忽略。
detach()方法,将线程和线程对象分离,线程不被阻塞执行。
void func() { } int main() { std::thread t(func); t.detach(); return 0; }
注意:
detach之后就无法在和线程发生联系,不能detach之后在通过join来等待线程执行完。
线程接收任意参数:
void func(int i) { cout<<i<<endl; } int main() { std::thread t(func, 1); t.join(); return 0; }
线程不能复制,可以移动:
void func() { } int main() { std::thread t(func); std::thread t1(std::move(t)); t.join(); t1.join(); return 0; }
线程被移动之后,线程对象t将不再代表任何线程。
通过std::bind或者lambda表达式来创建线程:
void func(int i) { } int main() { std::thread t(std::bind(func, 1)); std::thread t1([](int i, int b){ }, 1, 2); t.join(); t1.join(); return 0; }
用法:
1、获取当前信息
获取当前线程ID,CPU核心数量。
void func() { } int main() { std::thread t(func); cout<<t.get_id()<<endl; //获取当前线程ID //获取cpu核数,如果获取失败返回0 cout<<std::thread::hardware_conncurrency()<<endl; t.join(); return 0; }
2、线程休眠
当前休眠一段时间
void func() { //休眠3s std::thread::sleep_for(std::chrono::seconds(3)); } int main() { std::thread t(func); t.join(); return 0; }
互斥量
互斥量是一种同步原语,是一种线程同步的手段,用来保护多线程同时访问的共享数据。
std::mutex : 独占的互斥量,不能递归使用。
std::timed_mutex : 带超时的独占互斥量,不能递归使用。
std::recursive_mutex :递归互斥量,不带超时功能。
std::recursive_timed_mutex :带超时的递归互斥量。
std::mutex
使用lock()加锁,unlock()解除。try_lock()尝试锁定互斥量,成功返回true,失败返回false,try_lock()是非阻塞的。
使用lock_guard可以简化lock和unlock的写法,更加安全,lock_guard在构造时(分配资源)会自动锁定互斥量,退出作用域后进行析构时(释放资源)会自动解锁,保证互斥量的正确操作,避免忘记unlock。std::mutex g_lock; std::lock_guard<std::mutex> locker(g_lock);//出作用域之后自动解锁 std::this_thread::get_id()//获取线程id
std::recursive_mutex
递归锁允许同一线程多次获得该互斥锁,可以用来解决同一线程需要多次获取互斥量时死锁的问题。
一个线程多次获取同一个互斥量时会发生死锁。class Complex { std::mutex mutex; int i; Complex():i(0) {} void mul(int x) { std::lock_guard<std::mutex> 1ock(mutex) ; i *= x; } void div(int x) { std::1ock_guard<std::mutex> lock(mutex) ; i /= x; } void both(int x,int y) { std::lock_guard<std::mutex> 1ock(mutex) ; mul(x) ; div(y) ; } }; int main() { Complex complex; complex.both(32, 23) ; return 0; } /* 会发生死锁,在调用both时获取互斥量,之后调用mul又要获取相同的互斥量, 但这个互斥量已经被当前的线程获取,无法释放,这时就会发生死锁。 */
解决此办法就是用递归锁,允许在同一个线程中多次获得互斥量。
class Complex { std::recursive_mutex mutex; int i; Complex():i(0) {} void mul(int x) { std::lock_guard<std::recursive_mutex> 1ock(mutex) ; i *= x; } void div(int x) { std::1ock_guard<std::recursive_mutex> lock(mutex) ; i /= x; } void both(int x,int y) { std::lock_guard<std::recursive_mutex> 1ock(mutex) ; mul(x) ; div(y) ; } }; int main() { Complex complex; complex.both(32,23) ; return 0; }
注意:尽量不要使用递归锁,原因:
1)需要用到递归锁定的多线程互斥处理往往本身就是可以简化的,允许递归互斥很容 易放纵复 杂逻辑的产生,从而导致一些多线程同步引起的晦涩问题。
2)递归锁比起非递归锁,效率会低一些。
3)递归锁虽然允许同一个线程多次获得同一个互斥量,可重复获得的最大次数并未具体说明,一 旦超过一定次数,再对lock进行调用就会抛出std::system错误。
std::timed_mutex 、std::recursive_timed_mutex
std::timed_mutex是超时的独占锁,std::recursive_timed_mutex是超时的递归锁,主要用在获取锁时增加超时等待功能,因为有时不知道获取锁需要多久,为了不至于一直在等待获取互斥量,就设置一个等待超时时间,在超时后还可以做其他的事情。
std::timed_mutex比std:mutex多了两个超时获取锁的接口: try_lock_for和try_lock_until,这两个接口是用来设置获取互斥量的超时时间,使用时可以用一个while循环去不断地获取互斥量。
std:timed_ mutex 的基本用法:
std::timed_mutex mutex; void work() { std::chrono::milliseconds timeout(100); while(true) { if(mutex.try_lock_for(timeout)) { std::chrono::milliseconds sleepDuration(250); std::this_thread::sleep_for(sleepDuration); mutex.unlock(); std::this_thread::sleep_for(sleepDuration); } else { std::chrono::milliseconds sleepDuration(100); std::this_thread::sleep_for(sleepDuration); } } } int main() { std::thread t1(work); std::thread t2(work); t1.join(); t2.join(); return 0; }
通过while不断的获取超时锁,如果超时还没获取到锁时,就休眠100毫秒,再尝试获取超时锁。
条件变量
条件变量用于等待同步机制,能阻塞一个或多个线程,直到收到另一个线程发出的通知或者超时,才会唤醒当前阻塞的线程。条件变量需要和互斥量配合使用。
condition_variable,配合std::unique_lock<std::mutex>进行wait操作(性能更好)
condition_variable_any,和任意带有lock、unlock语义的mutex搭配使用,比较灵活,效率比condition_variable差一些。
使用过程:
1、拥有条件变量的线程获取互斥量。
2、循环检查某个条件,如果条件不满足,则阻塞直到条件满足;如果条件满足,则向下执行。
3、某个线程满足条件执行完之后调用notify_one或notify_all唤醒一个或者所有的等待线程。
使用条件变量实现一个同步队列,同步队列作为一个线程安全的数据共享区,常用于线程之间数据读取。
同步队列实现:
#include <condition_variable> #include <lsit> #include <mutex> #include <thread> template<typename T> class SyncQueue { bool isFull() const { return m_queue.size() == m_maxSize; } bool isEmpty() const { return m_queue.empty; } public: SyncQueue(int maxSize): m_maxSize(maxSize){} //添加数据 void put(const T& x) { std::lock_guard<std::mutex> locker(m_mutex); while(isFull()) { cout<<"缓冲区满了,需要等待..."<<endl; m_notFull.wait(m_nutex); } m_queue.push_back(x); m_notEmpty.notify_one(); } //取数据 void take(T& x) { std::lock_guard<std::mutex> locker(m_mutex); while(isEmpty()) { cout<<"缓冲区空了,需要等待..."<<endl; m_notEmpty.wait(m_nutex); } x = m_queue.front(); m_queue.pop_front(); m_notFull.notify_one(); } bool empty() { std::lock_guard<std::mutex> locker(m_mutex); return m_queue.empty(); } bool full() { std::lock_guard<std::mutex> locker(m_mutex); return m_queue.size == m_maxSize; } size_t size() { std::lock_guard<std::mutex> locker(m_mutex); return m_queue.size(); } int count() { return m_queue.size(); } private: std::list<T> m_queue; //缓冲区 std::mutex m_mutex; //互斥量和条件变量结合使用 std::condition_variable_any m_notEmpty; //不为空的条件变量 std::condition_variable_any m_notFull; //没有满的条件变量 int m_maxSize; //同步队列最大的size }
这个同步队列在没有满的情况下可以插人数据,如果满了,则会调用m_notFull阻塞等待,待消费线程取出数据之后发一个未满的通知,然后前面阻塞的线程就会被唤醒继续往下执行;如果队列为空,就不能取数据,会调用m_notEmpty条件变量阻塞,等待插入数据的线程发出不为空的通知时,才能继续往下执行。
使用lock_guard,它利用了RAII机制可以保证安全释放mutex。
两种写法效果是一样的,但是后者更简洁,条件变量会先检查判断式是否满足条件,如果满足条件,则重新获取mutex,然后结束wait,继续往下执行;如果不满足条件,则释放mutex,然后将线程置为waiting状态,继续等待。wait函数中会释放mutex,而lock_guard还拥有mutex,只会在出作用域之后才释放mutex。wait释放锁之后会处于等待状态,再被notify_one或者notify_all唤醒之后会先获取mutex,这个相当于lock_guard的mutex在释放之后又获取到了mutex。unique_lock可以随时释放锁。
std::lock_guard<std::mutex> locker(m_mutex); while(isFull()) { cout<<"缓冲区满了,需要等待..."<<endl; m_notFull.wait(m_nutex); } 可以改写: std::lock_guard<std::mutex> locker(m_mutex); m_notFull.wait(locker, [this]{ return !isFull();});
把 std::lock_guard改成std::unique_lock,把std::condition_variable_any改为std:condition_variable,并且用等待一个判断式的方法来实现一个简单的线程池。
使用unique_lock和condition_variable 实现的同步队列:
#include <condition_variable> #include <lsit> #include <mutex> #include <thread> template<typename T> class SyncQueue { public: SyncQueue(){} //添加数据 void put(const T& x) { std::lock_guard<std::mutex> locker(m_mutex); m_queue.push_back(x); m_notEmpty.notify_one(); } //取数据 void take(T& x) { std::unique_lock<std::mutex> locker(m_mutex); m_notEmpty.wait(locker, [this]{ return !m_queue.empty();}); x = m_queue.front(); m_queue.pop_front(); } bool empty() { std::lock_guard<std::mutex> locker(m_mutex); return m_queue.empty(); } size_t size() { std::lock_guard<std::mutex> locker(m_mutex); return m_queue.size(); } private: std::list<T> m_queue; //缓冲区 std::mutex m_mutex; //互斥量和条件变量结合使用 std::condition_variabl m_notEmpty; //不为空的条件变量 }
用unique_lock代替lock _guard,使语义更加准确,用性能更好的condition_variable替代condition_variable_any,对程序加以优化,仍然可以用condition_variable_any。执行wait时不再通过while循环来判断,而是通过lambda表达式来判断,写法上更简洁。
原子变量
原子类型:std::atomic<T>
使用原子变量就不需要使用互斥量来保护该变量。可以更方便的实现线程保护。
计数器:
//使用mutex struct Counter { int value; std::mutex mutex; void increment() { std::lock_guard<std::mutex> lock(mutex); ++value; } void decrement() { std::lock_guard<std::mutex> lock(mutex); --value; } int get() { return value; } }; //使用原子变量 struct Counter { std::atomic<int> value; void increment() { ++value; } void decrement() { --value; } int get() { return value.load(); } };
call_once/once_flag 的使用:
为了保证在多线程环境中某个函数仅被调用一次,比如,需要初始化某个对象,而这个对象只能初始化一次时,就可以用std::call_once来保证函数在多线程环境中只被调用一次。使用std:call_once时,需要一个once_flag作为call_once的入参,它的用法比较简单。
std::once_flag flag; void do_once() { std::call_once(flag, [](){ cout<< " called once" <<endl; }); } int main() { std::thread t1(do_once); std::thread t2(do_once); std::thread t3(do_once); t1.join(); t2.join(); t3.join(); }
异步操作
C++11中异步操作相关的类,主要有std::future、std:promise和 std::package_task。std:future作为异步结果的传输通道,可以很方便地获取线程函数的返回值;
std::promise用来包装一个值,将数据和 future绑定起来,方便线程赋值;
std::package_task用来包装一个可调用对象,将函数和 future绑定起来,以便异步调用。
1、获取线程函数返回值的类:std::future
thread 库提供了future用来访问异步操作的结果,因为一个异步操作的结果不能马上获取,只能在未来某个时候从某个地方获取,这个异步操作的结果是一个未来的期待值,所以被称为future,future提供了获取异步操作结果的通道。可以以同步等待的方式来获取结果,可以通过查询future的状态( future_status)来获取异步操作的结果。future_status有如下3种状态:
deferred,异步操作还没开始。
ready,异步操作已经完成。
timeout,异步操作超时。std::future_status status; do { status = future.wait_for(std::chrono::seconds(1)); if( status == std::future_status::deferred) { cout<<"deferred"<<endl; } else if( status == std::future_status::timeout) { cout<<"timeout"<<endl; } else if( status == std::future_status::ready) { cout<<"ready"<<endl; } }while(status != std::future_status::ready);
获取future的结果有3种方式:get、wait、wait_for,
get:等待异步操作结束并返回结果
wait:只是等待异步操作完成
wait_for:超时等待返回结果
2、协助线程赋值的类:std::promise
std:promise将数据和 future绑定起来,为获取线程函数中的某个值提供便利,在线程函数中为外面传进来的promise赋值,在线程函数执行完成之后就可以通过promis的future获取该值了。注意:取值是间接地通过promise内部提供的future来获取的。
std::promise的基本用法如下:
std::promise<int> pr; std::thread t( [](std::promise<int> &p) { p.set_value_at_thread_exit(10); }, std::ref(pr)); ; std::future<int> f = pr.get_future(); auto r = f.get();
3、可调用对象的包装类std::package_task
std:packaged_task包装了一个可调用对象的包装类(如function、lambda expression、bind expression和 another function object),将函数和future绑定起来,以便异步调用,它和std::promise在某种程度上有点像,promise保存了一个共享状态的值,而 packaged_task保存的是一个函数。
std:packaged_task的基本用法如下:
std::package_task<int> task( [](){ return 2; } ); std::thread t(std::ref(task)); std::future<int> f = task.get_future(); auto r = f.get();
std::future、std:promise和 std::package_task之间的关系
future被promise和 package_task用来作为异步操作或者异步结果的连接通道,用std::future和std::shared_future来获取异步调用的结果。 future是不可拷贝的,只能移动,shared_future 是可以拷贝的,当需要将future放到容器中则需要用shared_future。
package_task和shared_future的基本用法:
int func( int x) { return x; } int main() { std::package_task<int(int)> tsk(func); //获取future的结果有3重方式:get、wait、wait_for std::future<int> fut = tsk.get_future(); std::thread(std::move(tsk), 2).detach(); //tsk作为线程函数 int value = fut.get(); //等待tsk完成并返回结果 cout<< value <<endl; //std::future是不能复制的,不能放到容器中,需要用到shared_future vector<std::shared_future<int>> v; auto f = std::async(std::launch::async, [](int a, int b){ return a+ b; }, 2, 3); v.push_back(f); cout<<v[0].get()<<endl; return 0; } //2 5
线程异步操作函数async
std::async可以用来直接创建异步的task,异步任务返回的结果也保存在future中,当需要获取异步任务的结果时,只需要调用future.get()方法即可,如果不关注异步任务的结果,只是简单地等待任务完成的话, 则调用future.wait()方法。
std::async的函数原型:
第一个参数:线程的创建策略,有两种策略,默认的策略是立即创建线程。
std::launch::async:在调用async时就开始创建线程。
std::launch::deferred:延迟加载方式创建线程。
调用async时不创建线程,直到调用了future的get或者wait时才创建线程。
第二个参数:线程函数,
第三个参数:线程函数的参数。async(std::launch::async|std::launch::deferred, f, args...)
std::async的使用:
std::future<int> f1 = std::async(std::launch::async, [](){ return 8; }); cout<<f1.get()<<endl; //8 std::future<int> f2 = std::async(std::launch::async, [](){ cout<<8<<endl; }); f2.wait(); std::future<int> future = std::async(std::launch::async, [](){ std::this_thread::sleep_for(std::chrono::seconds(3)); return 8; }); cout<<"waiting...."<<endl; std::future_status status; do { status = future.wait_for(std::chrono::seconds(1)); if(status == std::future_status::deferred) { cout<<"deferred"<<endl; } else if(status == std::future_status::timeout) { cout<<"timeout"<<endl; } eles if(status == std::future_status::ready) { cout<<"ready"<<endl; } }while(status != std::future_status::ready);
std::async是更高层次的异步操作,使得我们不用关注线程创建内部细节,就能方便地获取异步执行状态和结果,还可以指定线程创建策略:应该用std:async替代线程的创建,让它成为我们做异步操作的首选。