folly/Synchronized介绍一种简单抽象的并发互斥技术。用来代替复杂,笨重,容易出错的代码,简单易用,避免错误。
我们很多cpp多线程程序使用锁来共享数据,这是追随一句互斥并发控制的历史格言,将互斥与对象关联,而非代码。思考一下代码:
class RequestHandler {
...
RequestQueue requestQueue_;
SharedMutex requestQueueMutex_;
std::map<std::string, Endpoint> requestEndpoints_;
SharedMutex requestEndpointsMutex_;
HandlerState workState_;
SharedMutex workStateMutex_;
...
};
任何时候代码的读写都需要保护数据,需要互斥读写,示例
void RequestHandler::processRequest(const Request& request) {
stop_watch<> watch;
checkRequestValidity(request);
SharedMutex::WriteHolder lock(requestQueueMutex_);
requestQueue_.push_back(request);
stats_->addStatValue("requestEnqueueLatency", watch.elapsed());
LOG(INFO) << "enqueued request ID " << request.getID();
}
然而正确的技巧应该是完全建立约定,开发者操作这些数据成员必须关注获取的锁以及访问正确的数据,而不是只看到表面的错误。
用Synchronized重写上个代码案例
class RequestHandler {
...
Synchronized<RequestQueue> requestQueue_;
Synchronized<std::map<std::string, Endpoint>> requestEndpoints_;
Synchronized<HandlerState> workState_;
...
};
void RequestHandler::processRequest(const Request& request) {
stop_watch<> watch;
checkRequestValidity(request);
requestQueue_.wlock()->push_back(request);
stats_->addStatValue("requestEnqueueLatency", watch.elapsed());
LOG(INFO) << "enqueued request ID " << request.getID();
}
最大效率完成这次重写,获取锁关联RequestQueue对象,写入队列,操作之后立即释放锁。
从表面上看,没什么值得写出来的,与之前相比也没有明显的提升
这里的特点是不可见的和可见的同等重要
如果你想持有锁完成一系列操作,Synchronized库提供一些选项来做这些。
wlock()方法返回一个LockedPtr对象(或者lock() 如果你不需要共享锁)
对象可以存储一个变量,只要对象存在同时锁可以一直持有,这个对象也可以锁住指向指针下的数据。
{
auto lockedQueue = requestQueue_.wlock();
lockedQueue->push_back(request1);
lockedQueue->push_back(request2);
}
rlock()方法和wlock()方法类似,获取共享锁会好于独占锁
存储一个LockedPtr对象,我们推荐明确一个嵌套区域,可是帮助可视化描绘临界区域,同时明确LockedPtr销毁的时间而不是长时间持有
作为替代方法,Synchronized库提供机制运行函数的时候持有锁,可以在lambdas定义短暂临界区域
void RequestHandler::processRequest(const Request& request) {
stop_watch<> watch;
checkRequestValidity(request);
requestQueue_.withWLock([&](auto& queue) {
// withWLock() automatically holds the lock for the
// duration of this lambda function
queue.push_back(request);
});
stats_->addStatValue("requestEnqueueLatency", watch.elapsed());
LOG(INFO) << "enqueued request ID " << request.getID();
}
withWLock()方法的优点是强制定义一个范围使用临界区域,让临界区域显示的存在于代码中,帮助激励代码可以快速释放
Template class Synchronized
Synchronized模板类需要两个参数,数据类型和锁类型: Synchronized
如果没有特殊要求,默认使用folly::SharedMutex,支持使用folly::LockTraits,folly::LockTraits可以特殊化支持其他自定义互斥锁,这不是常规方法。
当使用共享互斥锁类型或升级互斥锁类型实例化时,Synchronized提供的API与独占互斥锁略有不同
如果实例化是两种互斥类型之一,有一个成员函数lock_shared(),Synchronized对象有一致的wlock, rlock or ulock 方法获取不同的锁类型
当使用共享锁或升级锁,这些API确保调用者显示的选择获取共享锁、独占锁或升级锁,调用者不能无意使用不正确的锁。
rlock API 提供常量访问基本的数据类型,确保是共享锁的时候,不能修改TA。
Constructors
默认构造器默认初始化数据以及相关互斥
Synchronized定义显示构造器,通过对象。
// Default constructed
Synchronized<map<string, int>> syncMap1;
// Copy constructed
Synchronized<map<string, int>> syncMap2(syncMap1);
// Initializing from an existing map
map
init[“world”] = 42;
Synchronized
Assignment, swap, and copying
???
lock()
如果互斥类型使用Synchronized是简单独占锁(不是共享锁),Synchronized 提供lock()方法返回LockedPtr来持有锁访问数据,
The LockedPtr对象通过lock()持有锁会一直存在,最好声明一个单独的内部范围来存储这个变量,以确保一旦不再需要锁,立即销毁LockedPtr:
void fun(Synchronized<vector<string>, std::mutex>& vec) {
{
auto locked = vec.lock();
locked->push_back("hello");
locked->push_back("world");
}
LOG(INFO) << "successfully added greeting";
}
wlock() and rlock()
如果互斥类型使用Synchronized是共享锁,Synchronized 提供wlock()方法获取独占锁,rlock() 方法获取共享锁。
rlock() 只能提供基本常量数据访问,确保不能做修改,直到持有共享锁才能修改。
int computeSum(const Synchronized<vector<int>>& vec) {
int sum = 0;
auto locked = vec.rlock();
for (int n : *locked) {
sum += n;
}
return sum;
}
void doubleValues(Synchronized<vector<int>>& vec) {
auto locked = vec.wlock();
for (int& n : *locked) {
n *= 2;
}
}
这个例子给我们带来了一个值得警惕的讨论,通过lock(), wlock(), or rlock()返回LockedPtr对象只在锁存在时持有锁。
这个对象不持有锁很难访问数据,也不是不可能,特别是你永远不要存储指针或者引用的内部数据存货长于LockedPtr对象。
来个实例,如果我们要在示例中写入以下代码,在锁释放之后会继续访问vector。
// No. NO. NO!
for (int& n : *vec.wlock()) {
n *= 2;
}
内部迭代器被创建vec.wlock()返回的值很快被销毁,迭代器指向向量数据,但在循环执行内部逻辑之前,锁会立即释放
不用说,这是一个漫长调试之夜的罪恶推手
对于初始化语句中使用的对象的生存期,基于范围的for循环稍微有些微妙。
大多数其他有问题的案列比这个更容易发现
因为LockedPtr的生命周期更加明显可见
withLock()
作为替换Lock的接口,Synchronized库还提供了一个withLock()方法,当持有锁的时候执行函数或者lambda表达式。
对比lock()的好处
lambda表达式需要拥有自己的嵌套空间,使用临界区域在代码中可视化
当使用lock(),调用者总被推荐定义新的范围,这么选择是非必要的。
withLock() 确保新的范围总是被定义
因为新的范围是需要的,withLock()总是鼓励用户更快的释放锁资源。
因为临界区域在代码中是可视化的,不会意外放入了毫不相关的代码。
独立的lambda区域使存储受保护数据的原始指针或引用指针,继续在临界区之外使用这些指针变得更加困难
withLock()使的很难导致基于迭代器循环错误发生
vec.withLock([](auto& locked) {
for (int& n : locked) {
n *= 2;
}
});
这类的代码就不会发生跟Lock()在迭代器循环中的错误
当使用Synchronized做共享锁互斥,提供了独立的withWLock() and withRLock()方法来代替withLock()。
Synchronized支持升级和降级互斥锁的级别,和用于实例化同步类型的互斥类型与cpp标准库中的互斥类型具有相同的接口
{
// only const access allowed to the underlying object when an upgrade lock
// is acquired
auto ulock = vec.ulock();
auto newSize = ulock->size();
}
auto newSize = vec.withULockPtr([](auto ulock) {
// only const access allowed to the underlying object when an upgrade lock
// is acquired
return ulock->size();
});
unlock() and scopedUnlock()
Synchronized强制范围内做同步是一个很好的机制,但同样也有局限性,需要临界区是无错的,有时候一些代码需要跳出临界区,但依然还在里面
LockedPtr类提供了unlock()方法解决问题
Synchronized<map<int, string>> dic;
...
{
auto locked = dic.rlock();
auto iter = locked->find(0);
if (iter == locked.end()) {
locked.unlock(); // don't hold the lock while logging
LOG(ERROR) << "key 0 not found";
return false;
}
processValue(*iter);
}
LOG(INFO) << "succeeded";
Synchronized<map<int, string>> dic;
...
{
auto locked = dic.wlock();
auto iter = locked->find(0);
if (iter == locked->end()) {
{
auto unlocker = locked.scopedUnlock();
LOG(INFO) << "Key 0 not found, inserting it."
}
locked->emplace(0, "zero");
} else {
*iter = "zero";
}
}