简体   繁体   English

关于跨多个线程的shared_mutex和shared_ptr

[英]About shared_mutex and shared_ptr across multiple threads

I implemented code such that multiple instances running on different threads reads other instances' data using reader-writer lock and shared_ptr.我实现了这样的代码,以便在不同线程上运行的多个实例使用读写锁和 shared_ptr 读取其他实例的数据。 It seemed fine, but I am not 100% sure about that and I came up with some questions about usage of those.看起来不错,但我对此不是 100% 确定的,我提出了一些关于这些用法的问题。

Detail细节

I have multiple instances of a class called Chunk and each instance does some calculations in a dedicated thread.我有多个名为 Chunk 的 class 实例,每个实例都在专用线程中进行一些计算。 A chunk needs to read neighbour chunks' data as well as its own data, but it doesn't write neighbours' data, so reader-writer lock is used.一个块需要读取相邻块的数据以及它自己的数据,但它不写入邻居的数据,所以使用读写锁。 Also, neighbours can be set at runtime.此外,可以在运行时设置邻居。 For example, I might want o set a different neighbour chunk at runtime, sometimes just nullptr.例如,我可能想在运行时设置一个不同的邻居块,有时只是 nullptr。 It is possible to delete a chunk at runtime, too.也可以在运行时删除块。 Raw pointers can be used but I thought shared_ptr and weak_ptr are better for this, in order to keep track of the lifetime.可以使用原始指针,但我认为 shared_ptr 和 weak_ptr 更适合这个,以便跟踪生命周期。 Own data in shared_ptr and neighbours' data in weak_ptr. shared_ptr 中自己的数据和weak_ptr 中邻居的数据。

I provided a simpler version of my code below.我在下面提供了我的代码的更简单版本。 ChunkData has data and a mutex for it. ChunkData 有数据和一个互斥体。 I use InitData for data initialization and DoWork function is called in a dedicated thread after that.我使用 InitData 进行数据初始化,之后在专用线程中调用 DoWork function。 other functions can be called from main thread.其他函数可以从主线程调用。 This seems to work, but I am not so confident.这似乎有效,但我不是那么自信。 Especially, about use of shared_ptr across multiple threads.特别是关于在多个线程中使用 shared_ptr 。

  1. What happens if a thread calls shared_ptr's reset() (in ctor and InitData) and other uses it with weak_ptr's lock (in DoWork)?如果一个线程调用 shared_ptr 的 reset()(在 ctor 和 InitData 中)而其他线程将它与 weak_ptr 的锁(在 DoWork 中)一起使用,会发生什么? Does this need a lock dataMutex or chunkMutex?这需要锁dataMutex或chunkMutex吗?

  2. How about copy(in SetNeighbour)?复制(在 SetNeighbour 中)怎么样? Do I need locks for this as well?我也需要锁吗?

I think other parts are ok, but please let me know if you find anything dangerous.我认为其他部分还可以,但是如果您发现任何危险,请告诉我。 Appreciate that.感谢。

By the way, I considered about storing shared_ptr of Chunk instead of ChunkData, but decided not to use this method because internal code, which I don't manage, has GC system and it can delete a pointer to Chunk when I don't expect it.顺便说一句,我考虑过存储 Chunk 的 shared_ptr 而不是 ChunkData,但决定不使用这种方法,因为我不管理的内部代码具有 GC 系统,它可以在我不期望的时候删除指向 Chunk 的指针它。

class Chunk
{
public:
    class ChunkData
    {
    public:

        shared_mutex dataMutex; // mutex to read/write data
        int* data;
        int size;

        ChunkData() : data(nullptr) { }

        ~ChunkData()
        {
            if (data)
            {
                delete[] data;
                data = nullptr;
            }
        }
    };

private:
    mutex chunkMutex;   // mutex to read/write member variables
    shared_ptr<ChunkData> chunkData;
    weak_ptr<ChunkData> neighbourChunkData;

public:
    Chunk(string _name)
        : chunkData(make_shared<ChunkData>())
    {
    }

    ~Chunk()
    {
        EndProcess();
        unique_lock lock(chunkMutex);   // is this needed?
        chunkData.reset();
    }

    void InitData(int size)
    {
        ChunkData* NewData = new ChunkData();
        NewData->size = size;
        NewData->data = new int[size];

        {
            unique_lock lock(chunkMutex);   // is this needed?
            chunkData.reset(NewData);
            cout << "init chunk " << name << endl;
        }
    }

