我试图实现一个使用两个线程的类:一个用于生产者,一个用于消费者。当前实现不使用锁:
#include <boost/lockfree/spsc_queue.hpp>
#include <atomic>
#include <thread>
using Queue =
boost::lockfree::spsc_queue<
int,
boost::lockfree::capacity<1024>>;
class Worker
{
public:
Worker() : working_(false), done_(false) {}
~Worker() {
done_ = true; // exit even if the work has not been completed
worker_.join();
}
void enqueue(int value) {
queue_.push(value);
if (!working_) {
working_ = true;
worker_ = std::thread([this]{ work(); });
}
}
void work() {
int value;
while (!done_ && queue_.pop(value)) {
std::cout << value << std::endl;
}
working_ = false;
}
private:
std::atomic<bool> working_;
std::atomic<bool> done_;
Queue queue_;
std::thread worker_;
};
应用程序需要将工作项排入一定时间,然后等待事件休眠。这是模拟行为的最小主要部分:
int main()
{
Worker w;
for (int i = 0; i < 1000; ++i)
w.enqueue(i);
std::this_thread::sleep_for(std::chrono::seconds(1));
for (int i = 0; i < 1000; ++i)
w.enqueue(i);
std::this_thread::sleep_for(std::chrono::seconds(1));
}
我非常确定我的实现存在错误:如果工作线程完成并且在执行working_ = false
之前,另一个enqueue
会出现什么?是否可以在不使用锁的情况下使我的代码线程安全?
解决方案需要:
我根据你的建议做了Worker
课程的另一个实现。这是我的第二次尝试:
class Worker
{
public:
Worker()
: working_(ATOMIC_FLAG_INIT), done_(false) { }
~Worker() {
// exit even if the work has not been completed
done_ = true;
if (worker_.joinable())
worker_.join();
}
bool enqueue(int value) {
bool enqueued = queue_.push(value);
if (!working_.test_and_set()) {
if (worker_.joinable())
worker_.join();
worker_ = std::thread([this]{ work(); });
}
return enqueued;
}
void work() {
int value;
while (!done_ && queue_.pop(value)) {
std::cout << value << std::endl;
}
working_.clear();
while (!done_ && queue_.pop(value)) {
std::cout << value << std::endl;
}
}
private:
std::atomic_flag working_;
std::atomic<bool> done_;
Queue queue_;
std::thread worker_;
};
我在worker_.join()
方法中引入了enqueue
。这可能会影响性能,但在极少数情况下(当队列变空并且在线程退出之前,另一个enqueue
会出现)。 working_
变量现在是atomic_flag
,在enqueue
中设置并在work
中清除。需要while
之后的其他working_.clear()
,因为如果在clear
之前推送了另一个值,但在while
之后,则不会处理该值。
此实施是否正确?
我做了一些测试,实施似乎也有效。
OT:将它作为编辑或答案更好吗?
答案 0 :(得分:2)
如果工作线程完成并且在执行working_ = false之前,又会出现另一个队列?
然后,该值将被推送到队列,但在设置标志后将另一个值排队后才会处理。您(或您的用户)可以决定是否可以接受。使用锁可以避免这种情况,但它们不符合您的要求。
如果正在运行的线程即将完成并设置working_ = false;
但在下一个值排队之前尚未停止运行,则代码可能会失败。在这种情况下,您的代码将在正在运行的线程上调用operator=,这会导致根据链接的文档调用std::terminate
。
在将worker分配给新线程之前添加worker_.join()
应该可以防止这种情况。
另一个问题是,如果队列已满,queue_.push
可能会失败,因为它具有固定大小。目前您只是忽略该情况,并且该值不会添加到完整队列中。如果等待队列有空间,则不会快速入队(在边缘情况下)。您可以接受push
返回的bool(告诉它是否成功)并从enqueue
返回。这样,调用者可以决定是否要等待或丢弃该值。
或使用非固定大小的队列。 Boost对此有这样的选择:
可用于在推送期间完全禁用动态内存分配,以确保无锁行为。 如果数据结构配置为固定大小,则内部节点存储在数组中并对其进行寻址 通过数组索引。这将队列的可能大小限制为索引可以处理的元素数 类型(通常为2 ** 16-2),但在缺少双宽度比较和交换指令的平台上,这是最好的方法 实现锁定自由。
答案 1 :(得分:1)
您的工作线程需要超过2个状态。
如果强制关机,它会跳过空闲关机。如果任务用完,它将转换为空闲关闭。在空闲关闭时,它会清空任务队列,然后进入关闭状态。
关闭已设置,然后您将离开工作任务的末尾。
生产者首先把东西放在队列中。然后它检查工人状态。如果关闭或空闲关闭,首先join
(并将其转换为未运行),然后启动新工作线程。如果没有运行,只需启动一个新工作人员。
如果生产者想要启动一个新工作者,它首先要确保我们处于未运行状态(否则,逻辑错误)。然后我们转换到Doing tasks状态,然后启动工作线程。
如果生产者想要关闭辅助任务,它会设置完成标志。然后它检查工人状态。如果它除了没有运行之外什么都会加入它。
这可能导致工作线程无故启动。
有一些情况下,上面可以阻止,但也有一些之前。
然后,我们写了一份正式或半正式的证据,证明上述内容不会丢失信息,因为在编写无锁代码时,除非您有证据,否则您无法完成。
答案 2 :(得分:0)
这是我对问题的解决方案。我不喜欢回答自己,但我认为展示实际代码可能对其他人有所帮助。
#include <boost/lockfree/spsc_queue.hpp>
#include <atomic>
#include <thread>
// I used this semaphore class: https://gist.github.com/yohhoy/2156481
#include "binsem.hpp"
using Queue =
boost::lockfree::spsc_queue<
int,
boost::lockfree::capacity<1024>>;
class Worker
{
public:
// the worker thread starts in the constructor
Worker()
: working_(ATOMIC_FLAG_INIT), done_(false), semaphore_(0)
, worker_([this]{ work(); })
{ }
~Worker() {
// exit even if the work has not been completed
done_ = true;
semaphore_.signal();
worker_.join();
}
bool enqueue(int value) {
bool enqueued = queue_.push(value);
if (!working_.test_and_set())
// signal to the worker thread to wake up
semaphore_.signal();
return enqueued;
}
void work() {
int value;
// the worker thread continue to live
while (!done_) {
// wait the start signal, sleeping
semaphore_.wait();
while (!done_ && queue_.pop(value)) {
// perform actual work
std::cout << value << std::endl;
}
working_.clear();
while (!done_ && queue_.pop(value)) {
// perform actual work
std::cout << value << std::endl;
}
}
}
private:
std::atomic_flag working_;
std::atomic<bool> done_;
binsem semaphore_;
Queue queue_;
std::thread worker_;
};
我尝试了@Cameron的建议,不关闭线程并添加信号量。这实际上仅在第一个enqueue
和最后一个work
中使用。这不是无锁的,只是在这两种情况下。
我在之前的版本(请参阅我编辑的问题)和此版本之间进行了一些性能比较。当没有多少开始和停止时,没有显着差异。但是,当enqueue
必须signal
工作线程而不是启动新线程时,enqueue
要快10倍。这是一种罕见的情况,所以它不是很重要,但无论如何它都是一种改进。
此实现满足:
work
和enqueue
忙时); 答案 3 :(得分:0)
非常局部的答案:我认为所有这些原子,信号量和状态都是从“线程”到“工人”的反向沟通渠道。为什么不为此使用另一个队列?至少考虑一下会帮助您解决问题。