同时处理数据。我需要注意什么?

时间:2014-09-26 20:18:09

标签: c++ multithreading c++11 parallel-processing

我有一个例程,用于加载和解析文件中的数据。有可能需要同时从两个位置检索同一文件中的数据,即在后台缓存过程中和用户请求中。

具体来说,我使用的是C ++ 11线程和互斥库。我们使用Visual C ++ 11(又名2012)进行编译,因此受到它缺乏的限制。

我天真的实现是这样的:

map<wstring,weak_ptr<DataStruct>> data_cache;
mutex data_cache_mutex;

shared_ptr<DataStruct> ParseDataFile(wstring file_path) {
    auto data_ptr = make_shared<DataStruct>();

    /* Parses and processes the data, may take a while */

    return data_ptr;
}

shared_ptr<DataStruct> CreateStructFromData(wstring file_path) {
    lock_guard<mutex> lock(data_cache_mutex);

    auto cache_iter = data_cache.find(file_path);
    if (cache_iter != end(data_cache)) {
        auto data_ptr = cache_iter->second.lock();
        if (data_ptr)
            return data_ptr;
        // reference died, remove it
        data_cache.erase(cache_iter);
    }

    auto data_ptr = ParseDataFile(file_path);

    if (data_ptr)
        data_cache.emplace(make_pair(file_path, data_ptr));

    return data_ptr;
}

我的目标是双重的:

  • 允许多个线程同时加载单独的文件
  • 确保文件仅处理一次

我当前的方法存在的问题是它根本不允许并发解析多个文件。如果我理解将会发生什么,他们每个人都会锁定并最终线性处理,一次一个线程。它可能会从运行更改为线程先通过锁定的顺序,但最终结果是相同的。

我考虑过的一个解决方案是创建第二张地图:

map<wstring,mutex> data_parsing_mutex;

shared_ptr<DataStruct> ParseDataFile(wstring file_path) {
    lock_guard<mutex> lock(data_parsing_mutex[file_path]);
    /* etc. */
    data_parsing_mutex.erase(file_path);
}

但现在我必须关注data_parsing_mutex如何更新。所以我想我需要另一个互斥量?

map<wstring,mutex> data_parsing_mutex;
mutex data_parsing_mutex_mutex;

shared_ptr<DataStruct> ParseDataFile(wstring file_path) {
    unique_lock<mutex> super_lock(data_parsing_mutex_mutex);
    lock_guard<mutex> lock(data_parsing_mutex[file_path]);
    super_lock.unlock();

    /* etc. */

    super_lock.lock();
    data_parsing_mutex.erase(file_path);
}

事实上,考虑到这一点,除非我再次检查缓存,否则当用户请求后台进程尚未完成时,它不会避免必须对文件进行双重处理。

但到现在为止,我的蜘蛛般的感官正在说There must be a better way。在那儿?期货,承诺或原子能帮助我吗?

2 个答案:

答案 0 :(得分:1)

根据您的描述,听起来您正在尝试使用线程池进行DataStruct的延迟初始化,以及引用计数缓存。 std::async应该能够为此类提供大量必要的调度和同步。

使用std::async,代码看起来像这样......

map<wstring,weak_ptr<DataStruct>> cache;
map<wstring,shared_future<shared_ptr<DataStruct>>> pending;

mutex cache_mutex, pending_mutex;

shared_ptr<DataStruct> ParseDataFromFile(wstring file) {
    auto data_ptr = make_shared<DataStruct>();

    /* Parses and processes the data, may take a while */

    return data_ptr;

}


shared_ptr<DataStruct> CreateStructFromData(wstring file) {
    shared_future<weak_ptr<DataStruct>> pf;
    shared_ptr<DataStruct> ce;

    {
        lock_guard(cache_mutex);

        auto ci = cache.find(file);

        if (!(ci == cache.end() || ci->second.expired()))
          return ci->second.lock();
    }

    {
        lock_guard(pending_mutex);

        auto fi = pending.find(file);

        if (fi == pending.end() || fi.second.get().expired()) {

            pf = async(ParseDataFromFile, file).share();
            pending.insert(fi, make_pair(file, pf));

        } else {

            pf = pi->second;

        }
    }

    pf.wait();
    ce = pf.get();

    {
        lock_guard(cache_mutex);

        auto ci = cache.find(file);

        if (ci == cache.end() || ci->second.expired())
          cache.insert(ci, make_pair(file, ce));
    }

    {
        lock_guard(pending_mutex);

        auto pi = pending.find(file);

        if (pi != pending.end())
          pending.erase(pi);
    }

    return ce;

}

这可能会稍微优化一下,但一般的想法应该是相同的。

答案 1 :(得分:0)

在典型的计算机上,尝试同时加载文件没什么意义,因为磁盘访问将成为瓶颈。相反,让单个线程加载文件(或使用异步I / O)并将解析分离到线程池会更好。然后将结果存储在共享容器中。

关于防止双重工作,你应该考虑这是否真的有必要。如果你只是因为过早优化而做到这一点,你可能会通过专注于使程序响应而不是高效来让用户更快乐。也就是说,确保用户快速获得他们要求的内容,即使这意味着要做双重工作。

OTOH,如果技术上没有解析文件两次,你可以跟踪共享容器中每个文件的状态(加载,解析,解析)。