【问题标题】:Multi-Threaded accessing same deque C++多线程访问相同的双端队列 C++
【发布时间】:2016-03-25 21:16:51
【问题描述】:

在我的多线程应用程序中,我有两个线程使用相同的std::deque。其中一个写入它,而另一个读取它(用于数据分析)。

我收到此错误:

Deque 迭代器不可取消引用

编辑: 这是我用于从双端队列读取的代码。该错误在 if 条件的某个更深的地方引发(我正在使用 at 访问双端队列)。

for (int i = 0; i <myDeque.size(); i++){
    try{
        if (myDeque.at(i) > 10){
            //do stufff
        }
    }
    catch (...){
        cout << "ERROR" << endl;
    }
}

我认为,这是由于双端队列的这种多线程访问而发生的。 我无法通过 try-catch 块捕获错误。我不能这样做吗,因为它被扔进了一个更深的“平面”? 有没有可能修复这个错误?

【问题讨论】:

  • deque 是空的吗?
  • 您是否使用任何同步机制(例如锁或互斥锁)来序列化对出队对象的访问?如果没有,你应该;当您希望多个线程能够访问共享数据结构时,这是标准解决方案。
  • assert(!d.empty()); 在您取消引用之前。
  • 虽然您可能会遇到数据竞争,除非您也有某种同步机制,但您的问题是您使用的是无效的迭代器。如果您的一个线程正在写入双端队列,而另一个仍在遍历它,则您的迭代器将失效。
  • @ShadowRanger en.cppreference.com/w/cpp/container/deque/emplace_back 表示 emplace_back() 使所有迭代器无效,push_back() 和 push_front() 也是如此。我是不是误解了什么?

标签: c++ multithreading


【解决方案1】:

这是一个非常简单的读取器/写入器对通过队列进行通信的示例。

注意使用 condition_variable 来同步有关是否有工作要做以及写入器是否完成的通信(表明读取器在清空队列时可以停止):

#include <iostream>
#include <thread>
#include <deque>
#include <condition_variable>

std::mutex m;
std::condition_variable reader_action;
bool all_written = false;

std::deque<int> buffer;

// note: this function is called with the mutex unlocked
// we have popped i from the buffer
void handle_read_event(int i)
{
    if (i % 100000 == 0)
        std::cout << i << std::endl;
}

int main()
{
    std::thread writer([]{
        for (int i = 0 ; i < 1000000 ; ++i)
        {
            {
                auto lock = std::unique_lock<std::mutex>(m);
                buffer.push_back(i);
                lock.unlock();
                reader_action.notify_one();
            }
        }

        auto lock = std::unique_lock<std::mutex>(m);
        all_written = true;
        lock.unlock();
        reader_action.notify_one();
    });

    std::thread reader([]{
        while(1)
        {
            auto lock = std::unique_lock<std::mutex>(m);
            reader_action.wait(lock, [] { return all_written || (!buffer.empty()); });
            if (!buffer.empty()) {
                int i = buffer.front();
                buffer.pop_front();
                lock.unlock();
                handle_read_event(i);
            }
            else if(all_written)
            {
                break;
            }
        }
    });

    writer.join();
    reader.join();
    return 0;
}

预期输出:

0
100000
200000
300000
400000
500000
600000
700000
800000
900000

如果我们决定不介意在排空队列时让写入器等待,我们可以这样实现第二个线程:

std::thread reader([]{
    while(1)
    {
        auto lock = std::unique_lock<std::mutex>(m);
        reader_action.wait(lock, [] { return all_written || (!buffer.empty()); });
        while (!buffer.empty()) {
            int i = buffer.front();
            buffer.pop_front();
            handle_read_event(i);
        }

        if(all_written)
        {
            break;
        }
    }
});

...或任何其他适合我们目的的策略。

【讨论】:

  • 我想尽可能少地减少互斥体的使用,因为我认为emplace_back()在双端队列的第一个元素上运行,而front()和pop_front()在双端队列的第一个元素上运行,他们没有访问相同的元素。然后我将你的代码修改为this,它似乎工作正常,但是如果将编写器线程中的i从100000更改为1000000,输出就会变得异常,它打印很多零,我不知道为什么
  • 1 你必须使用互斥体,因为容器不是线程安全的。
  • 2.您对性能的担忧是没有根据的。使用非竞争互斥体并不比原子交换更昂贵。
【解决方案2】:

使用锁定?在一些常用的位置,声明一个mutex:

std::mutex deque_lock;

然后在blocks that acquire it中包装对deque的读写:

... nondeque stuff ...
{
    std::lock_guard<std::mutex> lock(deque_lock);
    mydeque.push_back(...);
}
... more nondeque stuff...

阅读时:

... nondeque stuff ...
{
    std::lock_guard<std::mutex> lock(deque_lock);
    for (const auto& elem : mydeque) {
        ... do stuff with each element, ideally cheap things to avoid blocking writer ...
    }
}
... more nondeque stuff...

尽量减少锁定块中的工作;如果这是一项昂贵的工作,可能值得复制出锁下的值,然后在没有锁的情况下使用它以避免阻塞其他线程。

【讨论】:

  • 当其他线程想要访问被阻塞的内存时,它会做什么。它是停止还是只是继续并“删除”它想要在双端队列上执行的操作?
  • @black 它被阻塞并等待直到它可以获取互斥锁上的锁才能继续。
  • @ShadowRanger 谢谢!所以我会在写作过程中这样做(push_back)。我也应该为阅读而做吗? (对于我的问题中的 for 循环)。我将阅读 25 个元素(在示例中只有 10 个)。是不是太长了?
  • @black 如果您在使用多线程概念编写程序之前了解了一些多线程概念,那么您会对自己有很大帮助。你有没有考虑读一本书?
  • @black:您将在读取deque 的代码周围使用相同的deque_lock。这个想法是,一次只有一个线程可以拥有deque_lock,因此无论哪个线程拥有它都拥有独占访问权,直到释放锁(lock_guard 在它创建和初始化的块结束时被销毁)中)。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-12-28
  • 2015-04-26
  • 2011-02-01
  • 1970-01-01
  • 2018-07-02
相关资源
最近更新 更多