如何将一个线程安全的数据用于另一个线程安全的数据?

huangapple go评论65阅读模式
英文:

How to use a thread-safe data into another thread-safe data?

问题

The code you provided appears to be written in C++ and involves thread safety concerns when working with two classes, A and B, each containing a map and mutex for thread-safe access. You have two main concerns:

  1. Data Validity: You are concerned that if a data element is found in A and then deleted by another thread before being updated in B, it could lead to issues.

  2. Deadlock: You are worried about potential deadlocks when trying to lock both A::mut and B::mut if nested locking is used.

Your concerns are valid, and addressing them correctly is essential for maintaining data integrity and avoiding deadlocks.

To address your concerns, you can follow these guidelines:

  1. Data Validity:

    • One approach to ensure data validity is to use a shared lock for reading data from A and then immediately lock B to update it. This minimizes the window of vulnerability where data can be deleted from A by another thread.
    • Alternatively, you can consider using a more advanced data structure that supports atomic operations or a combination of locks to ensure both data structures (A and B) are updated together.
  2. Deadlock:

    • You should avoid nested locking like the one you've mentioned (A::mut and B::mut locked sequentially). Nested locks can indeed lead to deadlocks if not handled carefully.
    • Instead, use a consistent order when acquiring locks. For example, always lock A::mut before B::mut or vice versa to avoid deadlock situations. Make sure all threads follow this locking order.

Here's an example of how you might modify your code to address these concerns:

A a;
B b;
// Initialize a and b

int i = 1;
{
    // Lock A::mut first
    std::lock_guard<std::mutex> lk_a(a.mut);
    if (auto it = a.m.find(i); it != a.m.end()) {
        auto& data = it->second;
        {
            // Then lock B::mut
            std::lock_guard<std::mutex> lk_b(b.mut);
            b.m[i] = data; // Update B with data from A
        }
    }
}

By following a consistent locking order (first lock A::mut, then B::mut in this example), you can reduce the risk of deadlocks. Additionally, the use of a local scope for the locks ({ ... }) ensures that the locks are released when they go out of scope, preventing potential deadlocks.

However, it's important to note that these code snippets provide a basic guideline for addressing your concerns. Depending on the complexity of your actual application, you may need to implement more advanced synchronization techniques or use higher-level concurrency libraries to achieve optimal thread safety.

英文:
using Ta = some copyable UDT;
using Tb = another copyable UDT;

class A {
public:
    map&lt;int, Ta&gt; m;
    mutex mut;
    
    Ta find(int i) {
        lock_guard&lt;mutex&gt; lk(mut);
        if (auto it = m.find(i); it != m.end()) {
            return it-&gt;second;
        }
    }

    void update(int i,  const Ta&amp; data) {
        lock_guard&lt;mutex&gt; lk(mut);
        m[i] = data;
    }
};

class B {
public:
    map&lt;int, Tb&gt; m;
    mutex mut;

    Tb find(int i) {
        lock_guard&lt;mutex&gt; lk(mut);
        if (auto it = m.find(i); it != m.end()) {
            return it-&gt;second;
        }
    }

    void update(int i, const Tb&amp; data) {
        lock_guard&lt;mutex&gt; lk(mut);
        m[i] = data;
    }
};

If codes are like above, and to make it simple, I make all variables public, and A::m and B::m have some relationship, If some data inserts into A::m, it should also insert into B::m, delete as well(i.e. datas should both be valid in A::m and B::m)
Now I want to find some data in A and update it into B

A a;
B b;
some init...
auto data = a.find(1);
b.update(1, process(data));

But I wonder, if some thread just delete the data after a.find(1), and it makes the data actually invalid, and it makes b.update(1, process(data)); actually meanningless too.

But if I write like

lock_guard&lt;mutex&gt; lk(A::mut);
if (auto it = A::m.find(i); it != A::m.end()) {
   auto&amp; data = it-&gt;second;
   {
       lock_guard&lt;mutex&gt; lk(B::mut);
       B::m[i] = data;
    }
}

It makes A::mut and B::mut nested, and I think it could cause deadlock.
So do my two concerns make sense? And is there any perfect solution to use a thread-safe data into another thread-safe data?

答案1

得分: 1

如果某些来自类A的数据(...),然后来自类B的一些数据(...)

每当您需要满足某些不变性条件时,只需将您的逻辑封装在类内部,并在高层级上同步,无需为单个findpush等操作而担心锁。

class Foo {
public:
    void findAndUpdate(int i) {
        std::lock_guard<std::mutex> lk(_m);
        auto data = _mapA.find(1);
        if (data == _mapA.end()) {
            return; // 给出一些诊断信息,抛出异常,或者其他操作
        }
        // 其他逻辑在此处...
        _mapB[i] = process(data->second);
    }

private:
    std::mutex _m;
    std::map<int, Ta> _mapA;
    std::map<int, Tb> _mapB;
};

否则,两个单独的线程安全操作在一起就不再是线程安全的,尝试同时锁定两个互斥量在设计上是没有意义的。

英文:

> If some data from class A (...), then some data from class B (...)

Whenever you have some invariants to satisfy, just encapsulate your logic inside the class and synchronize on a high level, without bothering with locks for a single find, push, etc.

class Foo {
public:
    void findAndUpdate(int i) {
        std::lock_guard&lt;std::mutex&gt; lk(_m);
        auto data = _mapA.find(1);
        if (data == _mapA.end()) {
            return; // give some diagnostics, throw exception, whatever
        }
        // whatever other logic here ...
        _mapB[i] = process(data-&gt;second);
    }

private:
    std::mutex _m;
    std::map&lt;int, Ta&gt; _mapA;
    std::map&lt;int, Tb&gt; _mapB;
};

Otherwise, 2 separate thread-safe operations are not thread-safe together, and trying to simultaneously lock both mutexes doesn't make sense from the design point of view.

huangapple
  • 本文由 发表于 2023年7月13日 19:05:45
  • 转载请务必保留本文链接:https://go.coder-hub.com/76678666.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定