【问题标题】:Thread safety with shared queues共享队列的线程安全
【发布时间】:2018-10-23 16:21:40
【问题描述】:

我在 C++ 中的多线程应用程序方面没有太多经验,我目前正在尝试设计一个具有四个线程和三个共享队列的应用程序。这是我一年前用 Python 编写的应用程序的 C++ 版本,但它的运行速度不够快,无法满足新的要求(我一直想在 C++ 中学习多线程,所以这似乎是一个好机会)。

计划的线程是:

1) 从文件末尾抓取行的线程(相当于在 linux CL 中运行 tail -f)

2) 线程处理文件中的行

3) 线程将处理后的行发送到别处

4) 在日志文件中写入调试输出的可选线程(是否使用在运行时指定)

我打算使用的队列设置是:

线程 1 和 2 之间的一个共享队列;线程 1 放入物品,线程 2 取出物品。永远不要反过来。线程 2 和 3 的一个共享队列,其行为方式与前者相同。第三个可选队列,前三个线程都写入,第四个线程读取。

我遇到的问题是,与 Python 中的 multiprocessing.Queue() 对象不同,C++ 队列默认不是线程安全的,我还没有找到标准的共享队列实现。

问题:

1) 我在 Python 中使用的基本大纲(上图)可以在 C++ 中重复使用,而开发起来不会一团糟吗?

2) 是否有我尚未找到的线程安全队列的标准实现,或者我必须求助于外来库(例如 Boost)还是自己制作?

【问题讨论】:

  • boost 有你需要的东西:boost.org/doc/libs/1_59_0/doc/html/lockfree.html
  • 出于好奇,您是否证明了在步骤 1、2 和 3 中使用单独的线程比使用单个线程在循环中执行所有三个步骤更好?
  • @SolomonSlow 在 Python 中,在单个线程中循环执行所有步骤(这是我第一次尝试实现设计的方式)对于原始要求来说太慢了。更改为多线程(由于 GIL 应用于那里的“线程”,在 Python 中技术上是“多处理”)实现将事情加速到可接受的水平。
  • 那么,如果你重叠输入、输出和计算,进程可以跟上,但如果你不重叠它们,它就落后了?这并没有在成功和失败之间留下很大的空间。如果您认为您的程序所使用的消防水带在未来可能会流得更快,那么现在开始考虑一个不同的、更可扩展的架构还为时不早,但与此同时......
  • 我不会将 boost 称为“异国情调”的库,它被广泛使用,而且它的许多库正在寻找进入 c++ 标准库的途径

标签: c++ multithreading queue


【解决方案1】:

如果你不想使用 boost,这里有一个或多或少完整的实现:

#include <mutex>
#include <condition_variable>
#include <queue>

class Queue
{
public:
  Queue() = default;
  ~Queue() = default;

  int pop()
  {
    std::unique_lock<std::mutex> lock_guard(mutex_);

    while (internal_queue_.empty())
    {
      cond_.wait(lock_guard);
    }

    auto item = i_queue_.front();
    i_queue_.pop();
    return item;
  }

  void push(int item)
  {
    std::unique_lock<std::mutex> lock_guard(mutex_);
    i_queue_.push(item);
    lock_guard.unlock();
    cond_.notify_one();
  }

private:
  std::queue<int> i_queue_;
  std::mutex mutex_;
  std::condition_variable cond_;
};

int main()
{
  Queue queue;
  queue.push(1);
  auto i = queue.pop();
  return 0;
}

请注意,pop 将阻塞您的线程,直到队列为空。 Push 将通知等待的线程。也就是说,您只需要在您的线程中使用一个循环来一对一地弹出项目。如果没有更多物品可供消费,则循环将等待。

【讨论】:

  • 如果 i_queue_ 从 std::queue 更改为 std::queue<:string> 这应该仍然有效,对吧?第二个问题:给定队列的消费线程和生产线程意味着异步操作(生产者可以在生产者处理之前的项目时将另一个项目放到队列的末尾)。当您说线程将被阻塞时,这是否意味着它们将被完全阻塞(根本无法做任何事情),或者只是被阻止访问队列?
  • @ChrisB 是的,std::string 应该同样有效。如果您在队列为空时尝试pop(),队列将阻塞。如果您不希望它阻止将 cond_.wait(lock_guard) 更改为 return(您需要添加某种标志来表示您没有返回值)
  • while 循环可以替换为cond_.wait(lock_guard, [&amp;]{return !internal_queue_.empty();});,在这种情况下没有太大区别,但在您开始添加超时时很重要。
  • 当然,它适用于任何其他类型,包括 std::string。阻塞意味着如果您从循环中消费项目,如果队列为空,循环将等待,并且只有在新项目到达时才会释放阻塞。这样,如果队列为空,您就不需要检查循环。正如上面 Alan 所提到的,您可以简单地删除条件变量的使用并将此逻辑放在您的消费者部分。这样,您只需要一个互斥锁即可使队列线程安全。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2012-11-05
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多