【问题标题】:C++11 reentrant class locking strategyC++11 可重入类锁定策略
【发布时间】:2012-11-05 20:40:20
【问题描述】:

我有一个使用pimpl 成语的接口,但是该接口需要是可重入的。然而,调用线程不需要知道锁定。这是一个由四部分组成的问题和一部分无偿设计的 C++11 示例(包括示例以解决我遇到的几个类似常见问题的问题:lockingpimplrvalue 和 C++11,其中答案的质量有些可疑)。

在头文件中,example.hpp:

#ifndef EXAMPLE_HPP
#define EXAMPLE_HPP

#include <memory>
#include <string>

#ifndef BOOST_THREAD_SHARED_MUTEX_HPP
# include <boost/thread/shared_mutex.hpp>
#endif

namespace stackoverflow {

class Example final {
public:
  typedef ::boost::shared_mutex shared_mtx_t;
  typedef ::boost::shared_lock< shared_mtx_t > shared_lock_t;
  typedef ::boost::unique_lock< shared_mtx_t > unique_lock_t;

  Example();
  Example(const std::string& initial_foo);

  ~Example();
  Example(const Example&) = delete;             // Prevent copying
  Example& operator=(const Example&) = delete;  // Prevent assignment

  // Example getter method that supports rvalues
  std::string foo() const;

  // Example setter method using perfect forwarding & move semantics. Anything
  // that's std::string-like will work as a parameter.
  template<typename T>
  bool foo_set(T&& new_val);

  // Begin foo_set() variants required to deal with C types (e.g. char[],
  // char*). The rest of the foo_set() methods here are *NOT* required under
  // normal circumstances.

  // Setup a specialization for const char[] that simply forwards along a
  // std::string. This is preferred over having to explicitly instantiate a
  // bunch of const char[N] templates or possibly std::decay a char[] to a
  // char* (i.e. using a std::string as a container is a Good Thing(tm)).
  //
  // Also, without this, it is required to explicitly instantiate the required
  // variants of const char[N] someplace. For example, in example.cpp:
  //
  // template bool Example::foo_set<const char(&)[6]>(char const (&)[6]);
  // template bool Example::foo_set<const char(&)[7]>(char const (&)[7]);
  // template bool Example::foo_set<const char(&)[8]>(char const (&)[8]);
  // ...
  //
  // Eww. Best to just forward to wrap new_val in a std::string and proxy
  // along the call to foo_set<std::string>().
  template<std::size_t N>
  bool foo_set(const char (&new_val)[N]) { return foo_set(std::string(new_val, N)); }

  // Inline function overloads to support null terminated char* && const
  // char* arguments. If there's a way to reduce this duplication with
  // templates, I'm all ears because I wasn't able to generate a templated
  // versions that didn't conflict with foo_set<T&&>().
  bool foo_set(char* new_val)       { return foo_set(std::string(new_val)); }
  bool foo_set(const char* new_val) { return foo_set(std::string(new_val)); }

  // End of the foo_set() overloads.

  // Example getter method for a POD data type
  bool bar(const std::size_t len, char* dst) const;
  std::size_t bar_capacity() const;

  // Example setter that uses a unique lock to access foo()
  bool bar_set(const std::size_t len, const char* src);

  // Question #1: I can't find any harm in making Impl public because the
  // definition is opaque. Making Impl public, however, greatly helps with
  // implementing Example, which does have access to Example::Impl's
  // interface. This is also preferre, IMO, over using friend.
  class Impl;

private:
  mutable shared_mtx_t rw_mtx_;
  std::unique_ptr<Impl> impl_;
};

} // namespace stackoverflow

#endif // EXAMPLE_HPP

然后在实现中:

#include "example.hpp"

#include <algorithm>
#include <cstring>
#include <utility>

namespace stackoverflow {

class Example;
class Example::Impl;


#if !defined(_MSC_VER) || _MSC_VER > 1600
// Congratulations!, you're using a compiler that isn't broken

// Explicitly instantiate std::string variants
template bool Example::foo_set<std::string>(std::string&& src);
template bool Example::foo_set<std::string&>(std::string& src);
template bool Example::foo_set<const std::string&>(const std::string& src);

// The following isn't required because of the array Example::foo_set()
// specialization, but I'm leaving it here for reference.
//
// template bool Example::foo_set<const char(&)[7]>(char const (&)[7]);
#else
// MSVC workaround: msvc_rage_hate() isn't ever called, but use it to
// instantiate all of the required templates.
namespace {
  void msvc_rage_hate() {
    Example e;
    const std::string a_const_str("a");
    std::string a_str("b");
    e.foo_set(a_const_str);
    e.foo_set(a_str);
    e.foo_set("c");
    e.foo_set(std::string("d"));
  }
} // anon namespace
#endif // _MSC_VER



// Example Private Implementation

class Example::Impl final {
public:
  // ctors && obj boilerplate
  Impl();
  Impl(const std::string& init_foo);
  ~Impl() = default;
  Impl(const Impl&) = delete;
  Impl& operator=(const Impl&) = delete;

