簡體   English   中英

關於跨多個線程的shared_mutex和shared_ptr

[英]About shared_mutex and shared_ptr across multiple threads

我實現了這樣的代碼,以便在不同線程上運行的多個實例使用讀寫鎖和 shared_ptr 讀取其他實例的數據。 看起來不錯,但我對此不是 100% 確定的,我提出了一些關於這些用法的問題。

細節

我有多個名為 Chunk 的 class 實例,每個實例都在專用線程中進行一些計算。 一個塊需要讀取相鄰塊的數據以及它自己的數據,但它不寫入鄰居的數據,所以使用讀寫鎖。 此外,可以在運行時設置鄰居。 例如,我可能想在運行時設置一個不同的鄰居塊,有時只是 nullptr。 也可以在運行時刪除塊。 可以使用原始指針,但我認為 shared_ptr 和 weak_ptr 更適合這個,以便跟蹤生命周期。 shared_ptr 中自己的數據和weak_ptr 中鄰居的數據。

我在下面提供了我的代碼的更簡單版本。 ChunkData 有數據和一個互斥體。 我使用 InitData 進行數據初始化,之后在專用線程中調用 DoWork function。 其他函數可以從主線程調用。 這似乎有效,但我不是那么自信。 特別是關於在多個線程中使用 shared_ptr 。

  1. 如果一個線程調用 shared_ptr 的 reset()(在 ctor 和 InitData 中)而其他線程將它與 weak_ptr 的鎖(在 DoWork 中)一起使用,會發生什么? 這需要鎖dataMutex或chunkMutex嗎?

  2. 復制(在 SetNeighbour 中)怎么樣? 我也需要鎖嗎?

我認為其他部分還可以,但是如果您發現任何危險,請告訴我。 感謝。

順便說一句,我考慮過存儲 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;
        }
    }
};

我有幾點意見:

  1. ~ChunkData :您可以將data成員從int*更改為unique_ptr<int[]>以獲得相同的結果,而無需顯式析構函數。 您的代碼是正確的,只是不太方便。

  2. ~Chunk :我認為您不需要鎖或調用重置方法。 到析構函數運行時,根據定義,沒有人應該引用 Chunk object。 所以鎖永遠不會有爭議。 並且重置是不必要的,因為shared_ptr析構函數會處理它。

  3. InitData :是的,需要鎖,因為 InitData 可以與 DoWork 競爭。 您可以通過將 InitData 移動到構造函數來避免這種情況,但我認為這種划分是有原因的。 您還可以將shared_ptr更改為std::atomic<std::shared_ptr<ChunkData> >以避免鎖定。

  4. 像這樣編寫 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避免了為引用計數器分配額外的 memory。 這也將所有分配和釋放移出臨界區。

  1. DoWork :您的評論“准備好 chunkData->data[i] 並修改一些成員”。 你只拿了一個shared_lock但說你修改了成員。 好吧,它是閱讀還是寫作? 或者您的意思是說您修改了 Chunk 而不是 ChunkData,而 Chunk 受其自己的互斥體保護?

  2. SetNeighbour :您需要鎖定自己的 chunkMutex 和鄰居的。 您不應該同時鎖定兩者以避免哲學家就餐的問題(盡管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. GetDataAtSetDataAt :您需要鎖定 chunkMutex。 否則你可能會與 InitData 競爭。 不需要使用std::lock因為鎖的順序永遠不會交換。

編輯1:

  1. DoWorkif (shared_ptr<ChunkData> neighbour = neighbourChunkData.lock())行不會使鄰居保持活動狀態。 將變量聲明移出 if 以保留引用。

編輯:替代設計方案

我擔心的是,如果 InitData 仍在運行或等待運行,您的 DoWork 可能無法繼續。 你想如何處理這個問題? 我建議你可以等到工作完成。 像這樣的東西:

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

這樣做的缺點是,如果從未調用過 InitData 或者它因異常而失敗,您可能會死鎖。 有一些方法可以解決這個問題,例如使用std::shared_future shared_future ,它知道它是有效的(在計划 InitData 時設置)以及它是否失敗(記錄關聯的promisepackaged_task的異常)。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM