一、并行编程的同步
在并行和多线程编程中,一个难点在于多个锁的顺序处理问题。这不小心就有可能引起死锁,所以处理起来一定是慎之又慎。另外多个锁就引出另外一个问题,锁的控制粒度大小。而粒度的大小又可能明显的引起效率的变动。如果锁更多呢?某个锁的内部产生异常怎么办?当然这都有解决方法,但是不是看上去很复杂的样子。
有痛点就会有解决方案,C++17中提供了一个std::scoped_lock。
二、std::scoped_lock
std::scoped_lock,可以理解成域锁,原来是锁一个互斥体,现在是锁一片互斥体。它是采用了RAII机制的一种实现方式并可以通过std::lock的机制来避免死锁。它一下子解决了上面提到的不少的问题。进步从来不是一蹴而就。先看一下其定义:
template<typename... _MutexTypes>
class scoped_lock
{
public:
explicit scoped_lock(_MutexTypes&... __m) : _M_devices(std::tie(__m...))
{ std::lock(__m...); }
explicit scoped_lock(adopt_lock_t, _MutexTypes&... __m) noexcept
: _M_devices(std::tie(__m...))
{ } // calling thread owns mutex
~scoped_lock()
{ std::apply([](auto&... __m) { (__m.unlock(), ...); }, _M_devices); }
scoped_lock(const scoped_lock&) = delete;
scoped_lock& operator=(const scoped_lock&) = delete;
private:
tuple<_MutexTypes&...> _M_devices;
};
template<>
class scoped_lock<>
{
public:
explicit scoped_lock() = default;
explicit scoped_lock(adopt_lock_t) noexcept { }
~scoped_lock() = default;
scoped_lock(const scoped_lock&) = delete;
scoped_lock& operator=(const scoped_lock&) = delete;
};
template<typename _Mutex>
class scoped_lock<_Mutex>
{
public:
using mutex_type = _Mutex;
explicit scoped_lock(mutex_type& __m) : _M_device(__m)
{ _M_device.lock(); }
explicit scoped_lock(adopt_lock_t, mutex_type& __m) noexcept
: _M_device(__m)
{ } // calling thread owns mutex
~scoped_lock()
{ _M_device.unlock(); }
scoped_lock(const scoped_lock&) = delete;
scoped_lock& operator=(const scoped_lock&) = delete;
private:
mutex_type& _M_device;
};
代码不复杂,一个标准的多参处理模板类,一个全特化的类,一个单独参数的模板类,用来分别处理不同的情况。这样看来写代码的确实是想的周到。
三、例程
看一个例程(cppreferenct.com):
#include <chrono>
#include <functional>
#include <iostream>
#include <mutex>
#include <string>
#include <thread>
#include <vector>
using namespace std::chrono_literals;
struct Employee
{
std::vector<std::string> lunch_partners;
std::string id;
std::mutex m;
Employee(std::string id) : id(id) {}
std::string partners() const
{
std::string ret = "Employee " + id + " has lunch partners: ";
for (int count{}; const auto& partner : lunch_partners)
ret += (count++ ? ", " : "") + partner;
return ret;
}
};
void send_mail(Employee&, Employee&)
{
// Simulate a time-consuming messaging operation
std::this_thread::sleep_for(1s);
}
void assign_lunch_partner(Employee& e1, Employee& e2)
{
static std::mutex io_mutex;
{
std::lock_guard<std::mutex> lk(io_mutex);
std::cout << e1.id << " and " << e2.id << " are waiting for locks" << std::endl;
}
{
// Use std::scoped_lock to acquire two locks without worrying about
// other calls to assign_lunch_partner deadlocking us
// and it also provides a convenient RAII-style mechanism
std::scoped_lock lock(e1.m, e2.m);
// Equivalent code 1 (using std::lock and std::lock_guard)
// std::lock(e1.m, e2.m);
// std::lock_guard<std::mutex> lk1(e1.m, std::adopt_lock);
// std::lock_guard<std::mutex> lk2(e2.m, std::adopt_lock);
// Equivalent code 2 (if unique_locks are needed, e.g. for condition variables)
// std::unique_lock<std::mutex> lk1(e1.m, std::defer_lock);
// std::unique_lock<std::mutex> lk2(e2.m, std::defer_lock);
// std::lock(lk1, lk2);
{
std::lock_guard<std::mutex> lk(io_mutex);
std::cout << e1.id << " and " << e2.id << " got locks" << std::endl;
}
e1.lunch_partners.push_back(e2.id);
e2.lunch_partners.push_back(e1.id);
}
send_mail(e1, e2);
send_mail(e2, e1);
}
int main()
{
Employee alice("Alice"), bob("Bob"), christina("Christina"), dave("Dave");
// Assign in parallel threads because mailing users about lunch assignments
// takes a long time
std::vector<std::thread> threads;
threads.emplace_back(assign_lunch_partner, std::ref(alice), std::ref(bob));
threads.emplace_back(assign_lunch_partner, std::ref(christina), std::ref(bob));
threads.emplace_back(assign_lunch_partner, std::ref(christina), std::ref(alice));
threads.emplace_back(assign_lunch_partner, std::ref(dave), std::ref(bob));
for (auto& thread : threads)
thread.join();
std::cout << alice.partners() << '\n' << bob.partners() << '\n'
<< christina.partners() << '\n' << dave.partners() << '\n';
}
代码自己跑一下,没有什么特殊的。但要根据定义来和例程分析一下应用的过程,真正掌握,然后在工程上引入应用就会掌握。这里需要提醒就是scoped_lock支持的互斥体有很多(比如std::recursive_mutex等),所以如果是复杂情况的应用,千万要小心。
四、总结
技术的进步就是朝着简化开发者的应用的方向展开的。包括现在的AI,虽然说当下AI取代程序员可能有点忽悠的成分,但未来这种是大概率的事件。至少大多数的普通程序员被AI替代是一种非常可能的情况。这个时间可能不会很长。但这并不代表程序员的消失,反而有可能会出现另外一种形式的程序员。
大家拭目以待吧!