如果已知访问顺序是安全的,如何在没有互斥体的情况下同步 threads/CPUs?
How to synchronize threads/CPUs without mutexes if sequence of access is known to be safe?
考虑以下因素:
// these services are running on different threads that are started a long time ago
std::vector<io_service&> io_services;
struct A {
std::unique_ptr<Object> u;
} a;
io_services[0].post([&io_services, &a] {
std::unique_ptr<Object> o{new Object};
a.u = std::move(o);
io_services[1].post([&a] {
// as far as I know changes to `u` isn't guaranteed to be seen in this thread
a.u->...;
});
});
实际代码将结构传递给一堆不同的 boost::asio::io_service
对象,并且结构的每个字段由不同的服务对象填充 (永远不会从不同的 io_service objects/threads 同时在服务之间通过引用传递,直到处理完成).
据我所知,当我在线程之间传递任何内容时,即使没有 read/write 竞争(如同时访问),我总是需要某种显式 synchronization/memory 刷新。这种情况下正确的做法是什么?
请注意,对象 不属于我,不可复制或移动。我可以使用 std::atomic<Object*>
(如果我没记错的话),但我宁愿使用智能指针。有办法吗?
编辑:
看起来 std::atomic_thread_fence 是完成这项工作的工具,但我无法真正包装 'memory model' 概念以安全编码。
我的理解是此代码需要以下几行才能正常工作。真的是这样吗?
// these services are running on different threads that are started a long time ago
std::vector<io_service&> io_services;
struct A {
std::unique_ptr<Object> u;
} a;
io_services[0].post([&io_services, &a] {
std::unique_ptr<Object> o{new Object};
a.u = std::move(o);
std::atomic_thread_fence(std::memory_order_release);
io_services[1].post([&a] {
std::atomic_thread_fence(std::memory_order_acquire);
a.u->...;
});
});
只有在没有同步的情况下才会出现数据竞争时才需要同步。数据竞争被定义为不同线程的无序访问。
您没有这种未排序的访问权限。 t.join()
保证后面的所有语句都严格排在 运行 作为 t
的一部分的所有语句之后。所以不需要同步。
详细说明:(解释为什么 thread::join
具有上述声明的属性)首先,来自标准 [thread.thread.member] 的 thread::join
的描述:
void join();
- Requires: joinable() is true.
- Effects: Blocks until
the thread represented by *this has completed.
- Synchronization: The
completion of the thread represented by *this synchronizes with (1.10)
the corresponding successful join() return.
a).上面说明join()
提供了同步(具体:*this代表的线程完成与调用join()
的外线程同步)。下一个 [intro.multithread]:
- An evaluation A inter-thread happens before an evaluation B if
(13.1) — A synchronizes with B, or ...
这表明,由于 a),我们有 t
线程间的完成发生在 的 return 之前 join()
打电话。
最后,[intro.multithread]:
- Two actions are potentially concurrent if
(23.1) — they are performed
by different threads, or
(23.2) — they are unsequenced, and at least
one is performed by a signal handler.
The execution of a program
contains a data race if it contains two potentially concurrent
conflicting actions, at least one of which is not atomic, and neither
happens before the other ...
以上描述了数据竞争的必要条件。 t.join()
的情况不满足这些条件,因为如图所示,t
的完成实际上 发生在 的 return 之前=14=].
因此不存在数据竞争,所有数据访问都保证有明确定义的行为。
(我想说的是,自从@Smeeheey 回答问题以来,您似乎已经以某种重要的方式改变了您的问题;本质上,他回答了您最初措辞的问题,但不能因此得到认可因为你问了两个不同的问题。这是糟糕的形式 - 以后,请 post 一个新问题,这样原始回答者就可以得到应得的信用。)
如果多个线程read/write一个变量,即使你知道该变量是按定义的顺序访问的,你必须 仍然通知编译器。执行此操作的正确方法必然涉及同步、原子或记录在案以执行先验之一的内容(例如 std::thread::join
)。假定同步路由在实现中既明显又不受欢迎..:[=17=]
用原子学解决这个问题可能只包含std::atomic_thread_fence
;但是,C++ 中的获取栅栏不能单独 同步 释放栅栏——必须修改实际的原子对象。因此,如果你想单独使用栅栏,你需要指定 std::memory_order_seq_cst
;完成后,您的代码将按其他方式工作。
如果你想坚持使用 release/acquire 语义,幸运的是即使是最简单的原子也可以 – std::atomic_flag
:
std::vector<io_service&> io_services;
struct A {
std::unique_ptr<Object> u;
} a;
std::atomic_flag a_initialized = ATOMIC_FLAG_INIT;
io_services[0].post([&io_services, &a, &a_initialized] {
std::unique_ptr<Object> o{new Object};
a_initialized.clear(std::memory_order_release); // initiates release sequence (RS)
a.u = std::move(o);
a_initialized.test_and_set(std::memory_order_relaxed); // continues RS
io_services[1].post([&a, &a_initialized] {
while (!a_initialized.test_and_set(std::memory_order_acquire)) ; // completes RS
a.u->...;
});
});
有关发布顺序的信息,请参阅 here。
考虑以下因素:
// these services are running on different threads that are started a long time ago
std::vector<io_service&> io_services;
struct A {
std::unique_ptr<Object> u;
} a;
io_services[0].post([&io_services, &a] {
std::unique_ptr<Object> o{new Object};
a.u = std::move(o);
io_services[1].post([&a] {
// as far as I know changes to `u` isn't guaranteed to be seen in this thread
a.u->...;
});
});
实际代码将结构传递给一堆不同的 boost::asio::io_service
对象,并且结构的每个字段由不同的服务对象填充 (永远不会从不同的 io_service objects/threads 同时在服务之间通过引用传递,直到处理完成).
据我所知,当我在线程之间传递任何内容时,即使没有 read/write 竞争(如同时访问),我总是需要某种显式 synchronization/memory 刷新。这种情况下正确的做法是什么?
请注意,对象 不属于我,不可复制或移动。我可以使用 std::atomic<Object*>
(如果我没记错的话),但我宁愿使用智能指针。有办法吗?
编辑: 看起来 std::atomic_thread_fence 是完成这项工作的工具,但我无法真正包装 'memory model' 概念以安全编码。 我的理解是此代码需要以下几行才能正常工作。真的是这样吗?
// these services are running on different threads that are started a long time ago
std::vector<io_service&> io_services;
struct A {
std::unique_ptr<Object> u;
} a;
io_services[0].post([&io_services, &a] {
std::unique_ptr<Object> o{new Object};
a.u = std::move(o);
std::atomic_thread_fence(std::memory_order_release);
io_services[1].post([&a] {
std::atomic_thread_fence(std::memory_order_acquire);
a.u->...;
});
});
只有在没有同步的情况下才会出现数据竞争时才需要同步。数据竞争被定义为不同线程的无序访问。
您没有这种未排序的访问权限。 t.join()
保证后面的所有语句都严格排在 运行 作为 t
的一部分的所有语句之后。所以不需要同步。
详细说明:(解释为什么 thread::join
具有上述声明的属性)首先,来自标准 [thread.thread.member] 的 thread::join
的描述:
void join();
- Requires: joinable() is true.
- Effects: Blocks until the thread represented by *this has completed.
- Synchronization: The completion of the thread represented by *this synchronizes with (1.10) the corresponding successful join() return.
a).上面说明join()
提供了同步(具体:*this代表的线程完成与调用join()
的外线程同步)。下一个 [intro.multithread]:
- An evaluation A inter-thread happens before an evaluation B if
(13.1) — A synchronizes with B, or ...
这表明,由于 a),我们有 t
线程间的完成发生在 的 return 之前 join()
打电话。
最后,[intro.multithread]:
- Two actions are potentially concurrent if
(23.1) — they are performed by different threads, or
(23.2) — they are unsequenced, and at least one is performed by a signal handler.
The execution of a program contains a data race if it contains two potentially concurrent conflicting actions, at least one of which is not atomic, and neither happens before the other ...
以上描述了数据竞争的必要条件。 t.join()
的情况不满足这些条件,因为如图所示,t
的完成实际上 发生在 的 return 之前=14=].
因此不存在数据竞争,所有数据访问都保证有明确定义的行为。
(我想说的是,自从@Smeeheey 回答问题以来,您似乎已经以某种重要的方式改变了您的问题;本质上,他回答了您最初措辞的问题,但不能因此得到认可因为你问了两个不同的问题。这是糟糕的形式 - 以后,请 post 一个新问题,这样原始回答者就可以得到应得的信用。)
如果多个线程read/write一个变量,即使你知道该变量是按定义的顺序访问的,你必须 仍然通知编译器。执行此操作的正确方法必然涉及同步、原子或记录在案以执行先验之一的内容(例如 std::thread::join
)。假定同步路由在实现中既明显又不受欢迎..:[=17=]
用原子学解决这个问题可能只包含std::atomic_thread_fence
;但是,C++ 中的获取栅栏不能单独 同步 释放栅栏——必须修改实际的原子对象。因此,如果你想单独使用栅栏,你需要指定 std::memory_order_seq_cst
;完成后,您的代码将按其他方式工作。
如果你想坚持使用 release/acquire 语义,幸运的是即使是最简单的原子也可以 – std::atomic_flag
:
std::vector<io_service&> io_services;
struct A {
std::unique_ptr<Object> u;
} a;
std::atomic_flag a_initialized = ATOMIC_FLAG_INIT;
io_services[0].post([&io_services, &a, &a_initialized] {
std::unique_ptr<Object> o{new Object};
a_initialized.clear(std::memory_order_release); // initiates release sequence (RS)
a.u = std::move(o);
a_initialized.test_and_set(std::memory_order_relaxed); // continues RS
io_services[1].post([&a, &a_initialized] {
while (!a_initialized.test_and_set(std::memory_order_acquire)) ; // completes RS
a.u->...;
});
});
有关发布顺序的信息,请参阅 here。