  // Use a template because we don't care which Lockable concept or LockType
  // is being used, just so long as a lock is held.
  template <typename LockType>
  bool bar(LockType& lk, std::size_t len, char* dst) const;

  template <typename LockType>
  std::size_t bar_capacity(LockType& lk) const;

  // bar_set() requires a unique lock
  bool bar_set(unique_lock_t& lk, const std::size_t len, const char* src);

  template <typename LockType>
  std::string foo(LockType& lk) const;

  template <typename T>
  bool foo_set(unique_lock_t& lk, T&& src);

private:
  // Example datatype that supports rvalue references
  std::string foo_;

  // Example POD datatype that doesn't support rvalue
  static const std::size_t bar_capacity_ = 16;
  char bar_[bar_capacity_ + 1];
};

// Example delegating ctor
Example::Impl::Impl() : Impl("default foo value") {}

Example::Impl::Impl(const std::string& init_foo) : foo_{init_foo} {
  std::memset(bar_, 99 /* ASCII 'c' */, bar_capacity_);
  bar_[bar_capacity_] = '\0'; // null padding
}


template <typename LockType>
bool
Example::Impl::bar(LockType& lk, const std::size_t len, char* dst) const {
  BOOST_ASSERT(lk.owns_lock());
  if (len != bar_capacity(lk))
    return false;
  std::memcpy(dst, bar_, len);

  return true;
}


template <typename LockType>
std::size_t
Example::Impl::bar_capacity(LockType& lk) const {
  BOOST_ASSERT(lk.owns_lock());
  return Impl::bar_capacity_;
}


bool
Example::Impl::bar_set(unique_lock_t &lk, const std::size_t len, const char* src) {
  BOOST_ASSERT(lk.owns_lock());

  // Return false if len is bigger than bar_capacity or the values are
  // identical
  if (len > bar_capacity(lk) || foo(lk) == src)
    return false;

  // Copy src to bar_, a side effect of updating foo_ if they're different
  std::memcpy(bar_, src, std::min(len, bar_capacity(lk)));
  foo_set(lk, std::string(src, len));
  return true;
}


template <typename LockType>
std::string
Example::Impl::foo(LockType& lk) const {
  BOOST_ASSERT(lk.owns_lock());
  return foo_;
}


template <typename T>
bool
Example::Impl::foo_set(unique_lock_t &lk, T&& src) {
  BOOST_ASSERT(lk.owns_lock());
  if (foo_ == src) return false;
  foo_ = std::move(src);
  return true;
}


// Example Public Interface

Example::Example() : impl_(new Impl{}) {}
Example::Example(const std::string& init_foo) : impl_(new Impl{init_foo}) {}
Example::~Example() = default;

bool
Example::bar(const std::size_t len, char* dst) const {
  shared_lock_t lk(rw_mtx_);
  return impl_->bar(lk, len , dst);
}

std::size_t
Example::bar_capacity() const {
  shared_lock_t lk(rw_mtx_);
  return impl_->bar_capacity(lk);
}

bool
Example::bar_set(const std::size_t len, const char* src) {
  unique_lock_t lk(rw_mtx_);
  return impl_->bar_set(lk, len, src);
}

std::string
Example::foo() const {
  shared_lock_t lk(rw_mtx_);
  return impl_->foo(lk);
}

template<typename T>
bool
Example::foo_set(T&& src) {
  unique_lock_t lk(rw_mtx_);
  return impl_->foo_set(lk, std::forward<T>(src));
}

} // namespace stackoverflow

我的问题是:

  1. 是否有更好的方法来处理私有实现内部的锁定?
  2. 鉴于定义不透明,公开 Impl 是否有任何实际危害?
  3. 当使用clang 的-O4 启用Link-Time Optimization 时,链接器应该可以绕过std::unique_ptr 的取消引用开销。有人验证过吗?
  4. 有没有办法调用foo_set("asdf") 并正确获取示例链接?我们在弄清楚const char[6] 的正确显式模板实例化是什么时遇到了问题。现在我已经设置了一个模板特化,它创建一个std::string 对象并代理对 foo_set() 的调用。综合考虑,这似乎是最好的方法,但我想知道如何传递“asdf”和std::decay 结果。

关于锁定策略,出于以下几个原因,我对此产生了明显的偏见:

  • 我可以在适当的情况下将互斥锁更改为独占互斥锁
  • 通过设计 Impl API 以包含所需的锁,锁定语义的编译时保证非常强大
  • 很难忘记锁定某些内容(当这种情况发生时会出现“简单 API”错误,一旦 API 被修复,编译器会再次捕获此错误)
  • 由于 RAII 和 Impl 没有对互斥体的引用,很难将某些内容锁定或创建死锁
  • 使用模板无需从唯一锁降级为共享锁
  • 由于此锁定策略涵盖的代码比实际需要的多,因此需要显式努力将锁从唯一降级为共享,这可以处理非常常见的场景,其中使用共享锁做出的假设需要在进入时重新测试独特的锁定区域
  • 错误修复或 Impl API 更改不需要重新编译整个应用程序,因为 example.hpp 的 API 是外部固定的。

我读到ACE 也使用这种类型的锁定策略,但我欢迎来自 ACE 用户或其他人的一些现实批评:将锁定作为接口的必需部分传递。

为了完整起见,这里有一个 example_main.cpp 供大家参考。

#include <sysexits.h>

#include <cassert>
#include <iostream>
#include <memory>
#include <stdexcept>

#include "example.hpp"

int
main(const int /*argc*/, const char** /*argv*/) {
  using std::cout;
  using std::endl;
  using stackoverflow::Example;

  {
    Example e;
    cout << "Example's foo w/ empty ctor arg: " << e.foo() << endl;
  }

  {
    Example e("foo");
    cout << "Example's foo w/ ctor arg: " << e.foo() << endl;
  }

  try {
    Example e;
    { // Test assignment from std::string
      std::string str("cccccccc");
      e.foo_set(str);
      assert(e.foo() == "cccccccc");  // Value is the same
      assert(str.empty());            // Stole the contents of a_str
    }
    { // Test assignment from a const std::string
      const std::string const_str("bbbbbbb");
      e.foo_set(const_str);
      assert(const_str == "bbbbbbb");               // Value is the same
      assert(const_str.c_str() != e.foo().c_str()); // Made a copy
    }
    {
      // Test a const char[7] and a temporary std::string
      e.foo_set("foobar");
      e.foo_set(std::string("ddddd"));
    }
    { // Test char[7]
      char buf[7] = {"foobar"};
      e.foo_set(buf);
      assert(e.foo() == "foobar");
    }
    { //// And a *char[] & const *char[]
      // Use unique_ptr to automatically free buf
      std::unique_ptr<char[]> buf(new char[7]);
      std::memcpy(buf.get(), "foobar", 6);
      buf[6] = '\0';
      e.foo_set(buf.get());
      const char* const_ptr = buf.get();
      e.foo_set(const_ptr);
      assert(e.foo() == "foobar");
    }

    cout << "Example's bar capacity: " << e.bar_capacity() << endl;
    const std::size_t len = e.bar_capacity();

    std::unique_ptr<char[]> buf(new char[len +1]);

    // Copy bar in to buf
    if (!e.bar(len, buf.get()))
      throw std::runtime_error("Unable to get bar");
    buf[len] = '\0'; // Null terminate the C string
    cout << endl << "foo and bar (a.k.a.) have different values:" << endl;
    cout << "Example's foo value: " << e.foo() << endl;
    cout << "Example's bar value: " << buf.get() << endl;

    // Set bar, which has a side effect of calling foo_set()
    buf[0] = 'c'; buf[1] = buf[2] = '+'; buf[3] = '\0';
    if (!e.bar_set(sizeof("c++") - 1, buf.get()))
      throw std::runtime_error("Unable to set bar");

    cout << endl << "foo and bar now have identical values but only one lock was acquired when setting:" << endl;
    cout << "Example's foo value: " << e.foo() << endl;
    cout << "Example's bar value: " << buf.get() << endl;
  } catch (...) {
    return EX_SOFTWARE;
  }

  return EX_OK;
}

并构建使用C++11libc++的说明:

