为什么标准不需要 std::mutex::~mutex 与最新解锁同步

Ant*_*nko 4 c++ mutex c++11

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);
    }
}
Run Code Online (Sandbox Code Playgroud)

标准中是否有任何部分可以保证在没有注释行的情况下~X永远不会在内部遇到竞争条件?~string

对对象和/或生命周期的独占访问可以由具有 RELAXED 语义的原子变量来管理(==除了生命周期结束的事实是同步的之外,没有其他数据)。我们有一个互斥体来保护对对象的访问,因此使用宽松的操作似乎可以同步独占访问/生命周期和互斥体来访问数据。

如果标准要求~mutex与最新同步unlock,并且如果我们将互斥体的声明移到受保护数据下方,那么我们可以承受默认值,~X但如果没有此要求,我们始终需要有显式析构函数来锁定用于保护任何成员的所有互斥体。

PS为了帮助理解我的问题,请使用这个例子https://en.cppreference.com/w/cpp/thread/mutex,有一条评论说g_pages没有锁的访问是安全的。为什么,标准的哪一部分保证了这一点?两个线程连接的事实仅保证不会对映射进行多线程访问,但不能保证与最新mutex::unlock操作的同步关系。我运行许多不同的程序试图暴露竞争条件,并且我非常确定两者的mutex使用以及与之同步的程序必须至少使用一些原子才能工作。但问题是标准不需要使用,而我所知的所有实现都恰好使用它。std::atomic_thread_fencelockunlockthread.joinmutexstd::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)
{
    // simulate a long page fetch
    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();
 
    // safe to access g_pages without lock now, as the threads are joined
    for (const auto &pair : g_pages)
        std::cout << pair.first << " => " << pair.second << '\n';
}
Run Code Online (Sandbox Code Playgroud)

PPS 正如 @user17732522 所指出的,上面的例子保证是安全的,因为thread.join. 您可以删除互斥锁和其中一个线程(只需使用 1 个线程),这样就安全了。所以我稍微修改了这个例子来演示这个问题。请注意,宽松的内存顺序在这里很重要。如果我们用一对获取/释放来替换它,它将是完全线程安全的代码,但如果我们假设std::mutex::unlock使用,那么使用relaxed也是正确的std::atomic_thread_fence

#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)
{
    // simulate a long page fetch
    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));

    // whether it safe to access g_pages without lock now, as the threads signaled they are done?
    for (const auto& pair : g_pages)
        std::cout << pair.first << " => " << pair.second << '\n';

    t1.join();
    t2.join();
}
Run Code Online (Sandbox Code Playgroud)

bit*_*ask 9

有点。该标准确实要求您不要销毁生命周期已结束的对象。如果您遇到多个线程尝试X::~X()在同一个对象上运行的情况,那么您已经处于 UB 状态了。

~X对于一个尝试某些对象的线程x和另一个尝试x.set同一个对象的线程,可以提出类似的论点。如果您遇到这种情况,您就已经违反了x. UB 源于比赛x.str就是由此产生的结果。

因此,您永远不应该发现自己处于需要在您拥有的析构函数中锁定的情况X。如果多个线程正在访问同一个x对象,则该对象可能属于线程池之外。