    // This is executed in other thread. e.g. thread t(&Chunk::DoWork, this);
    void DoWork()
    {
        lock_guard lock(chunkMutex); // we modify some members reading chunk data, so need this.
        if (chunkData)
        {
            shared_lock readLock(chunkData->dataMutex);
            if (chunkData->data)
            {
                // ready chunkData->data[i] and modify some members
            }
        }
        // does this work?
        if (shared_ptr<ChunkData> neighbour = neighbourChunkData.lock())
        {
            shared_lock readLock(neighbour->dataMutex);
            if (neighbour->data)
            {
                // ready neighbour->data[i] and modify some members
            }
        }
    }

    shared_ptr<ChunkData> GetChunkData()
    {
        unique_lock lock(chunkMutex);
        return chunkData;
    }

    void SetNeighbour(Chunk* neighbourChunk)
    {
        if (neighbourChunk)
        {
            // safe?
            shared_ptr<ChunkData> newNeighbourData = neighbourChunk->GetChunkData();
            unique_lock lock(chunkMutex);   // lock for chunk properties
            {
                shared_lock readLock(newNeighbourData->dataMutex);  // not sure if this is needed.
                neighbourChunkData = newNeighbourData;
            }
        }
    }

    int GetDataAt(int index)
    {
        shared_lock readLock(chunkData->dataMutex);
        if (chunkData->data && 0 <= index && index < chunkData->size)
        {
            return chunkData->data[index];
        }
        return 0;
    }

    void SetDataAt(int index, int element)
    {
        unique_lock writeLock(chunkData->dataMutex);
        if (chunkData->data && 0 <= index && index < chunkData->size)
        {
            chunkData->data[index] = element;
        }
    }
};

I have several remarks:我有几点意见:

  1. ~ChunkData : You could change your data member from int* to unique_ptr<int[]> to get the same result without an explicit destructor. ~ChunkData :您可以将data成员从int*更改为unique_ptr<int[]>以获得相同的结果,而无需显式析构函数。 Your code is correct though, just less convenient.您的代码是正确的,只是不太方便。

  2. ~Chunk : I don't think you need a lock or call the reset method. ~Chunk :我认为您不需要锁或调用重置方法。 By the time the destructor runs, by definition, no one should have a reference to the Chunk object.到析构函数运行时,根据定义,没有人应该引用 Chunk object。 So the lock can never be contested.所以锁永远不会有争议。 And reset is unnecessary because the shared_ptr destructor will handle that.并且重置是不必要的,因为shared_ptr析构函数会处理它。

  3. InitData : Yes, the lock is needed because InitData can race with DoWork. InitData :是的,需要锁,因为 InitData 可以与 DoWork 竞争。 You could avoid this by moving InitData to the constructor but I assume there are reasons for this division.您可以通过将 InitData 移动到构造函数来避免这种情况,但我认为这种划分是有原因的。 You could also change the shared_ptr to std::atomic<std::shared_ptr<ChunkData> > to avoid the lock.您还可以将shared_ptr更改为std::atomic<std::shared_ptr<ChunkData> >以避免锁定。

  4. It is more efficient to write InitData like this:像这样编写 InitData 效率更高:

void InitData(int size)
{
    std::shared_ptr<ChunkData> NewData = std::make_shared<ChunkData>();
    NewData->size = size;
    NewData->data = new int[size]; // or std::make_unique<int[]>(size)
    {
        std::lock_guard<std::mutex> lock(chunkMutex);
        chunkData.swap(NewData);
    }
    // deletes old chunkData outside locked region if it was initialized before 
}

make_shared avoids an additional memory allocation for the reference counter. make_shared避免了为引用计数器分配额外的 memory。 This also moves all allocations and deallocations out of the critical section.这也将所有分配和释放移出临界区。

  1. DoWork : Your comment "ready chunkData->data[i] and modify some members". DoWork :您的评论“准备好 chunkData->data[i] 并修改一些成员”。 You only take a shared_lock but say that you modify members.你只拿了一个shared_lock但说你修改了成员。 Well, which is it, reading or writing?好吧,它是阅读还是写作? Or do you mean to say that you modify Chunk but not ChunkData, with Chunk being protected by its own mutex?或者您的意思是说您修改了 Chunk 而不是 ChunkData,而 Chunk 受其自己的互斥体保护?

  2. SetNeighbour : You need to lock both your own chunkMutex and the neighbour's. SetNeighbour :您需要锁定自己的 chunkMutex 和邻居的。 You should not lock both at the same time to avoid the dining philosopher's problem (though std::lock solves this).您不应该同时锁定两者以避免哲学家就餐的问题(尽管std::lock解决了这个问题)。

    void SetNeighbour(Chunk* neighbourChunk)
    {
        if(! neighbourChunk)
            return;
        std::shared_ptr<ChunkData> newNeighbourData;
        {
            std::lock_guard<std::mutex> lock(neighbourChunk->chunkMutex);
            newNeighbourData = neighbourChunk->chunkData;
        }
        std::lock_guard<std::mutex> lock(this->chunkMutex);
        this->neighbourChunkData = newNeighbourData;
    }
  1. GetDataAt and SetDataAt : You need to lock chunkMutex. GetDataAtSetDataAt :您需要锁定 chunkMutex。 Otherwise you might race with InitData.否则你可能会与 InitData 竞争。 There is no need to use std::lock because the order of locks is never swapped around.不需要使用std::lock因为锁的顺序永远不会交换。

EDIT 1:编辑1:

  1. DoWork : The line if (shared_ptr<ChunkData> neighbour = neighbourChunkData.lock()) doesn't keep the neighbur alive. DoWorkif (shared_ptr<ChunkData> neighbour = neighbourChunkData.lock())行不会使邻居保持活动状态。 Move the variable declaration out of the if to keep the reference.将变量声明移出 if 以保留引用。

EDIT: Alternative design proposal编辑:替代设计方案

What I'm bothered with is that your DoWork may be unable to proceed if InitData is still running or waiting to run.我担心的是,如果 InitData 仍在运行或等待运行,您的 DoWork 可能无法继续。 How do you want to deal with this?你想如何处理这个问题? I suggest you make it possible to wait until the work can be done.我建议你可以等到工作完成。 Something like this:像这样的东西:

class Chunk
{
    std::mutex chunkMutex;
    std::shared_ptr<ChunkData> chunkData;
    std::weak_ptr<ChunkData> neighbourChunkData;
    std::condition_variable chunkSet;

    void waitForChunk(std::unique_lock<std::mutex>& lock)
    {
        while(! chunkData)
            chunkSet.wait(lock);
    }
public:
    // modified version of my code above
    void InitData(int size)
    {
        std::shared_ptr<ChunkData> NewData = std::make_shared<ChunkData>();
        NewData->size = size;
        NewData->data = new int[size]; // or std::make_unique<int[]>(size)
        {
            std::lock_guard<std::mutex> lock(chunkMutex);
            chunkData.swap(NewData);
        }
        chunkSet.notify_all();
    }
    void DoWork()
    {
        std::unique_lock<std::mutex> ownLock(chunkMutex);
        waitForChunk(lock); // blocks until other thread finishes InitData
        {
            shared_lock readLock(chunkData->dataMutex);
            ...
        }
        
        shared_ptr<ChunkData> neighbour = neighbourChunkData.lock();
        if(! neighbour)
            return;
        shared_lock readLock(neighbour->dataMutex);
        ...
    }
    void SetNeighbour(Chunk* neighbourChunk)
    {
        if(! neighbourChunk)
            return;
        shared_ptr<ChunkData> newNeighbourData;
        {
            std::unique_lock<std::mutex> lock(neighbourChunk->chunkMutex);
            neighbourChunk->waitForChunk(lock); // wait until neighbor has finished InitData
            newNeighbourData = neighbourChunk->chunkData;
        }
        std::lock_guard<std::mutex> ownLock(this->chunkMutex);
        this->neighbourChunkData = std::move(newNeighbourData);
    }
};

The downside to this is that you could deadlock if InitData is never called or if it failed with an exception.这样做的缺点是,如果从未调用过 InitData 或者它因异常而失败,您可能会死锁。 There are ways around this, like using an std::shared_future which knows that it is valid (set when InitData is scheduled) and whether it failed (records exception of associated promise or packaged_task ).有一些方法可以解决这个问题,例如使用std::shared_future shared_future ,它知道它是有效的(在计划 InitData 时设置)以及它是否失败(记录关联的promisepackaged_task的异常)。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM