【问题标题】:asio implicit strand and data synchronizationasio 隐式链和数据同步
【发布时间】:2013-06-01 03:06:42
【问题描述】:

当我阅读 asio 源代码时,我很好奇 asio 如何使数据在线程之间同步,甚至是隐式链。这些是asio中的代码:

io_service::run

 mutex::scoped_lock lock(mutex_);

  std::size_t n = 0;
  for (; do_run_one(lock, this_thread, ec); lock.lock())
    if (n != (std::numeric_limits<std::size_t>::max)())
      ++n;
  return n;

io_service::do_run_one

 while (!stopped_)
  {
    if (!op_queue_.empty())
    {
      // Prepare to execute first handler from queue.
      operation* o = op_queue_.front();
      op_queue_.pop();
      bool more_handlers = (!op_queue_.empty());

      if (o == &task_operation_)
      {
        task_interrupted_ = more_handlers;

        if (more_handlers && !one_thread_)
        {
          if (!wake_one_idle_thread_and_unlock(lock))
            lock.unlock();
        }
        else
          lock.unlock();

        task_cleanup on_exit = { this, &lock, &this_thread };
        (void)on_exit;

        // Run the task. May throw an exception. Only block if the operation
        // queue is empty and we're not polling, otherwise we want to return
        // as soon as possible.
        task_->run(!more_handlers, this_thread.private_op_queue);
      }
      else
      {
        std::size_t task_result = o->task_result_;

        if (more_handlers && !one_thread_)
          wake_one_thread_and_unlock(lock);
        else
          lock.unlock();

        // Ensure the count of outstanding work is decremented on block exit.
        work_cleanup on_exit = { this, &lock, &this_thread };
        (void)on_exit;

        // Complete the operation. May throw an exception. Deletes the object.
        o->complete(*this, ec, task_result);

        return 1;
      }
    }

在它的do_run_one中,mutex的解锁都是在execute handler之前。如果存在隐式链,handler 不会并发执行,但问题是:线程 A 运行一个修改数据的处理程序,线程 B 运行下一个处理程序读取已被线程 A 修改的数据。没有互斥锁的保护,线程B如何看到线程A对数据所做的更改?在处理程序执行之前解锁互斥锁不会在线程访问处理程序访问的数据之间建立发生之前的关系。 当我走得更远时,处理程序执行使用一个名为fenced_block的东西:

 completion_handler* h(static_cast<completion_handler*>(base));
    ptr p = { boost::addressof(h->handler_), h, h };

    BOOST_ASIO_HANDLER_COMPLETION((h));

    // Make a copy of the handler so that the memory can be deallocated before
    // the upcall is made. Even if we're not about to make an upcall, a
    // sub-object of the handler may be the true owner of the memory associated
    // with the handler. Consequently, a local copy of the handler is required
    // to ensure that any owning sub-object remains valid until after we have
    // deallocated the memory here.
    Handler handler(BOOST_ASIO_MOVE_CAST(Handler)(h->handler_));
    p.h = boost::addressof(handler);
    p.reset();

    // Make the upcall if required.
    if (owner)
    {
      fenced_block b(fenced_block::half);
      BOOST_ASIO_HANDLER_INVOCATION_BEGIN(());
      boost_asio_handler_invoke_helpers::invoke(handler, handler);
      BOOST_ASIO_HANDLER_INVOCATION_END;
    }

这是什么?我知道栅栏似乎是 C++11 支持的同步原语,但这个栅栏完全是由 asio 本身编写的。这个 fenced_block 是否有助于完成数据同步工作?

更新

在我google并阅读thisthis之后,asio确实使用内存栅栏原语来同步线程中的数据,这比解锁更快,直到处理程序执行完成(speed difference on x86)。事实上,Java volatile 关键字是通过在 write 之后插入内存屏障来实现的,然后再读取这个变量来建立happens-before关系。

如果有人可以简单地描述 asio 内存栅栏的实现或添加我遗漏或误解的内容,我会接受。

【问题讨论】:

  • 你能详细说明一下你对线程A和B问题的不理解吗?如果因为处理程序通过链(隐式或显式)执行而没有发生并发,那么如果线程 A 修改了 X,为什么在线程 A 之后运行的线程 B 不会观察到对 X 的更改?在没有并发的情况下,互斥锁没有任何好处。
  • 这是可见性问题,它在 java 内存模型中明确定义。 C++ 没有这样的东西(定义明确的内存模型),但我认为由于线程缓存,它是相同的,因为这是 CPU 的一个特性。线程 A 修改数据可能只会修改缓存在自己区域(CPU 缓存)中的副本,而不会触及主内存。如果没有同步,线程 B 可能会或可能不会看到这个更新的值,这取决于 cpu 缓存是否刷新到主内存。但是在c++中,没有所谓的可见性问题,它只是说:每当写入或读取变量时,使用锁

标签: thread-safety boost-asio memory-barriers


【解决方案1】:

在操作调用用户处理程序之前,Boost.Asio 使用memory fence 来提供适当的内存重新排序,而不会强制处理程序执行的相互执行。因此,线程 B 会观察到在线程 A 的上下文中发生的内存变化。

C++03 没有指定关于多线程执行的内存可见性要求。但是,C++11 在 § 1.10 多线程执行和数据竞争以及原子操作和线程支持库部分中定义了这些要求。 Boost 和 C++11 互斥锁执行适当的内存重新排序。对于其他实现,值得检查 mutex 库的文档以验证是否发生了内存重新排序。

Boost.Asio 内存栅栏是一个实现细节,因此总是会发生变化。 Boost.Asio 通过asio/detail/fenced_block.hpp 中的一系列条件定义将自身从架构/编译器特定实现中抽象出来,其中仅包含一个内存屏障实现。底层实现包含在一个类中,通过 typedef 创建一个 fenced_block 别名。

以下是相关摘录:

#elif defined(__GNUC__) && (defined(__hppa) || defined(__hppa__))
# include "asio/detail/gcc_hppa_fenced_block.hpp"
#elif defined(__GNUC__) && (defined(__i386__) || defined(__x86_64__))
# include "asio/detail/gcc_x86_fenced_block.hpp"
#elif ...

...

namespace asio {
namespace detail {

...

#elif defined(__GNUC__) && (defined(__hppa) || defined(__hppa__))
typedef gcc_hppa_fenced_block fenced_block;
#elif defined(__GNUC__) && (defined(__i386__) || defined(__x86_64__))
typedef gcc_x86_fenced_block fenced_block;
#elif ...

...

} // namespace detail
} // namespace asio

内存屏障的实现特定于架构和编译器。 Boost.Asio 有一系列asio/detail/*_fenced_blocked.hpp 头文件。例如,win_fenced_block 使用 InterlockedExchange 表示 Borland;否则,它使用xchg 汇编指令,该指令在与内存地址一起使用时具有隐式锁定前缀。对于gcc_x86_fenced_block,Boost.Asio 使用memory 汇编指令。

如果您发现自己需要使用栅栏,请考虑使用 Boost.Atomic 库。 Boost.Atomic 在 Boost 1.53 中引入,提供了基于 C++11 标准的thread and signal fences 的实现。在将 Boost.Atomic 添加到 Boost 之前,Boost.Asio 一直在使用自己的内存栅栏实现。此外,Boost.Asio 围栏是基于范围的。 fenced_block 将在其构造函数中执行获取,并在其析构函数中执行释放。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-04-01
    • 1970-01-01
    • 2012-12-14
    • 2017-01-17
    相关资源
    最近更新 更多