[多線程併發並行]_[C/C++11]_[多線程訪問修改集合vector會衝突的兩個解決方案]

場景

  1. 在開發C/C++多線程程序時,STL集合類是我們經常用到的類,比如vector. 而C/C++的這些集合類並沒有同步版本,所以在多線程訪問時,如果某個線程正在修改集合類A, 而其他線程正在訪問A,那麼會造成數據衝突,導致程序拋出異常。這裏說的訪問A,意思是調用A的任何方法。難道我們需要在用到A的地方對A進行加鎖? 麻煩不止,而且很容易造成性能下降。

數據衝突線程1訪問集合B:

 auto &one = gCollectionB[loadInt];
 auto to_int = atoi(one->c_str());

線程2正在刪除集合B的元素:

gCollectionB.erase(gCollectionB.begin()+index);

說明

  1. Java有併發類CopyOnWriteArrayList,ConcurrentHashMap等. C/C++的標準庫沒有. 當然,我們使用第三方庫肯定也行。如果不使用第三方庫的話也可以自己實現簡單的Copy On Write方式的集合操作.

  2. 我這裏舉出的兩個方案是爲了解決以下兩類問題的,犧牲了內存來提高代碼訪問性能. 使用shared_ptr是爲了能使用引用計數的方式管理對象,而使用atomic_load,atomic_store是爲了能使用原子方式(可以通過atomic_is_lock_free判斷是否原子)進行獲取和替換託管對象.

  3. 注意,代碼基於C++11實現,使用的是樂觀鎖.對於C++11的語言特性,可以參考C++11語言特性和標準庫

1. 修改集合裏的元素

  1. 問題: 在線程1. 集合A,需要修改A裏的某個元素對象object1的屬性值. 這時候線程2在訪問集合B的相同object1. 這種場景一般用在: 線程1是界面線程可以修改集合元素,而線程2是工作線程只能讀取集合元素。

  2. 解決方案:

    1). 使用copy-on-write機制, 先複製object1object1New,接着object1New裏修改屬性值, 最後把object1替換爲object1New.

    2). 集合使用shared_ptr<T>封裝objectX對象,即vector<shared_ptr<T>>, 這樣對象可以直接替換,也不用擔心什麼時候object1會銷燬.因爲集合B在刪除object1時包含的T對象會自動銷燬.

    3). 注意這裏是兩個線程,使用兩個不同的集合,只是集合裏的對象是一樣的。看最下邊的完整測試代碼CopyOnWriteType_1類.

auto &one = (*vv)[i];
// 複製對象,讀取對象內容不需要加鎖.
auto oneNew = new string(*(one.get()));
(*vv)[i] = shared_ptr<string>(oneNew);

2. 刪除相同集合的元素

  1. 問題: 在線程1. 集合A,需要刪除A裏的位置是10的元素對象object10,而這時候線程2在訪問這個集合A的位置10.

  2. 解決方案:

    1). 需要刪除集合元素時,先複製集合A得到新集合B.

    2). 再刪除集合B裏的元素.

    3). 使用atomic_store把共享指針的託管集合A原子替換爲集合B.

  3. 注意,只能有一個線程進行集合刪除操作,可以有多個線程讀取集合. 查看類CopyOnWriteType_2, 主要使用了原子替換原集合的方法,而不能刪除原集合的某個索引,因爲這個線程如果刪除這個索引,而只讀線程正好訪問到這個索引的對象會導致數據衝突。還有使用atomic_load是爲了能原子複製集合的共享指針,避免普通複製共享指針時另一個線程正在替換該共享指針的託管對象。

  4. 注意, shared_ptr所有的方法都不是線程安全的方法。

例子



#include <string>
#include <iostream>
#include <memory>
#include <thread>
#include <atomic>
#include <assert.h>
#include <vector>
#include <chrono>
#include <functional>
#include <random>
#include <sstream>
#include <mutex>
using namespace std;

static mutex gLogMutex;
static vector<string> gLogArray;
struct Stage;
typedef vector<shared_ptr<Stage>> VSS;
static atomic<int> gCount(0);
void DumpLog()
{
    gLogMutex.lock();
    for (auto &str : gLogArray)
        cout << str << endl;

    gLogMutex.unlock();
}

void PRINT(const char *str)
{
    gLogMutex.lock();
    gLogArray.push_back(str);
    gLogMutex.unlock();
}

template <typename T>
void Log(const char *str, T t)
{
    stringstream ss;
    ss << str << " : " << t;
    gLogMutex.lock();
    gLogArray.push_back(ss.str());
    // cout << ss.str() << endl;
    gLogMutex.unlock();
}

int rand_int(int low, int high)
{
    static default_random_engine re;
    using Dist = uniform_int_distribution<int>;
    static Dist ud;
    return ud(re, Dist::param_type(low, high));
}

// 問題2:
// 在線程1. 集合A,需要刪除A裏的位置是10的元素對象object10,而這時候線程2在訪問這個集合A的位置10.
// 條件:
// 1. 只能有一個線程進行集合刪除操作,可以有多個線程讀取集合.
// 解決方案:
// 1. 需要刪除集合元素時,先複製原子複製集合A得到新集合B.
// 2. 再刪除集合B裏的元素.
// 3. 使用atomic_store把原共享指針的託管集合A替換爲集合B.

class Stage{
public:
    ~Stage(){
        PRINT("~Stage\n");
        gCount--;
    }
    int data_ready;
    void* data;
    string name;
};

