c++11 新特性实战 (一)
c++11多线程操作
线程
- thread
int main() { thread t1(Test1); t1.join(); thread t2(Test2); t2.join(); thread t3 = t1; thread t4(t1); thread t5 = std::move(t1); thread t6(std::move(t1)); return 0; }
t3,t4创建失败,因为thread的拷贝构造和赋值运算符重载的原型是:
thread(const thread&) = delete; thread& operator=(const thread&) = delete;
被禁用了,但是t5, t6线程是创建成功的。std::move把t1转换为右值,调用的是函数原型为
thread& operator=(thread&& _Other) noexcept
和thread(thread&& _Other) noexcept
。当线程对象t1被移动拷贝和移动赋值给t5和t6的时候,t1就失去了线程控制权,也就是一个线程只能同时被一个线程对象所控制。最直观的是t1.joinable()返回值为false,joinable()函数后面介绍。
使用类成员函数作为线程参数:
class Task { public: Task(){} void Task1() {} void Task2() {} private: }; int main() { Task task; thread t3(&Task::Task1, &task); t3.join(); return 0; }
关键点是要创建一个类对象,并作为第二个参数传入
thread()
线程的构造函数中去。管理当前线程的函数
- yield
此函数的准确性为依赖于实现,特别是使用中的 OS 调度器机制和系统状态。例如,先进先出实时调度器( Linux 的
SCHED_FIFO
)将悬挂当前线程并将它放到准备运行的同优先级线程的队列尾(而若无其他线程在同优先级,则yield
无效果)。#include <iostream> #include <chrono> #include <thread> // 建议其他线程运行一小段时间的“忙睡眠” void little_sleep(std::chrono::microseconds us) { auto start = std::chrono::high_resolution_clock::now(); auto end = start + us; do { std::this_thread::yield(); } while (std::chrono::high_resolution_clock::now() < end); } int main() { auto start = std::chrono::high_resolution_clock::now(); little_sleep(std::chrono::microseconds(100)); auto elapsed = std::chrono::high_resolution_clock::now() - start; std::cout << "waited for " << std::chrono::duration_cast<std::chrono::microseconds>(elapsed).count() << " microseconds\n"; }
- get_id
这个函数不用过多介绍了,就是用来获取当前线程id的,用来标识线程的身份。
std::thread::id this_id = std::this_thread::get_id();
- sleep_for
位于this_thread命名空间下,msvc下支持两种时间参数。
std::this_thread::sleep_for(2s); std::this_thread::sleep_for(std::chrono::seconds(1));
- sleep_untile
参数构建起来挺麻烦的,一般场景下要求线程睡眠的就用sleep_for就行了
using std::chrono::system_clock; time_t tt = system_clock::to_time_t(system_clock::now()); struct std::tm *ptm = localtime(&tt); std::this_thread::sleep_until(system_clock::from_time_t(mktime(ptm)));
互斥
- mutex
对于互斥量看到一个很好的比喻:
代码示例:
mutex mtx; int gNum = 0; void Test1() { mtx.lock(); for(int n = 0; n < 5; ++n) gNum++; mtx.unlock(); } void Test2() { std::cout << "gNum = " << gNum << std::endl; } int main() { thread t1(Test1); t1.join(); thread t2(Test2); t2.join(); return 0; }
join()表示主线程等待子线程结束再继续执行,如果我们的期望是打印循环自增之后的gNum的值,那t1.join()就放在t2创建之前调用。因为t2的创建就标志着t2线程创建好然后开始执行了。
通常mutex不单独使用,因为lock和unlock必须配套使用,如果忘记unlock很可能造成死锁,即使unlock写了,但是如果在执行之前程序捕获到异常,也还是一样会死锁。如何解决使用mutex造成的死锁问题呢?下面介绍unique_gard和lock_guard的时候详细说明。
- timed_mutex
提供互斥设施,实现有时限锁定
std::mutex cout_mutex; // 控制到 std::cout 的访问 std::timed_mutex mutex; void job(int id) { using Ms = std::chrono::milliseconds; std::ostringstream stream; for (int i = 0; i < 3; ++i) { if (mutex.try_lock_for(Ms(100))) { stream << "success "; std::this_thread::sleep_for(Ms(100)); mutex.unlock(); } else { stream << "failed "; } std::this_thread::sleep_for(Ms(100)); } std::lock_guard<std::mutex> lock(cout_mutex); std::cout << "[" << id << "] " << stream.str() << "\n"; } int main() { std::vector<std::thread> threads; for (int i = 0; i < 4; ++i) { threads.emplace_back(job, i); } for (auto& i: threads) { i.join(); } }
这里的第28行衍生出一个知识点:STL的
emplace_back
函数。这是c++11新增的容器类的操作函数,如果第二个参数忽略,用法和push_back
相似,都是在stl后面追加元素。函数原型:template<class... _Valty> decltype(auto) emplace_back(_Valty&&... _Val);
是一个变长的模板函数,例子中的代码传递的是一个函数指针job,
emplace_back
的实现会把job传递给std::thread
的构造函数,与push_back
需要是std::thread类型作为参数不同,所以emplace_back
是直接在容器中构造了要添加的元素,省去了再次把参数拷贝到stl中的过程,效率更高。目前来看还没有什么副作用,所以推荐以后在使用stl的时候使用emplace_back
取代push_back
.使用timed_mutex的时候也无法用unique_lock这样的RAII机制来控制加解锁操作,所以不同的互斥量的使用场景要区分清楚。在对的时候使用对的东西也是码农进阶的一个标志~
try_lock_until的例子:
void f() { auto now=std::chrono::steady_clock::now(); test_mutex.try_lock_until(now + std::chrono::seconds(10)); std::cout << "hello world\n"; } int main() { std::lock_guard<std::timed_mutex> l(test_mutex); std::thread t(f); t.join(); }
- recursive_mutex
提供能被同一线程递归锁定的互斥设施
recursive_mutex
类是同步原语,能用于保护共享数据免受从个多线程同时访问。recursive_mutex
提供排他性递归所有权语义:调用方线程在从它成功调用
lock
或try_lock
开始的时期里占有recursive_mutex
。此时期间,线程可以进行对lock
或try_lock
的附加调用。所有权的时期在线程调用unlock
匹配次数时结束。线程占有
recursive_mutex
时,若其他所有线程试图要求recursive_mutex
的所有权,则它们将阻塞(对于调用lock
)或收到 false 返回值(对于调用try_lock
)。可锁定
recursive_mutex
次数的最大值是未指定的,但抵达该数后,对lock
的调用将抛出 std::system_error 而对try_lock
的调用将返回 false 。
若
recursive_mutex
在仍为某线程占有时被销毁,则程序行为未定义。recursive_mutex
类满足互斥体 (Mutex) 和标准布局类型**(StandardLayoutType) 的所有要求。#include <iostream> #include <thread> #include <string> #include <mutex> class X { std::recursive_mutex m; std::string shared; public: void fun1() { std::lock_guard<std::recursive_mutex> lk(m); shared = "fun1"; std::cout << "in fun1, shared variable is now " << shared << '\n'; } void fun2() { std::lock_guard<std::recursive_mutex> lk(m); shared = "fun2"; std::cout << "in fun2, shared variable is now " << shared << '\n'; fun1(); // 递归锁在此处变得有用 std::cout << "back in fun2, shared variable is " << shared << '\n'; }; }; int main() { X x; std::thread t1(&X::fun1, &x); std::thread t2(&X::fun2, &x); t1.join(); t2.join(); }
- recursive_timed_mutex
提供能被同一线程递归锁定的互斥设施,并实现有时限锁定
通用互斥管理
- lock_guard
void Test1() { std::lock_guard<std::mutex> lg(mtx); for(int n = 0; n < 5; ++n) { gNum++; std::cout << "gNum = " << gNum << std::endl; } } int main() { thread t1(Test1); thread t2(Test1); t1.join(); t2.join(); return 0; }
lock_guard相当于利用RAII机制(“资源获取就是初始化”)把mutex封装了一下,在构造中lock,在析构中unlock。避免了中间过程出现异常导致的mutex不能够正常unlock.
- scoped_lock(c++17)
- unique_lock
- defer_lock_t
- try_to_lock_t
- adopt_lock_t
- defer_lock
- try_to_lock
- adopt_lock
通用锁算法
- try_lock
- lock
单次调用
- once_flag
- call_once
条件变量
- condition_variable
- condition_variable_any
- notify_all_at_thread_exit
- cv_status
Future
- promise
- packaged_task
- future
- shared_future
- async
- launch
- future_status
- Future错误
- future_error
- future_category
- future_errc