Folly库中文文档-Synchronized

介绍

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();
}

然而正确的技巧应该是完全建立约定,开发者操作这些数据成员必须关注获取的锁以及访问正确的数据,而不是只看到表面的错误。

  1. 操作一系列数据,没有第一时间获取锁
  2. 获取到不同的锁代替了正确的锁
  3. 获取到读锁但修改了数据结构
  4. 虽然获取到读写锁,仅仅是能访问的常量数据

介绍folly/Synchronized.h

用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对象,写入队列,操作之后立即释放锁。
从表面上看,没什么值得写出来的,与之前相比也没有明显的提升
这里的特点是不可见的和可见的同等重要

  1. 在这之前,数据和互斥保护解不开集合在一起。
  2. 如果你尝试使用requestQueue_没有获取锁,就做不了数据保护,但是这几乎不可能不获取锁,而去访问数据。
  3. 写入操作执行完成就会立即释放锁,不需要保持锁做多余的操作

如果你想持有锁完成一系列操作,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;
init[“world”] = 42;
Synchronized> syncMap3(init);
EXPECT_EQ(syncMap3->size(), 1);

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()的好处

  1. lambda表达式需要拥有自己的嵌套空间,使用临界区域在代码中可视化
    当使用lock(),调用者总被推荐定义新的范围,这么选择是非必要的。
    withLock() 确保新的范围总是被定义

  2. 因为新的范围是需要的,withLock()总是鼓励用户更快的释放锁资源。
    因为临界区域在代码中是可视化的,不会意外放入了毫不相关的代码。

  3. 独立的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";
  }
}

你可能感兴趣的:(C++,Folly库)