class CopyOnWriteType_2
{
public:
    
    static void DoDeleterWork(shared_ptr<VSS> *vv, atomic<int> *index, bool *stopped)
    {
        PRINT("========== BEGIN DoDeleterWork ==========");
        // 嘗試5000次刪除隨機索引。
        int count = 5000;
        while (count){
           // 原子複製共享對象.
           auto source = atomic_load(vv);
           // 刪除元素.
           auto size = source->size();
           if(!size)
                break;

           auto i = index->load();
           if(i >= size){
                // 如果本線程執行比線程1快,索引值還是舊的就放棄刪除.
                this_thread::sleep_for(chrono::microseconds(200));
                continue;
            }

           Log("set index before: ",i);
           Log("set index before size: ",size);
           auto vs = new VSS(*source.get());
           Log("copy index before size: ",vs->size());
           vs->erase(vs->begin()+i);
           shared_ptr<VSS> temp(vs);

           Log("temp use count ",temp.use_count());
           atomic_store(vv,temp);
           Log("temp use count ",temp.use_count());
           auto sourceNew = atomic_load(vv);
           size = sourceNew->size();
           Log("set index after size: ",size);
           --count;
        }

        *stopped = true;
        PRINT("========== END DoDeleterWork ==========");
    }

    static void TestCollectionDeleteObject()
    {
        PRINT("BEGIN TestCollectionDeleteObject");
        
        auto collection = new VSS();
        const int kCycleNumber = 100;
        for (int i = 0; i < kCycleNumber; i++){
            auto s = new Stage();
            s->name = to_string(i);
            collection->push_back(std::shared_ptr<Stage>(s));
        }
        shared_ptr<VSS> sp(collection);

        gCount = collection->size();
        bool gStopped = false;
        atomic<int> gIndex(0);
        thread t1(bind(&DoDeleterWork, &sp, &gIndex, &gStopped));
        t1.detach();

        int gCount = 0;
        while (!gStopped){
            
            // 使用前先原子複製共享指針,這樣如果共享指針被其他線程reset了也不會拋出異常.
            auto sp1 = atomic_load(&sp);
            // 訪問元素
            auto size = sp1->size();
            Log("Access size", size);
            if(!size){
               this_thread::sleep_for(chrono::microseconds(500));
               continue;
            }

            gIndex = rand_int(0,size-1);
            Log("Access gIndex", gIndex.load());
            auto one = sp1->at(gIndex);
            auto to_int = atoi(one->name.c_str());
            assert(to_int >= 0);
            // this_thread::sleep_for(chrono::microseconds(200));
        }
        assert(gCount == 0);
        PRINT("END TestCollectionDeleteObject");
    }
};

// 問題1:
// 在線程1. 集合A,需要修改A裏的某個元素對象object1的屬性值. 這時候線程2在訪問集合B的相同object1.
// 解決方案:
// 1. 使用copy-on-write機制, 先複製object1 到 object1New. 之後在object1New裏修改屬性值.
// 2. 之後把 object1替換爲 object1New.
// 3. 集合使用shared_ptr<T>封裝objectX對象,即vector<shared_ptr<T>>, 這樣對象可以直接替換,
//    也不用擔心什麼時候object1會銷燬.因爲集合B在刪除object1時包含的T對象會自動銷燬.

class CopyOnWriteType_1
{

public:
    static void DoAnotherWork(vector<shared_ptr<string>> *vv, atomic<int> *index, bool *stopped)
    {
        PRINT("========== BEGIN DoAnotherWork ==========");
        // 嘗試1000次修改隨機對象.
        int count = 5000;
        while (count){
            int i = *index;
            Log("Doing AnotherWork index", i);
            auto &one = (*vv)[i];
            // 複製對象,讀取對象內容不需要加鎖.
            auto oneNew = new string(*(one.get()));
            (*vv)[i] = shared_ptr<string>(oneNew);
            --count;
        }

        *stopped = true;
        PRINT("========== END DoAnotherWork ==========");
    }

    static void TestCollectionAObject1Modify()
    {
        PRINT("BEGIN TestCollectionAObject1Modify");
        vector<shared_ptr<string>> gCollectionB;
        const int kCycleNumber = 40000;
        for (int i = 0; i < kCycleNumber; i++){
            auto str = new string(to_string(i));
            gCollectionB.push_back(shared_ptr<string>(str));
        }

        bool gStopped = false;
        atomic<int> gIndex(0);
        auto vv = new vector<shared_ptr<string>>(gCollectionB);
        thread t1(bind(&DoAnotherWork, vv, &gIndex, &gStopped));
        t1.detach();

        int maxRandInt = kCycleNumber - 1;
        int gCount = 0;
        while (!gStopped){
            if (!(gCount++ % 2))
                gIndex = rand_int(0, maxRandInt);

            auto loadInt = gIndex.load();
            Log("Access gIndex", loadInt);
            auto &one = gCollectionB[loadInt];
            auto to_int = atoi(one->c_str());
            assert(to_int >= 0);
        }
        PRINT("END TestCollectionAObject1Modify");
    }
};

int main(int argc, char const *argv[])
{
    PRINT("hello atomic");
    // CopyOnWriteType_1::TestCollectionAObject1Modify();
    CopyOnWriteType_2::TestCollectionDeleteObject();
    DumpLog();
    PRINT("world atomic");
    return 0;
}


參考

shared_ptr

shared_ptr atomic

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章