lundi 16 octobre 2023

Why standard doesn't require std::mutex::~mutex synchronizes-with with the latest unlock

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);
    }
}

Is there any part of the standard that guarantees that ~X will never experience race conditions inside ~string without the commented line?

The exclusive access to the object and/or lifetime could be managed by an atomic variable with RELAXED semantics (== no other data except the fact of the end of lifetime is synched). We have a mutex to protect the access to the object, so using relaxed operation seems to be ok to synchronize exclusive access/lifetime and mutex to access the data.

If the standard would require ~mutex to synchronize with the latest unlock and if we moved the declaration of the mutex below the protected data then we could afford default ~X but without this requirement, we need always have explicit destructor which locks all the mutexes used to protect any member.

PS to help understand my question use this example https://en.cppreference.com/w/cpp/thread/mutex there is a comment that accessing g_pages without a lock is safe. Why, which part of the standard guarantees this? The fact that both threads are joined only guarantees no multi-threaded access to the map, but it doesn't guarantee synchronize-with relationship with the latest mutex::unlock operation. I run many different programs trying to expose race conditions and I am pretty sure that mutex uses std::atomic_thread_fence for both lock and unlock which synchronizes with thread.join that must use at least some atomic to work. But the problem is standard doesn't require mutex to use std::atomic_thread_fence and it it just happens that all known to me implementations use it.

#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';
}

Aucun commentaire:

Enregistrer un commentaire