现代C++的多线程开发
前言
早期的C++
进行多线程编程,往往需要根据不同的系统编写不同的代码,但是在C++11
之后,std
中已经提供了多线程的支持,所以对于不同操作系统只需要编写一次代码即可。
本文记录一次多线程开发过程中,使用的C++新特性(指C++11及之后引入的库或语法规则),其中包括:
std::function
std::result_of / std::invoke_of
std::bind
std::thread
std::packaged_task
std::future
std::promise
std::shared_future
std::async
std::mutex
std::timed_mutex
std::recursive_mutex
std::recursive_timed_mutex
std::shared_mutex
std::lock_guard
std::unique_lock
std::shared_lock
std::try_lock
std::lock
std::condition_variable
注意:若无特殊说明,各种方法、类型在C++11
开始支持。
函数处理
C++11
之后提供了很多函数相关的模板类用于封装函数,从而替代掉通过typedef
的方式定义函数类型。
函数封装 std::function
该类型是一个模板类,常常用于绑定一个函数或者函数对象(重载了()
的类)。
对于该类型,可以理解成对每个函数指针都使用using
或typedef
定义了相应的别名,例如:
void callback(int a, int b) {}
using CallbackFunction = void(*)(int, int);
CallbackFunction cb1 = callback;
// using std::function has the same effects.
std::function<void(int, int)> cb2 = callback;
cb1(1, 2);
cb2(1, 2);
std::function
可以用来传递回调函数,相比于函数指针,它更加灵活,例如可以通过模板和模板参数包实现任意函数作为回调函数:
template<class T, class ...Args>
void test(std::function<T> cb, Args ...args)
{
cb(args...);
}
这比使用函数指针作为类型参数要方便的多。
除此之外,std::function
在多线程中可以绑定到std::packaged_task
上用于异步执行。
下面给出几个特殊的实例:
- 在匿名函数中如果需要实现递归函数,通过将匿名函数绑定到
std::function
:
上面的代码去掉外层匿名函数可以实现相同的效果,这里只是展示在匿名函数内部如果需要使用递归,则可以通过auto factorial = [](int n) { std::function<int(int)> fac = [&](int n) { return (n < 2) ? 1 : n * fac(n - 1); }; return fac(n); };
std::function
来实现。 - 可以将类成员函数绑定到
std::function
绑定时需要指定调用对象:
上面的代码中需要注意第一个参数的类型,如果第一个参数的类型为引用或者指针,这是在原对象上进行操作,而如果第一个参数类型为非引用或指针,则是会通过拷贝构造函数构造一个对象绑定到参数上进行操作,可以通过将复制构造器删除来证明(将代码中的struct Foo { Foo(int num) : num_(num) {} Foo(const Foo &other) = default; void print_add(int i) { std::cout << num_ + i << '\n'; } int num_; }; std::function<void(Foo&, int)> f_add_display = &Foo::print_add; Foo foo(314159); f_add_display(foo, 1); // same as foo.print_add(1); std::function<void(Foo, int)> copy_f_add_display = &Foo::print_add; copy_f_add_display(foo, 1); // same as Foo(foo).print_add(1);
default
改为delete
)此时无法通过编译。 std::funciton
可以绑定到类成员变量上,用于获取成员变量的值:
上述代码同样需要注意第一个参数类型时引用与指针和不是引用与指针时的区别。同时如果返回值定义为非引用,那么结果是一个右值,当返回值定义为引用类型的时候,其结果为一个左值。std::function<int(Foo &)> f_num = &Foo::num_; f_num(foo); // same as foo.num_, but f_num(foo) is a rvalue; std::function<int(Foo)> copy_f_num = &Foo::num_; copy_f_num(foo); // same as Foo(foo).num_, but f_num(foo) is a rvalue;
获取函数返回值类型 std::result_of / std::invoke_of
std::result_of
在C++17
中已被标记为deprecated
(过时的),在C++20
中已经被移除,取而代之的是std::invoke_of
。其功能是获取一个函数对象、函数等的返回类型。
std::result_of
被std::invoke_of
替代的原因是:当传入的模板不是一个可执行类型时,std::result_of
的行为是未定义的,而std::invoke_of
则没有type
类型,除此之外,std::result_of
对于一些情况下出现一些奇怪的特点:
- 返回值不能为一个函数或者数组(但可以是指针);
- 数组作为参数会被转换为指针;
- 返回值和参数均不能是抽象类;
- 参数的
cv
限定符会被丢弃; - 参数的类型不能是
void
;
std::result_of
和std::invoke_of
的使用方式略有不同,前者按照F(Args...)
的方式接收模板类型,而后者按照F, Args...
的方式接收参数,例如:
int test(int, int);
std::result_of<decltype(*test)(int, int)>::type a;
std::invoke_result<decltype(*test), int, int>::type b; // since C++ 17
// a and b are both int.
std::result_of / std::invoke_of
的使用场景往往是需要根据传入的函数来确定其返回值的时候使用,例如在std::function
中给出的回调例子,如果需要根据回调的返回值来确定调用的返回值:
// using result_of in template function, we must using && to get the type.
template<class Func, class ...Args>
typename std::result_of<Func&&(Args&&...)>::type test1(Func&& cb, Args&& ...args) {
using ReturnType = typename std::result_of<Func&&(Args&&...)>::type;
return cb(args...);
}
// same effects with decltype
template<class Func, class ...Args>
auto test2(Func&& cb, Args&& ...args) -> decltype(cb(args...)) {
return cb(args...);
}
// test1 and test2 are the same.
// when parameter is std::funciton
template<class T, class ...Args>
auto test3(std::function<T>&& cb, Args&& ...args)
-> typename std::result_of<std::function<T>(Args...)>::type
{
return cb(args...);
}
// when parameter is std::funciton and using decltype
template<class T, class ...Args>
auto test4(std::function<T> cb, Args ...args) -> decltype(cb(args...))
{
return cb(args...);
}
// test3 and test4 are the same.
int x() { return 0; }
int x1 = test1(x);
int x2 = test2(x);
int x3 = test3(std::function<int()>{x});
int x4 = test4(std::function<int()>{x});
上面的代码中需要注意的是,根据cppreference
中的说法,为了防止出现奇怪的特点,我们会使用引用进行传递,因此在第一个实现中使用万能引用,对于上面的代码也给出了通过decltype
利用返回值后置的特性实现。
decltype
也可以获取函数的返回值类型,std::result_of / sdt::invoke_result
也能获取返回值类型,二者最大的区别在于,前置需要传入一个值(或者变量),而后者需要传输类型。
函数绑定 std::bind
std::bind
用于给函数去别名这个重新定义函数的参数顺序以及默认值。
例如我们有如下的函数:
void func(int a, int b, int c)
{
// do someting...
std::cout << a << b << c << sdt::endl;
}
func(1, 2, 3);
在某些情况下我们可能会需要传入默认的值,那么函数可以写成如下的形式:
void func(int a, int b, int c = 3)
{
// do something...
std::cout << a << b << c << sdt::endl;
}
func(1, 2); // this is ok, it is same with func(1, 2, 3);
那么如果我们在使用的时候,我们发现我们可能需要频繁的使用func(1, x, y)
这个形式呢?由于默认参数只能出现在非默认参数的后面,那么此时要实现上述的功能,则必须将参数的调用顺序改变。此时我们可能需要重新定义一个函数:
void func(int x, int y)
{
func(1, x, y);
}
int x = 0, y = 0;
((void(*)(int, int))func)(x, y); // func(int, int) and func(int, int, int = 3) are same, so we need convert to make sure the call unambiguous.
上面每次调用的时候需要转换,当然也可·可以为后定义的func
取一个新名字。上面的方法最大的问题在于:我们只需要在某一个作用域频繁使用,而我们却要为整个命名空间增加一个新的函数。有没有一种方法能够只在当前的作用域生成一个函数,一旦退出当前的作用域,那么生成的函数同时失效?
通过std::bind
实现:
using namespace std::placeholders; // this if for placeholders: _1 and _2
auto newFunc = std::bind(func, 1, _1, _2);
int x = 0, y = 0;
newFunc(x, y); // this is same with func(1, x, y);
std::bind
返回的是一个重载了()
的类,其行为根据绑定的参数决定,占位符的个数代表调用的时候需要几个参数:_1
始终对应于第一个实参,_2
始终对应第二个实参而我们实际调用的是func(1, _1, _2);
,当然我们也可以通过std::bind()
来改变参数的调用位置:
auto newFunc = std::bind(func, 1, _2, _1); // this will call func(1, _2, _1);
newFunc(x, y); // this is same with func(1, y, x); because y is _2 and x is _1.
由于std::bind()
返回的是一个可调用对象,当然可以将其绑定到一个std::function
上:
// the template parameter is decided by the return type of func and the num of placehoders.
std::function<void(int, int)> newFunc = std::bind(func, 1, _1, _2);
std::function<void(int, int, int)> newFunc2 = std::bind(func, _1, _2, _3);
Lambda
函数也可以实现上述的所有功能,以其中一个为例:
auto newFunc = [] (int x, int y) {
func(1, x, y);
};
int x = 0, y = 0;
newFunc(x, y); // same with func(1, x, y);
注意:当绑定的函数参数是引用的时候,如果要使用默认值进行引用传递需要用std::cref / std::ref
进行封装,不然其传递的是一个值,例如:
void modifyA(int &a, int b, int c)
{
a = -1;
std::cout << a << b << c << std::endl;
}
int a = 0, b = 0, c = 0;
auto badFunc = std::bind(modifyA, a, b, c);
badFunc(); // after this a is still 0.
auto goodFunc = std::bind(modifyA, std::ref(a), b, c);
goodFunc(); // after this a is -1.
// implement by Lambda Function:
auto anotherGoodFunc = [&a, b, c]() {
modifyA(a, b, c);
};
anotherGoodFunc();
多线程中的函数处理
多线程 std::thread
在C++11
之前想要实现多线程的功能,则必须使用操作系统提供的接口(例如在Linux
中的pthread
),这样的话无法做到跨平台,对于不同的平台,如果使用多线程往往需要编写两份不同的代码。而在C++11
到来之后,其对不同的平台的线程库进行了封装,只需要编写一份代码则可以在不同的平台上实现多线程的功能(需要在运行的时候连接对应平台的线程库,例如在Linux
平台下则需要添加-lpthread
的编译选项)。
std::thread
的使用非常简单,只需要在构造的时候传入一个函数或者一个可调用对象(重载了()
操作符的类型)与需要的参数:
void func(int a, int b, int c) {}
std::thread t1(func, 1, 2, 3); // t1 is another thread running func(1, 2, 3);
std::thread t2([]() { std::cout << "Hello Thread!\n"; }); // t2 is another thread running a lambda function.
t1.join();
t2.join();
上面的代码中使用到了std::thread::join
其功能是让当前的进程等待某个进程执行结束后再继续执行当前的进程。例如在上面的代码中如果没有join
语句,那么当前的主线程可能执行结束后,t1
与t2
依然没有执行完成,此时由于主线程(进程)被回收,对应的子线程也会被回收,即t1、t2
不能继续执行,而如果调用了join
则只有对应的线程执行完成后,当前的线程才会继续执行,在C++20
中提出了std::jthread
即在析构的时候会自动调用join
的线程,其提出是为了更好的契合RAII
(Resources Acquisition is Initialization,对于资源的申请在初始化的时候完成,对于资源的释放由析构函数自动完成)思想。
std::thread
还有一个重要的函数detach
,其能够让线程脱离std::thread
独立执行,该函数往往用于实现某些后台进程的功能。一般情况下,当std::thraed
创建的对象被析构之后,其管理的线程则不能继续执行。对于有些情况下,我们需要让一个线程在后台执行而不需要通过std::thread
对象获取相关信息,那么我们可以让该线线程detach
。这样的好处是,可以让当前的线程及时的释放资源,例如:
void daemonFunc() {
// do something...
}
void oneThreadFunc()
{
shared_ptr<int> integerP(new int[(int)8e6]); // big data here
// do something...
std::thread daemonT(daemonFunc);
daemonT.detach();
// if we use daemonT.join() rather than daemonT.detach() to make sure daemonT finish, the integerP will be released after the daemonT thread.
} // integerP is released after this function.
std::thread t1(oneThreadFunc);
t1.join();
对于上面的的代码我们希望通过RAII
来管理数据,如果我们不通过detach
,那么我们必须通过daemonT.join()
来保证daemonFunc
退出后再析构daemonT
对象,如果这样的话integerP
的释放则会被延迟到daemonT
结束后,但是上面的代码通过detach
让线程脱离std::thread
也能执行,因此可以让integerP
的释放不用等待该线程结束后。
不过需要注意的是,在使用detach
的时候我们需要确定不会使用到当前作用域申请的数据,例如上面的代码中我们需要保证daemonFunc
中不使用integerP
所指向的数据。
多线程的函数绑定 std::packaged_task
std::packaged_task
的使用方法与std::function
一模一样(可以绑定任意一个可执行对象)。唯一不同的是其提供了std::packaged_task::get_future
方法,该方法可以获取一个std::future
对象用于获取函数的返回值(如果函数有返回值的话)。
异步获取返回值 std::future
上文提到的std::thread
非常好用,但是如果我们需要处理多线程函数的返回值呢?
当然通过std::thread
也是可以实现的:共享一个变量,当线程函数结束的时候,将该变量设置为函数的返回值即可,需要注意的是,这样的实现往往需要借助条件变量或者锁来实现。当需要考虑锁的时候,程序往往就会变得复杂起来,我们的需求明明很简单:获取多线程函数的返回值。能不能不用程序员管理锁即可实现该功能呢?
对于一个有返回值的多线程函数,我们只需要将其绑定到std:packaged_task
上,通过std::packaged_task::get_future
获取到std::future
对象之后,就可以等待线程执行完成后,获取到线程的返回值了,这也是std::future
的常见用法之一:
int threadFunc() { return 0; }
std::packaged_task<int()> threadTask{threadFunc};
std::future<int> result = threadTask.get_future();
std::thread t{std::move(threadTask)};
std::cout << result.get() << std::endl;
t.join(); // this is needed, when the threadFunc returns, the future can get but the thread may be stopping.
在上面的代码中一定要记住调用join
因为当函数返回的时候,std::future
便已经可以通过get
获取值了,但是此时的线程可能并未完全停止(可能正在释放资源)。
std::future
也可以在一个单线程程序中使用,其表现的行为类似于通过std::function
对函数取了一个别名:
int threadFunc() { return 0; }
std::packaged_task<int()> threadTask{threadFunc};
std::future<int> result = threadTask.get_future();
result.get();
// same implementation:
std::function<int()> func(threadFun};
func();
值传递 std::promise
std::promise
用于传递一个值(可以是任意类型),通过set_value
进行设置,通过get_future
获取std::future
对象后,可以通过std::future::get
方法获取传递的值,该类支持的所有功能均可以通过条件变量实现,但是使用std::promise
在某些场景可以简化代码,同时也不需要我们手动进行加锁等操作。
官网给出的一个例子:
void accumulate(std::vector<int>::iterator first,
std::vector<int>::iterator last,
std::promise<int> accumulate_promise) {
int sum = std::accumulate(first, last, 0);
accumulate_promise.set_value(sum); // Notify future
}
void do_work(std::promise<void> barrier) {
std::this_thread::sleep_for(std::chrono::seconds(1));
barrier.set_value();
}
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5, 6};
std::promise<int> accumulate_promise;
std::future<int> accumulate_future = accumulate_promise.get_future();
std::thread work_thread(accumulate, numbers.begin(), numbers.end(),
std::move(accumulate_promise));
std::cout << "result=" << accumulate_future.get() << '\n';
work_thread.join(); // wait for thread completion
}
上面的例子中,子线程对std::vector
中的元素求和,求和完成后,通过std::promise
进行传递,在主线程中通过预先获取的future
进行get
操作。
std::promise
也可以用来传递函数,这里不给出例子。
当std::promise
的模板类型为void
的时候,其往往可以作为一个Barrier
来使用,这里的例子在std::shared_future
部分给出。
共享future std::shared_future
std::shared_future
是对std::future
的封装,其能够让多个线程通过get
获取函数的返回值,该类型的表现行为与std::conditon_variable
类似。但是使用std::shared_future
我们可以通过值传递的方式(这里应该是浅拷贝),让每个线程拥有自己的std::shared_future
对象,这样可以不用加锁也能实现条件变量的功能,例如我们需要完成某些工作后,才能让所有的其他进程启动:
std::promise<void> ready;
std::shared_future<void> sf = ready.get_future();
std::thread t1{[sf]() { sf.get(); std::cout << "Thread 1 has started\n"; }};
std::thread t2{[sf]() { sf.wait(); std::cout << "Thread 2 has started\n"; }};
std::thread t3{[sf]() { sf.wait(); std::cout << "Thread 3 has started\n"; }}; // wait and get are the same.
std::this_thread::sleep_for(2000ms);
std::cout << "Finish Some Preparation.\n";
ready.set_value();
t1.join();
t2.join();
t3.join();
上面的代码子线程中的输出一定是晚于主线程中的输出的。
异步启动与延迟启动 std::async
在前面介绍的使用std::packaged_task
可以获取多线程函数的返回值,但是上面的代码每一次都需要使用std::packaged_task
进行一次封装,在大多数情况下写起来比较复杂,而std::async
同样可以使用实现上述的功能,而且其代码更加简洁:
int threadFunc(int &i) { i = -1; return -1; }
int x = 0;
std::future<int> result = std::async(threadFunc, std::ref(x)); // don't forget the std::ref to make it as a reference.
std::cout << result.get() << std::endl;
除此之外,std::async
还支持懒启动,即只有调用std::future::wait
或者std::future::get
时多线程才回开始执行,例如:
void threadFunc() { std::cout << "Thread is running...\n"; }
std::future<void> result = std::async(std::launch::deferred, threadFunc);
std::this_thread::sleep_for(2000ms);
result.wait(); // threadFunc starts to run.
锁
锁一直是多线程中必不可少的工具。C++11
之后标准库提供了各种各样的锁。
需要注意的是:下面介绍的锁均是不公平锁,被阻塞的进程在获取锁的时候不是按照阻塞的先后顺序获取锁。
互斥锁 std::mutex
互斥锁指的是任何时刻只有一个进程能够获得锁。其操作非常简单:
std::mutex mu;
mu.lock();
// do something.
mu.unlock();
上面的lock
在不能获得锁的时候会被阻塞。也可以使用try_lock
获取锁,当获取失败的时候,try_lock
会返回false
获取成功则是返回true
:
std::mutex mu;
if (mu.try_lock()) {
// do something...
mu.unlock();
} else {
std::cerr << "failed to get mutex\n";
}
为try_lock设置时间 std::timed_mutex
std::mute
中的try_lock
只会进行一次获取锁的操作,在很多时候我们希望在一段时间内一直尝试获取锁,而std::timed_mutex
就能实现这项功能,std::timed_mutex
与std::mutex
大同小异,唯一不同的就是std::timed_mutex
提供了std::timed_mutex::try_lock_for
与std::timed_mutex::try_lock_until
可以根据传入的参数来决定尝试获取锁的时间长短,当在指定的时间内获取到锁时返回true
,否则返回false
:
std::timed_mutex t_mu;
if (t_mu.try_lock_for(2000ms)) {
// ... do something
t_mu.unlock();
} else {
cerr << "Failed to get lock in 2000ms\n";
}
std::timed_mutex
是std::mutex
的扩充,其当然也有lock
与try_lock
方法。
可重入互斥锁 std::recursive_mutex
可重入锁指的是同一个线程可以多次获取同一个锁。上文提到的std::mutex
为非可重入锁,当一个已经获取std::mutex
的线程继续尝试lock
时,其会被一直阻塞(此时发生了单个线程的死锁)。而使用std::recursive_mutex
可以保证获取了锁的线程能够重复获取该锁,可重入锁一般用于需要递归的情况:
std::recursive_mutex r_mu;
int fabonacci(int n) {
r_mu.lock();
// using lock to make sure the cout will be outputting right.
std::cout << "trying to get fabonacci(" << n << ")\n";
if (n == 1 || n == 2) {
r_mu.unlock();
return 1;
} else {
int f1 = fabonacci(n - 1);
int f2 = fabonacci(n - 2);
std::cout << n << " " << f1 << " " << f2 << " " << f1 + f2 << std::endl;
r_mu.unlock();
return f1 + f2;
}
}
每一次获取锁了之后在return
之前都应该记得释放锁,这很不符合RAII
。
为可重入锁try_lock设置时间 std::recursive_timed_mutex
该锁与std::recursive_mutex
非常类似,两者区别与std::timed_mutex
与std::mutex
的区别相同,std::recursive_temed_mutex
可以通过try_lock_for
与try_lock_until
设置尝试的时间,这里不在进行赘述。
共享锁、读写锁 std::shared_mutex
该类型时C++17
开始提供的。
std::shared_mutex
提供两种获取锁的方法lock
与lock_shared
分别对应写锁与读锁:
- 当一个线程获取写锁时,其他的线程不能获取写锁或读锁;
- 当一个线程获取读锁时,其他的线程不能获取写锁但是可以获取读锁。
该锁可以用于实现经典多线程问题:读者和写者问题。
class ThreadSafeCounter {
public:
ThreadSafeCounter() = default;
// Multiple threads/readers can read the counter's value at the same time.
unsigned int get() const {
std::shared_lock lock(mutex_);
return value_;
}
// Only one thread/writer can increment/write the counter's value.
void increment() {
std::unique_lock lock(mutex_);
++value_;
}
// Only one thread/writer can reset/write the counter's value.
void reset() {
std::unique_lock lock(mutex_);
value_ = 0;
}
private:
mutable std::shared_mutex mutex_;
unsigned int value_{};
};
int main() {
ThreadSafeCounter counter;
auto increment_and_print = [&counter]() {
for (int i{}; i != 3; ++i) {
counter.increment();
std::osyncstream(std::cout)
<< std::this_thread::get_id() << ' ' << counter.get() << '\n'; // this osyncstream is since C++ 20.
}
};
std::thread thread1(increment_and_print);
std::thread thread2(increment_and_print);
thread1.join();
thread2.join();
}
上面的例子来自于官网的代码,上面的ThreadSafeCounter
类允许多个线程同时通过get
获取值,但是只能有一个线程进行increment
同时在进行increment
的时候,其他线程不能通过get
获取值。
上面的代码使用了std::shared_lock
与std::unique_lock
这一部分在后文中会进行介绍。
写线程饥饿
使用std::shared_mutex
还有一个很重要的点需要注意:写进程可能会出现饥饿。
考虑这样一种情况,如果此时有个读线程已经获取了lock_shared
此时写线程如果lock
则会被阻塞,而在写线程被阻塞的期间,若有很多的新的读进程进行lock_shared
进行读操作那么写线程必须要等待所有读线程完成操作后,释放所有的读锁后才能获取到写锁,这样的话写操作会被严重延迟。究其原因是C++
的所有锁均为不公平锁。
一种简单的方法是增加一个std::mutex
不管是读操作还是写操作之前都需要获取该mutex
同时在获取到读锁(或写锁)后释放掉该mutex
这样就能保证读线程和写线程在获取所之前会先进行一次竞争操作,便能够保证不破坏语义的情况下,防止写线程饥饿,但是这样会增加一定的开销。
通过RAII获取与释放锁
RAII
指的是Resources Acquisition Is Initialization
。说简单一点就是申请完资源后,不需要进行手动释放,系统自动释放资源。
例如:通过只能指针能够让在使用完资源后,不需要使用free
而是由系统(析构函数)自动释放。
在多线程的开发中,在成功获取了锁之后,还需要通过unlock
操作将锁释放。这样不符合RAII
的思想,于是std
中提供了一些能够在析构的时候自动释放锁的类型和方法。
std::lock_guard
该类型在构造的时候需要与任意一个具有lock
与unlock
方法的锁绑定,对象一旦创建完成便获得了锁,当对象析构的时候便释放了锁,例如:
std::mutex mu;
void threadFunc()
{
std::lock_guard lg{mu};
// acquire the lock
// do something...
} // there is no need to unlock
std::unique_lock
std::lock_guard
实现了RAII
的特点,但是其一旦获得了,便不能通过unlock
进行解锁。而std::unique_lock
可以在获取锁后选择unlock
或者lock
释放与重新获取锁:
std::mutex mu;
void threadFunc()
{
std::unique_lock ul{mu};
// acquire the lock
// do something...
ul.unlock(); // we can unlock if we need this lock no more.
// do something...
ul.lock(); // we can lock if we need this lock again.
} // there is no need to unlock
除此之外,std::unique_lock
的构造函数还能接收第二个参数,其有三种类型:
std::adop_lock
:传入该参入与不传表现一样,即创建完成后便获得了锁;std::try_lock
:传入该操作等价于在创建的时候进行一次try_lock
操作,由于std::unique_lock
重载了bool
运算法可以通过if(name)
的方式来判断是否成功获取锁;std::defer_lock
:传入该操作表示不获取锁,但是后续可以通过lock
获取锁,该方法一般与std::lock
一起使用以实现同时获取多个锁的效果。
std::shared_lock
该类型在C++14
开始支持。
该类型与std::unique_lock
类似唯一不同的是其调用的是绑定的锁的lock_shared
与unlock_shared
方法。该类型常常与std::shared_mutex
一起使用,用于实现std::shared_mutex
中共享锁(读锁)的RAII
。
其同样在构造的时候能够传入第二个参数,参数的类型与std::unique_lock
的第二个参数类型相同。
奇怪的事:std::shared_mutex
是在C++17
才开始支持的,而std::shared_lock
在C++14
就已经支持了,难道还有其他的支持lock_shared
语义的对象我没有发现吗?或者是官方打算让用户自己实现共享锁吗?
同时获取多个锁
std::try_lock
std::try_lock
是一个函数,其接受多个支持try_lock
的锁对象,且同时尝试获取多个锁,若有任意一个的try_lock
获取失败,那么其余已经获取成功的锁会释放,同时返回获取失败的索引下标(从0
开始计算)。如果均try_lock
成功那么该函数返回-1
。
std::mutex m1, m2;
int ret = std::try_lock(m1, m2);
if (ret == -1) {
// do something...
m1.unlock();
m2.unlock(); // don't forget release the locks.
} else {
// m1 and m2 are not got by this thread.
}
上面的代码需要手动释放锁,我们也可以将std::mutex
与std::unique_lock
绑定,以实现RAII
:
std::mutex m1, m2;
std::unique_lock<std::mutex> ul1{m1, std::defer_lock}, ul2{m2, std::defer_lock};
int ret = std::try_lock(m1, m2);
if (ret == -1) {
// do something...
// no need to release the locks.
} else {
// m1 and m2 are not got by this thread.
}
std::lock
std::lock
是同时获取多个锁,当不能获取某个锁的时候会被阻塞,同时保证之前已经获取的锁被释放。其使用方法与std::try_lock
类似,不同的是由于保证一定要同时获得锁之后才不会被阻塞,所以该函数没有返回值。
std::mutex m1, m2;
std::lock(m1, m2); // get m1 and m2 at same time.
// do something...
m1.unlock();
m2.unlock(); // don't forget release the locks.
上面的代码需要手动释放锁,我们也可以将std::mutex
与std::unique_lock
绑定,以实现RAII
:
std::mutex m1, m2;
std::unique_lock<std::mutex> ul1{m1, std::defer_lock}, ul2{m2, std::defer_lock};
std::lock(m1, m2); // get m1 and m2 at same time.
// do something...
// no need to release the locks.
一个小问题
为什么没有实现同时获取多个共享锁(std::lock_shared
)?
实现同时获取多个锁的目的是为了防止因为申请与释放锁的顺序不当防止出现死锁。对于共享锁的
lock_shared
如果能够成功获取,那么其他的线程依然能够通过lock_shared
获取锁,那么此时一定是不会发生死锁的,所以完全没有必要实现一个std::lock_shared
。
条件变量 std::condition_variable
条件变量需要与锁一起使用,提供wait
与notify
这一对操作,通过调用wait
会阻塞当前的线程,同时释放掉与条件变量绑定的锁,而notify
则可以唤醒被阻塞的线程,线程一旦被唤醒其将会重新获取wait
时释放的锁,如果此时锁已经被其他线程获取,那么则会进入阻塞状态,直到成功获取锁。
使用条件变量的时候,通常需要与某些变量相关联,例如某些线程等待其他的线程初始化完成后,这些进程才继续执行,那么就可以通过条件变量来实现,这里给出前面std::promise
中的例子的条件变量实现:
void accumulate(std::vector<int>::iterator first,
std::vector<int>::iterator last) {
std::unique_lock<std::mutex> locker{mu};
sum = std::accumulate(first, last, 0);
cv.notify_all(); // we usually use notify_all, cause we don't know who are waiting.
}
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5, 6};
std::thread work_thread(accumulate, numbers.begin(), numbers.end());
std::unique_lock<std::mutex> locker{mu};
cv.wait(locker, []() { return sum != -1;}); // wait until sum != -1;
std::cout << sum << std::endl;
work_thread.join();
return 0;
}
在使用条件变量的时候我们大多数条件下使用notify_all
进行唤醒,这是因为我们并不知道都有哪些进行被阻塞,当唤醒所有被阻塞进程后,不符合退出wait
条件的进程会继续被阻塞,同时需要注意的时,wait
会先判断是否满足停止条件,而不是先阻塞然后唤醒后再进行判断。
又一个小问题
条件变量在wait
之前需要先获取锁,然后才可以进行wait
操作,那么在notify
之前是否也一定需要获取锁呢?
当然不是。在大多数的情况下,进行
notify
之前需要获取锁是因为往往需要修改共享变量的值。而有些时候,我们在notify
之前可能并不需要修改共享变量的值,那么此时则可以不用获取锁。也就是说锁并不是和notify
关联的,需不需要获取锁是由我们是否需要修改共享变量而决定的,并不是由我们是否需要notify
来决定,只是大多数情况我们在notify
之前需要修改共享变量,这容易给人一种notify
一定需要加锁才能进行的错觉。实际上一种简单的证明notify
并不需要获得锁的方法是:我们在修改完共享变量之后即可释放锁,然后此时调用notify
依然不会出现问题(需要保证之后不再修改共享变量),例如上面的代码如下依然没有问题:
void accumulate(std::vector<int>::iterator first,
std::vector<int>::iterator last) {
std::unique_lock<std::mutex> locker{mu};
sum = std::accumulate(first, last, 0);
locker.unlock();
cv.notify_all(); // we can notify without any locks.
}
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5, 6};
std::thread work_thread(accumulate, numbers.begin(), numbers.end());
std::unique_lock<std::mutex> locker{mu};
cv.wait(locker, []() { return sum != -1;}); // wait until sum != -1;
std::cout << sum << std::endl;
work_thread.join();
return 0;
}
后续
本文只是介绍一些现代C++
多线程常用的类和方法,并没有介绍一些多线程编程的实例。后面会根据实际遇到的问题,介绍一些开发中常见的多线程问题。
参考
std::function cppreference
std::result_of / std::invoke_of cppreference
std::bind cppreference
std::thread cppreference
std::packaged_task cppreference
std::future cppreference
std::promise cppreference
std::shared_future cppreference
std::async cppreference
std::mutex cppreference
std::timed_mutex cppreference
std::recursive_mutex cppreference
std::recursive_timed_mutex cppreference
std::shared_mutex cppreference
std::lock_guard cppreference
std::unique_lock cppreference
std::shared_lock cppreference
std::condition_variable cppreference
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!