[多线程并发并行]_[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]; //保留旧对象计数器+1
            // 复制对象,读取对象内容不需要加锁.
            auto oneNew = new string(*(one.get()));
            (*vv)[i] = shared_ptr<string>(oneNew); // 旧对象计数器-1
            --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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Peter(阿斯拉达)

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值