clang++ -O4 -std=c++11 -stdlib=libc++ -I/path/to/boost/include -o example.cpp.o -c example.cpp
clang++ -O4 -std=c++11 -stdlib=libc++ -I/path/to/boost/include -o example_main.cpp.o -c example_main.cpp
clang++ -O4 -stdlib=libc++ -o example example.cpp.o example_main.cpp.o /path/to/boost/lib/libboost_exception-mt.dylib /path/to/boost/lib/libboost_system-mt.dylib /path/to/boost/lib/libboost_thread-mt.dylib

作为一个小小的奖励,我更新了这个示例,在 foo_set() 方法中使用右值引用包含完美转发。虽然并不完美,但获得正确的模板实例化所需的时间比我预期的要长,这是链接时的一个问题。这还包括 C 基本类型的适当重载,包括:char*const char*char[N]const char[N]

【问题讨论】:

  • 如果你将你的互斥锁设为mutable,你可以将你的方法设为常量。
  • ACE 是可怕的恕我直言 - 我不会把它作为参考 - 虽然这有点像下意识的反应......
  • 额外的包含保护 BOOST_THREAD_SHARED_MUTEX_HPP 不是必需的,可能不会给你任何东西。为什么final?它可以防止一些在测试期间可能有用的技巧。
  • foo_set("asdf") 的特化为char const (&amp;)[5]。更一般地说,字符串文字的特化是template&lt;std::size_t N&gt; x(char const (&amp;)[N])
  • 如果您只想支持一小部分有限的参数,为什么foo_set 是一个具有私有实现的公开可见的模板函数?收集具有您要公开的接口的公共foo_sets,并让标准重载解决方案处理它。他们的单行实现转发给您的foo_set_private&lt;T&gt;。如果您想要真正完美的转发,或者公开foo_set&lt;T&gt; 的实现。您的代码限制了您可以使用 调用 foo_set 的类型,然后隐藏此事实,从而在链接时生成错误。为什么不明确接受您支持的类型?

标签: c++ design-patterns boost c++11 locking


【解决方案1】:

对于问题 1,我想做的一件事是使用 SFINAE 将传入的锁类型限制为 LockType 允许 shared_lock_tunique_lock_t

即:

template <typename LockType>
typename std::enable_if<
  std::is_same< LockType, shared_lock_t > || std::is_same< LockType, unique_lock_t >,
  size_t
>::type 
bar_capacity(LockType& lk) const;

...但这确实有点冗长。

这意味着传递错误类型的 Lock 会给你一个“没有匹配”的错误。另一种方法是让两个不同的bar_capacity 暴露shared_lock_tunique_lock_t,以及一个使用模板LockType 的私有bar_capacity。

正如所写,任何具有.owns_lock() 方法并返回可转换为bool 的类型的类型都是有效的参数...

【讨论】:

    【解决方案2】:

    使用 Pimpl 习惯用法,互斥锁应该是实现的一部分。这将让您掌握何时启动锁。

    顺便说一句,为什么在 lock_guard 就足够的情况下使用 unique_lock?

    我认为公开 impl 没有任何好处。

    std::unique_ptr 对于大多数现代编译器来说应该和指针一样高效。但未验证。

    我会转发 const char[N] foo_set not as

      template<std::size_t N>
      bool foo_set(const char (&new_val)[N]) { return foo_set(std::string(new_val, N)); }
    

    但喜欢

      template<std::size_t N>
      bool foo_set(const char (&new_val)[N]) { return foo_set(N, new_val); }
    

    这避免了在头文件上创建字符串,让实现做任何需要的事情。

    【讨论】:

    • re:Impl 是公开的。我这样做是因为我在.cpp 中使用辅助函数被传递const Impl 引用作为使用编译器强制执行无异常代码的一种方式。但是,这不再是必需的!如果您阅读了这篇博文 codeblurbs.wordpress.com/2012/12/06/lust 中的示例(我需要证明/编辑),使用 Lambda 可以减少对公共 Impl 的需求,我对使用 lambda 来强制编译器保证感到非常兴奋。
    • re:foo_set()中的转发,在例子中编译试试。我向 Scott Meyers 扔了一张纸条回复:正是这个(char*const char*char[]const char[] 的完美转发,解决方案是规范中的一个丑陋的漏洞。如果你弄清楚了(或者我错过了一些明显的东西),我全神贯注。
    • 如您所见,我的建议不是完美转发,而是转发到可以管理特定案例的案例。
    猜你喜欢
    • 1970-01-01
    • 2023-03-25
    • 2012-01-10
    • 1970-01-01
    • 2018-05-27
    • 1970-01-01
    • 1970-01-01
    • 2011-10-12
    • 1970-01-01
    相关资源
    最近更新 更多