文章目录
一、thread类简介
C++11中加入了头文件,此头文件主要声明了std::thread线程类。C++11的标准类std::thread对线程进行了封装,定义了C++11标准中的一些表示线程的类、用于互斥访问的类与方法等。应用C++11中的std::thread便于多线程程序的移值。
1. thread成员函数
thread类的拷贝构造函数和赋值函数是被delete的,但支持移动构造函数。
(1)、get_id:获取线程ID,返回一个类型为std::thread::id的对象。
(2)、joinable:检查线程是否可被join。检查thread对象是否标识一个活动(active)的可行性线程。缺省构造的thread对象、已经完成join的thread对象、已经detach的thread对象都不是joinable。
(3)、join:调用该函数会阻塞当前线程。阻塞调用者(caller)所在的线程直至被join的std::thread对象标识的线程执行结束。
(4)、detach:将当前线程对象所代表的执行实例与该线程对象分离,使得线程的执行可以单独进行。一旦线程执行完毕,它所分配的资源将会被释放。
(5)、native_handle:该函数返回与std::thread具体实现相关的线程句柄。native_handle_type是连接thread类和操作系统SDK API之间的桥梁,如在Linux g++(libstdc++)里,native_handle_type其实就是pthread里面的pthread_t类型,当thread类的功能不能满足我们的要求的时候(比如改变某个线程的优先级),可以通过thread类实例的native_handle()返回值作为参数来调用相关的pthread函数达到目录。This member function is only present in class thread if the library implementation supports it. If present, it returns a value used to access implementation-specific information associated to the thread.
(6)、swap:交换两个线程对象所代表的底层句柄。
(7)、operator=:moves the thread object
(8)、hardware_concurrency:静态成员函数,返回当前计算机最大的硬件并发线程数目。基本上可以视为处理器的核心数目。
另外,std::thread::id表示线程ID,定义了在运行时操作系统内唯一能够标识该线程的标识符,同时其值还能指示所标识的线程的状态。Values of this type are returned by thread::get_id and this_thread::get_id to identify threads.
有时候我们需要在线程执行代码里面对当前调用者线程进行操作,针对这种情况,C++11里面专门定义了一个命名空间this_thread,此命名空间也声明在头文件中,其中包括:
- get_id()函数用来获取当前调用者线程的ID;
- yield()函数可以用来将调用者线程跳出运行状态,重新交给操作系统进行调度,即当前线程放弃执行,操作系统调度另一线程继续执行;
- sleep_until()函数是将线程休眠至某个指定的时刻(time point),该线程才被重新唤醒;
- sleep_for()函数是将线程休眠某个指定的时间片(time span),该线程才被重新唤醒,不过由于线程调度等原因,实际休眠实际可能比sleep_duration所表示的时间片更长。
二、线程创建
1. 普通函数
void foo(int x);
//传递foo的拷贝给线程
std::thread t(foo, 6);
//传递foo的地址给线程
std::thread t(&foo, 6);
2. 仿函数
class A{
public:
A{}
~A{}
public:
void f(int x, int y){}
void operator(int N){std::cout<<N<<std::endl;}
};
int main()
{
A a;
//传递a的拷贝给线程
std::thread t(a, 6);
//传递a的引用给线程
std::thread t(std::ref(a), 6);
//将主线程a转移给子线程,a在主线程不再有效
std::thread t(std::move(a), 6);
//传递A的临时对象给线程
std::thread t(A(), 6);
return 0;
}
3. 类成员函数
class A{
public:
A{}
~A{}
public:
void f(int x, int y){}
void operator(int N){std::cout<<N<<std::endl;}
};
int main(){
A a;
//传递a的拷贝的成员函数给线程
std::thread t(&A::f, a, 6, 8);
//传递a的地址的成员函数给线程
std::thread t(&A::f, &a, 6, 8);
return 0;
}
4. lambda
int main()
{
std::thread t([](int a){std::cout<<a<<std::endl;});
return 0;
}
5. 类内使用成员函数
有两种方式一种是使用std::bind,另一种是直接传入类成员地址,详见代码:
// 构造函数创建线程
class Hello {
public:
Hello() {
//std::thread t(std::bind(&Hello2::Entry, this, "World"));
std::thread t(&Hello::Entry, this, "World");
t.join();
}
private:
// 线程函数
void Entry(const char* what) {
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << "Hello, " << what << "!" << std::endl;
}
};
int main()
{
Hello hello;
return 0;
}
三、线程同步
1. 锁
1.1 lock_guard
lock_guard 通常用来管理一个 std::mutex 类型的对象,通过定义一个 lock_guard 一个对象来管理 std::mutex 的上锁和解锁。在 lock_guard 初始化的时候进行上锁,然后在 lock_guard 析构的时候进行解锁。这样避免了人为的对 std::mutex 的上锁和解锁的管理。
定义如下:
template<class Mutex> class lock_guard;
它的特点如下:
(1) 创建即加锁,作用域结束自动析构并解锁,无需手工解锁
(2) 不能中途解锁,必须等作用域结束才解锁
(3) 不能复制
注意:
lock_guard 并不管理 std::mutex 对象的声明周期,也就是说在使用 lock_guard 的过程中,如果 std::mutex 的对象被释放了,那么在 lock_guard 析构的时候进行解锁就会出现空指针错误。
示例代码如下:
#include <thread>
#include <mutex>
#include <iostream>
int g_i = 0;
std::mutex g_i_mutex;
void safe_increment()
{
const std::lock_guard<std::mutex> lock(g_i_mutex);
++g_i;
std::cout << std::this_thread::get_id() << ": " << g_i << '\n';
}
int main()
{
std::cout << "main: " << g_i << '\n';
std::thread t1(safe_increment);
std::thread t2(safe_increment);
t1.join();
t2.join();
std::cout << "main: " << g_i << '\n';
}
输出:
main: 0
140641306900224: 1
140641298507520: 2
main: 2
1.2 unique_lock
unique_lock 和 lock_guard 一样,对 std::mutex 类型的互斥量的上锁和解锁进行管理,一样也不管理 std::mutex 类型的互斥量的声明周期。但是它的使用更加的灵活,支持的构造函数如下:
方法 | 说明 | 详细说明 |
---|---|---|
unique_lock() noexcept; | 默认构造函数 | 默认构造函数 新创建的 unique_lock 对象不管理任何 Mutex 对象 |
explicit unique_lock(mutex_type& m); | 加锁 | 新创建的 unique_lock 对象,管理 Mutex 对象 m,并尝试调用 m.lock() 对 Mutex 对象进行上锁,如果此时另外某个 unique_lock 对象已经管理了该 Mutex 对象 m,则当前线程将会被阻塞。 |
unique_lock(mutex_type& m, try_to_lock_t tag); | 尝试加锁 | try-locking 初始化新创建的 unique_lock 对象,管理 Mutex 对象 m,并尝试调用 m.try_lock() 对 Mutex 对象进行上锁,但如果上锁不成功,并不会阻塞当前线程。 |
unique_lock(mutex_type& m, defer_lock_t tag) noexcept; | 延迟加锁 | deferred 初始化新创建的 unique_lock 对象,管理 Mutex 对象 m,但是在初始化的时候并不锁住 Mutex 对象。 m 应该是一个没有当前线程锁住的 Mutex 对象。 |
unique_lock(mutex_type& m, adopt_lock_t tag); | 递归加锁 | adopting 初始化 新创建的 unique_lock 对象管理 Mutex 对象 m, m 应该是一个已经被当前线程锁住的 Mutex 对象。(并且当前新创建的 unique_lock 对象拥有对锁(Lock)的所有权)。 |
template <class Rep, class Period> unique_lock(mutex_type& m, const chrono::duration<Rep,Period>& rel_time); | 限时锁定 | locking 一段时间(duration) 新创建的 unique_lock 对象管理 Mutex 对象 m,并试图通过调用 m.try_lock_for(rel_time) 来锁住 Mutex 对象一段时间(rel_time)。 |
template <class Clock, class Duration> unique_lock(mutex_type& m, const chrono::time_point<Clock,Duration>& abs_time); | 定时锁定 | locking 直到某个时间点(time point) 新创建的 unique_lock 对象管理 Mutex 对象m,并试图通过调用 m.try_lock_until(abs_time) 来在某个时间点(abs_time)之前锁住 Mutex 对象。 |
unique_lock(const unique_lock&) = delete; | 禁止拷贝 | 拷贝构造 [被禁用] unique_lock 对象不能被拷贝构造。 |
unique_lock(unique_lock&& x); | 所有权转移 | 新创建的 unique_lock 对象获得了由 x 所管理的 Mutex 对象的所有权(包括当前 Mutex 的状态)。调用 move 构造之后,x 对象如同通过默认构造函数所创建的,就不再管理任何 Mutex 对象了。 |
unique_lock是一个通用的互斥量锁定包装器,它允许延迟锁定,限时深度锁定,递归锁定,锁定所有权的转移以及与条件变量一起使用。
简单地讲,unique_lock 是 lock_guard 的升级加强版,它具有 lock_guard 的所有功能,同时又具有其他很多方法,使用起来更强灵活方便,能够应对更复杂的锁定需要。
特点如下:
- 创建时可以不锁定(通过指定第二个参数为std::defer_lock),而在需要时再锁定
- 可以随时加锁解锁
- 作用域规则同 lock_grard,析构时自动释放锁
- 不可拷贝和复制,可移动(移动构造函数)
- 条件变量需要该类型的锁作为参数(此时必须使用unique_lock)
示例代码:
#include <mutex>
#include <thread>
#include <chrono>
struct Box {
explicit Box(int num) : num_things{num} {}
int num_things;
std::mutex m;
};
void transfer(Box &from, Box &to, int num)
{
// don't actually take the locks yet
std::unique_lock<std::mutex> lock1(from.m, std::defer_lock);
std::unique_lock<std::mutex> lock2(to.m, std::defer_lock);
// lock both unique_locks without deadlock
std::lock(lock1, lock2);
from.num_things -= num;
to.num_things += num;
// 'from.m' and 'to.m' mutexes unlocked in 'unique_lock' dtors
}
int main()
{
Box acc1(100);
Box acc2(50);
std::thread t1(transfer, std::ref(acc1), std::ref(acc2), 10);
std::thread t2(transfer, std::ref(acc2), std::ref(acc1), 5);
t1.join();
t2.join();
}
1.3 总结
所有 lock_guard 能够做到的事情,都可以使用 unique_lock 做到,反之则不然。
那么何时使用 lock_guard 呢?很简单,需要使用锁的时候,首先考虑使用 lock_guard。它简单、明了、易读。如果用它完全 ok,就不要考虑其他了。如果现实不允许,再使用 unique_lock 。
2.条件变量
2.1 简述
在多线程编程中,当多个线程之间需要进行某些同步机制时,如某个线程的执行需要另一个线程完成后才能进行,可以使用条件变量。
c++11提供的 condition_variable 类是一个同步原语,它能够阻塞一个或者多个线程,直到另一线程修改共享变量并通知 condition_variable。
也可以把它理解为信号通知机制,一个线程负责发送信号,其他线程等待该信号的触发。
condition_variable 存在一些问题,如虚假唤醒,这可以通知增加额外的共享变量来避免。
针对增加额外的变量这一点,为什么不在另一线程循环检测这个变量,从而达到相同目的而不需要再使用条件变量?
循环检测时,程序在高速运行,占用过高的cpu,而条件变量的等待是阻塞,休眠状态下cpu使用率为0,省电!
对于运行中的线程,可能会被操作系统调度,切换cpu核心,这样一来,所有的缓存可能失效,而条件变量不会,省时!
对于只需要通知一次的情况,如初始化完成、登录成功等,建议不要使用 condition_variable,使用std::future更好。
2.2 使用
通知方:
- 获取 std::mutex, 通常是 std::lock_guard
- 修改共享变量(即使共享变量是原子变量,也需要在互斥对象内进行修改,以保证正确地将修改发布到等待线程)
- 在 condition_variable 上执行 notify_one/notify_all 通知条件变量(该操作不需要锁)
等待方:
- 获取相同的 std::mutex, 使用 std::unique_lock
- 执行 wait,wait_for或wait_until(该操作会自动释放锁并阻塞)
- 接收到条件变量通知、超时或者发生虚假唤醒时,线程被唤醒,并自动获取锁。唤醒的线程负责检查共享变量,如果是虚假唤醒,则应继续等待
std :: condition_variable仅适用于 std::unique_lock, 此限制允许在某些平台上获得最大效率。 std :: condition_variable_any提供可与任何BasicLockable对象一起使用的条件变量,例如std :: shared_lock。
示例代码如下:
#include <iostream>
#include <string>
#include <thread>
#include <mutex>
#include <condition_variable>
std::mutex m;
std::condition_variable cv;
std::string data;
bool ready = false;
bool processed = false;
void worker_thread()
{
// Wait until main() sends data
std::unique_lock<std::mutex> lk(m);
cv.wait(lk, []{return ready;});
// after the wait, we own the lock.
std::cout << "Worker thread is processing data\n";
data += " after processing";
// Send data back to main()
processed = true;
std::cout << "Worker thread signals data processing completed\n";
// Manual unlocking is done before notifying, to avoid waking up
// the waiting thread only to block again (see notify_one for details)
lk.unlock();
cv.notify_one();
}
int main()
{
std::thread worker(worker_thread);
data = "Example data";
// send data to the worker thread
{
std::lock_guard<std::mutex> lk(m);
ready = true;
std::cout << "main() signals data ready for processing\n";
}
cv.notify_one();
// wait for the worker
{
std::unique_lock<std::mutex> lk(m);
cv.wait(lk, []{return processed;});
}
std::cout << "Back in main(), data = " << data << '\n';
worker.join();
}
输出:
main() signals data ready for processing
Worker thread is processing data
Worker thread signals data processing completed
Back in main(), data = Example data after processing
其中:
- notify_one() 只唤醒一个线程,如果有多个线程,具体唤醒哪一个不确定,如果需要唤醒其他所有线程,使用 notify_all()
- 执行 notify_one() 时不需要锁
- 修改共享变量 ready/processed 时需要锁,共享变量用于避免虚假唤醒
- cv.wait 第一个参数必须是 unique_lock,因为它内部会执行 unlock和lock,如果需要设置超时,使用 wait_for/wait_until
2.3 总结
使用注意事项:
- 需要共享变量来避免虚假唤醒
- 共享变量的修改需要在锁内进行
- 通知线程在发出通知时不需要加锁
- 通知线程一般使用lock_guard即可
- 接收线程的wait函数第一个参数需要是unique_lock
- 接收线程需要判断是否为虚假唤醒
特别注意,如果不使用共享变量,当通知线程在接收线程准备接收之前发送通知,接收线程将要永远阻塞了。这里,共享变量已经置位,所以它也能避免丢失唤醒。
总之,条件变量使用广泛,了解它的优势和不足,便于我们作出最佳决策。
四、异步编程之future/promise
4.1 < future >头文件简介
Classes
std::future
std::future_error
std::packaged_task
std::promise
std::shared_future
Functions
std::async
std::future_category
4.2 std::future
简单来说,std::future提供了一种访问异步操作结果的机制。
从字面意思看,它表示未来。通常一个异步操作我们是不能马上就获取操作结果的,只能在未来某个时候获取。我们可以以同步等待的方式来获取结果,可以通过查询future的状态(future_status)来获取异步操作的结果。future_status有三种状态:
- deferred:异步操作还没开始
- ready:异步操作已经完成
- timeout:异步操作超时
获取future结果有三种方式:get、wait、wait_for,其中get等待异步操作结束并返回结果,wait只是等待异步操作完成,没有返回值,wait_for是超时等待返回结果。
例子:
//查询future的状态
std::future_status status;
do {
status = future.wait_for(std::chrono::seconds(1));
if (status == std::future_status::deferred) {
std::cout << "deferred\n";
} else if (status == std::future_status::timeout) {
std::cout << "timeout\n";
} else if (status == std::future_status::ready) {
std::cout << "ready!\n";
}
} while (status != std::future_status::ready);
4.3 std::promise
Promise对象可保存T类型的值,该值可被future对象读取(可能在另一个线程中),这是promise提供同步的一种手段。在构造promise时,promise对象可以与共享状态关联起来,这个共享状态可以存储一个T类型或者一个由std::exception派生出的类的值,并可以通过get_future来获取与promise对象关联的对象,调用该函数之后,两个对象共享相同的共享状态(shared state)。
Promise对象是异步provider,它可以在某一时刻设置共享状态的值。
Future对象可以返回共享状态的值,或者在必要的情况下阻塞调用者并等待共享状态标识变为ready,然后才能获取共享状态的值。
4.3.1 构造函数
1.默认构造函数,初始化一个空的共享状态。
2.带自定义内存分配器的构造函数,与默认构造函数类似,但是使用自定义分配器来分配共享状态。
3.拷贝构造函数,被禁用。
4.移动构造函数。
另外,std::promise 的 operator= 没有拷贝语义,即 std::promise 普通的赋值操作被禁用,operator= 只有 move 语义,所以 std::promise 对象是禁止拷贝的。
4.3.2 std::promise 成员函数
std::promise::get_future:返回一个与promise共享状态相关联的future对象
std::promise::set_value:设置共享状态的值,此后promise共享状态标识变为ready
std::promise::set_exception:为promise设置异常,此后promise的共享状态标识变为ready
std::promise::set_value_at_thread_exit:设置共享状态的值,但是不将共享状态的标志设置为 ready,当线程退出时该 promise 对象会自动设置为 ready(注意:该线程已设置promise的值,如果在线程结束之后有其他修改共享状态值的操作,会抛出future_error(promise_already_satisfied)异常)
std::promise::swap:交换 promise 的共享状态
例子:
#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'; // 打印 value: 10.
}
int main ()
{
std::promise<int> prom; // 生成一个 std::promise<int> 对象.
std::future<int> fut = prom.get_future(); // 和 future 关联.
std::thread t(print_int, std::ref(fut)); // 将 future 交给另外一个线程t.
prom.set_value(10); // 设置共享状态的值, 此处和线程t保持同步.
t.join();
return 0;
}
4.4 std::packaged_task
std::packaged_task包装了一个可调用的目标(如function, lambda expression, bind expression, or another function object),以便异步调用,它和promise在某种程度上有点像,promise保存了一个共享状态的值,而packaged_task保存的是一个函数。
std::packaged_task<int()> task([](){ return 7; });
std::thread t1(std::ref(task));
std::future<int> f1 = task.get_future();
auto r1 = f1.get();
4.5 小结
Promise,Future 和 Callback常常作为并发编程中一组非阻塞的模型。其中 Future 表示一个可能还没有实际完成的异步任务的【结果】,针对这个结果可以添加 Callback 以便在任务执行成功或失败后做出对应的操作,而 Promise 交由任务执行者,任务执行者通过 Promise 可以标记任务完成或者失败。
4.6 std::async
std::async大概的工作过程:先将异步操作用std::packaged_task包装起来,然后将异步操作的结果放到std::promise中,这个过程就是创造未来的过程。外面再通过future.get/wait来获取这个未来的结果。
可以说,std::async帮我们将std::future、std::promise和std::packaged_task三者结合了起来。
std::async的原型:
async(std::launch::async | std::launch::deferred, f, args...)
第一个参数是线程的创建策略,默认的策略是立即创建线程:
std::launch::async:在调用async就开始创建线程。
std::launch::deferred:延迟加载方式创建线程。调用async时不创建线程,直到调用了future的get或者wait时才创建线程。
第二个参数是线程函数,后面的参数是线程函数的参数。
简单的例子:
std::future<int> f1 = std::async(std::launch::async, [](){
return 8;
});
cout<<f1.get()<<endl; //output: 8
std::future<int> f2 = std::async(std::launch::async, [](){
cout<<8<<endl;
});
f2.wait(); //output: 8
参考链接:
https://blog.csdn.net/guotianqing/article/details/104017649
https://blog.csdn.net/sinat_35945236/article/details/124505414
https://blog.csdn.net/sevenjoin/article/details/82187127
https://blog.csdn.net/jiange_zh/article/details/51602938