从多个线程中的列表中删除元素

时间:2019-06-27 07:39:35

标签: c++ multithreading

我想知道擦除列表中的元素是否被认为是“写”操作,即擦除多线程中的元素是否是线程安全的?

例如,我有一个包含超过100k元素的列表,为了根据某种条件从其中删除元素,我想将其拆分为与可用线程一样多的部分。然后,每个线程将检查其部分并删除满足某些条件的特定元素。这样安全吗?

这是我的简单示例(注意:这是简化的情况,我知道一些边缘情况):

#include <list>
#include <vector>
#include <thread>
#include <iostream>
#include <algorithm>

int main() {

    constexpr size_t number_of_threads = 2;

    std::list<unsigned int> elements = { 1, 2, 3, 4, 4, 5, 6, 7};
    std::vector<std::thread> threads;

    size_t elements_per_thread = elements.size() / number_of_threads;

    for (size_t i = 0; i < number_of_threads; i++) {
        auto elements_begin = std::next(std::begin(elements), i * elements_per_thread);
        auto elements_end   = std::next(elements_begin, elements_per_thread);

        threads.emplace_back(
            [&elements, elements_begin, elements_end]() {
                elements.erase(std::remove_if(elements_begin, elements_end, [](unsigned int const& x) {
                    return x == 4;
                }), elements_end);
            }
        );
    }

    for (auto& thread : threads) {
        thread.join();
    }

    for (auto const& item : elements) {
        std::cout << item << " " << std::endl;
    }

    return 0;
}

这将输出正确的结果:

1
2
3
5
6
7

提前谢谢

2 个答案:

答案 0 :(得分:0)

  

例如,我有一个包含超过100k元素的列表,为了根据某种条件从其中删除元素,我想将其拆分为与可用线程一样多的部分。然后,每个线程将检查其部分并删除满足某些条件的特定元素。这样安全吗?

在阅读以下thread-safety note之后,我确信并发调用std::list::erase是不安全的:

  
      
  1. [...]使所有迭代器无效的容器操作会修改容器,即使这些迭代器未失效,也无法与现有迭代器上的任何操作同时执行。
  2.   

为完整起见,这是cppreference.com关于std::list::erase导致的引用/迭代器无效的内容:

  

对已删除元素的引用和迭代器无效。其他引用和迭代器不受影响。

您是否考虑过将每个线程的庞大列表splice变成一个较小的列表?然后,每个线程可以在同步和取消拼接之前在其自己的列表上使用remove_if

无论如何,具有100k元素的std::list听起来像是故意降低性能的方法。您是要进行实验还是在这里使用std::list的原因是什么?

答案 1 :(得分:0)

从列表中删除元素 的确是“写”操作。

某些下一个/上一个指针必须更改,并且其中一个节点将被释放。 例如,以列表A <-> B <-> C

要删除的B代码如下所示:

A->next = C
C->prev = A
delete B->data
delete B

这些是写操作,默认情况下不是线程安全的。即使要擦除的范围不同,也会在范围边界处发生竞争。

标准容器不是线程安全的。 (这适用于大多数编程语言)。线程同步非常昂贵,而且此成本还会影响非多线程代码。您无需为不使用的东西付费。此外,由于多线程是一种优化,因此数据结构设计人员很难在不知道访问模式的情况下知道如何进行优化。 (尽管就您而言,这是一种常规的访问模式)。

如果您使用的是C ++ 17,请尝试使用std::remove_if函数ExecutionPolicy重载。 https://en.cppreference.com/w/cpp/algorithm/remove。根据{{​​3}},这应在GCC 9(与-ltbb链接)和MSVC 19.14(VS 2017 15.7)中可用。 MSVC实际上确实并行化了功能(https://en.cppreference.com/w/cpp/compiler_support)。我相信海湾合作委员会也这样做。关于执行策略(https://devblogs.microsoft.com/cppblog/using-c17-parallel-algorithms-for-better-performance/),我上次检查时,在MSVC中未实现已排序/未排序之间的差异。

从您的示例中可以看出,您已经知道如何使用std::remove_if

  

通过移动(通过移动分配)将   范围内的元素,使不存在的元素   被删除出现在范围的开头。相对顺序   保留剩余的元素,并保留   容器不变。迭代器指向之间的元素   范围的新逻辑端和物理端仍然   可取消引用,但元素本身具有未指定的值   (根据MoveAssignable后置条件)。通常会要求移除   然后调用容器的擦除方法,该方法将擦除   未指定的值,并将容器的物理尺寸减小到   匹配其新的逻辑大小。

后续示例:

std::string str1 = "Text with some   spaces";
str1.erase(std::remove(str1.begin(), str1.end(), ' '), str1.end());

https://en.cppreference.com/w/cpp/algorithm/execution_policy_tag_t

最后,您在一些评论中提到您正在考虑摆脱std::list。 Bjarne Stroustrup提倡默认使用std::vector。这是因为即使在需要对数组进行O(N)移位运算的情况下,数组也可以胜过链接列表! (“可以” ...您应该自己看看哪个更快)

https://en.wikipedia.org/wiki/Erase%E2%80%93remove_idiom

https://isocpp.org/blog/2014/06/stroustrup-lists

如果您没有C ++ 17,则切换到std::vector还可以使擦除并行化,因为支持vector的数组没有任何移动部分。 注意事项:

  • 这当然取决于您的操作方式,因为有多个线程在移动 周围的事物将创造种族。
  • 根据拆分阵列的方式,可能会出现工作负载不平衡的情况。
  • 您还必须进行计算以避免所谓的“虚假共享”