为何C++标准不要求std::mutex::~mutex与最后一次unlock同步及线程安全疑问
问题1:默认析构函数的线程安全性
先看这段代码:
struct X { std::mutex m; std::string str; void set(std::string s) { auto _ = std::unique_lock(m); str = std::move(s); } ~X() { // auto _ = std::unique_lock(m); } };
不启用注释中的加锁代码时,C++标准中是否存在条款能保证~X在执行~string的过程中不会出现竞争条件?
我们可以用带有RELAXED语义的原子变量管理对象的独占访问及生命周期——即仅同步生命周期结束的事实,不同步其他数据。如果用互斥锁保护对象访问,那么用relaxed操作同步独占访问/生命周期、用互斥锁保护数据的方案看似可行。
如果标准要求~mutex与最后一次unlock建立synchronizes-with关系,且将互斥锁声明放在受保护数据之后,我们就能使用默认的~X;但若无此要求,则必须始终编写显式析构函数,对所有用于保护成员的互斥锁加锁。
问题2:示例代码的线程安全性疑问
有一个示例代码中注释指出无需加锁即可安全访问g_pages,这是为何?标准中的哪一条款能保证这一点?线程已join仅能保证不会对map进行多线程访问,但无法保证与最后一次mutex::unlock操作建立synchronizes-with关系。我运行多个程序尝试复现竞争条件,发现mutex的lock和unlock操作似乎都使用了std::atomic_thread_fence,这会与thread.join(其至少需使用某种原子操作实现)同步。但问题在于,标准并未要求mutex必须使用std::atomic_thread_fence,只是我所知的所有实现都这么做了。
示例代码:
#include <chrono> #include <iostream> #include <map> #include <mutex> #include <string> #include <thread> std::map<std::string, std::string> g_pages; std::mutex g_pages_mutex; void save_page(const std::string &url) { // 模拟页面获取耗时 std::this_thread::sleep_for(std::chrono::seconds(2)); std::string result = "fake content"; std::lock_guard<std::mutex> guard(g_pages_mutex); g_pages[url] = result; } int main() { std::thread t1(save_page, "http://foo"); std::thread t2(save_page, "http://bar"); t1.join(); t2.join(); // 线程已join,现在无需加锁即可安全访问g_pages for (const auto &pair : g_pages) std::cout << pair.first << " => " << pair.second << '\n'; }
正如用户@user17732522指出,上述示例因thread.join而保证安全——即使移除互斥锁和其中一个线程(仅保留一个线程),代码依然安全。因此我对示例稍作修改以展示核心问题:注意这里RELAXED内存序很重要:若替换为acquire/release对,代码将完全线程安全;但如果假设std::mutex::unlock使用std::atomic_thread_fence,那么使用RELAXED内存序也是正确的。
修改后的示例代码:
#include <atomic> #include <chrono> #include <iostream> #include <map> #include <mutex> #include <string> #include <thread> std::map<std::string, std::string> g_pages; std::mutex g_pages_mutex; std::atomic_flag g_f1 = {}; std::atomic_flag g_f2 = {}; void save_page(const std::string& url, std::atomic_flag* flag) { // 模拟页面获取耗时 std::this_thread::sleep_for(std::chrono::seconds(2)); std::string result = "fake content"; { std::lock_guard<std::mutex> guard(g_pages_mutex); g_pages[url] = result; } flag->clear(std::memory_order_relaxed); } int main() { g_f1.test_and_set(); g_f2.test_and_set(); std::thread t1(save_page, "http://foo", &g_f1); std::thread t2(save_page, "http://bar", &g_f2); while (g_f1.test_and_set(std::memory_order_relaxed)); while (g_f2.test_and_set(std::memory_order_relaxed)); // 线程已发出完成信号,现在无需加锁访问g_pages是否安全? for (const auto& pair : g_pages) std::cout << pair.first << " => " << pair.second << '\n'; t1.join(); t2.join(); }
内容的提问来源于stack exchange,提问作者Anton Dyachenko

