我正在尝试基于Dmitry Vyukov的this one written in C实现无锁的多生产者单一消费者队列。
到目前为止,我编写的单个测试几乎可以正常工作。但是消费者通常会错过第一个或第二个项目。有时,消费者会错过大约一半的投入。
现在,它不是免费的。每次使用new
运算符时,它都会锁定,但是我希望它能正常工作并编写一些更详尽的测试,然后再处理分配器。
// src/MpscQueue.hpp
#pragma once
#include <memory>
#include <atomic>
#include <optional>
/**
* Adapted from http://www.1024cores.net/home/lock-free-algorithms/queues/intrusive-mpsc-node-based-queue
* @tparam T
*/
template< typename T >
class MpscQueue {
public:
MpscQueue() {
stub.next.store( nullptr );
head.store( &stub );
tail = &stub;
}
void push( const T& t ) {
emplace( t );
}
void push( T&& t ) {
emplace( std::move( t ));
}
template< typename ... Args >
void emplace( Args...args ) {
auto node = new Node{ std::make_unique<T>( std::forward<Args>( args )... ), nullptr };
push( node );
}
/**
* Returns an item from the queue and returns a unique pointer to it.
*
* If the queue is empty returns a unique pointer set to nullptr
*
* @return A unique ptr to the popped item
*/
std::unique_ptr<T> pop() {
Node* tailCopy = tail;
Node* next = tailCopy->next.load();
auto finalize = [ & ]() {
tail = next;
std::unique_ptr<Node> p( tailCopy ); // free the node memory after we return
return std::move( tail->value );
};
if ( tailCopy == &stub ) {
if ( next == nullptr ) return nullptr;
tail = next;
tailCopy = next;
next = next->next;
}
if ( next ) return std::move( finalize());
if ( tail != head.load()) return nullptr;
push( &stub );
next = tailCopy->next;
return next ? std::move( finalize()) : nullptr;
}
private:
struct Node {
std::unique_ptr<T> value;
std::atomic<Node*> next;
};
void push( Node* node ) {
Node* prev = head.exchange( node );
prev->next = node;
}
Node stub;
std::atomic<Node*> head;
Node* tail;
};
// test/main.cpp
#pragma clang diagnostic push
#pragma ide diagnostic ignored "OCUnusedMacroInspection"
#define BOOST_TEST_MODULE test_module
#pragma clang diagnostic pop
#include <boost/test/unit_test.hpp>
// test/utils.hpp
#pragma once
#include <vector>
template< class T >
void removeFromBothIfIdentical( std::vector<T>& a, std::vector<T>& b ) {
size_t i = 0;
size_t j = 0;
while ( i < a.size() && j < b.size()) {
if ( a[ i ] == b[ j ] ) {
a.erase( a.begin() + i );
b.erase( b.begin() + j );
}
else if ( a[ i ] < b[ j ] ) ++i;
else if ( a[ i ] > b[ j ] ) ++j;
}
}
namespace std {
template< typename T >
std::ostream& operator<<( std::ostream& ostream, const std::vector<T>& container ) {
if ( container.empty())
return ostream << "[]";
ostream << "[";
std::string_view separator;
for ( const auto& item: container ) {
ostream << item << separator;
separator = ", ";
}
return ostream << "]";
}
}
template< class T >
std::vector<T> extractDuplicates( std::vector<T>& container ) {
auto iter = std::unique( container.begin(), container.end());
std::vector<T> duplicates;
std::move( iter, container.end(), back_inserter( duplicates ));
return duplicates;
}
#define CHECK_EMPTY( container, message ) \
BOOST_CHECK_MESSAGE( (container).empty(), (message) << ": " << (container) )
// test/MpscQueue.cpp
#pragma ide diagnostic ignored "cert-err58-cpp"
#include <thread>
#include <numeric>
#include <boost/test/unit_test.hpp>
#include "../src/MpscQueue.hpp"
#include "utils.hpp"
using std::thread;
using std::vector;
using std::back_inserter;
BOOST_AUTO_TEST_SUITE( MpscQueueTestSuite )
BOOST_AUTO_TEST_CASE( two_producers ) {
constexpr int until = 1000;
MpscQueue<int> queue;
thread producerEven( [ & ]() {
for ( int i = 0; i < until; i += 2 )
queue.push( i );
} );
thread producerOdd( [ & ]() {
for ( int i = 1; i < until; i += 2 )
queue.push( i );
} );
vector<int> actual;
thread consumer( [ & ]() {
using namespace std::chrono_literals;
std::this_thread::sleep_for( 2ms );
while ( auto n = queue.pop())
actual.push_back( *n );
} );
producerEven.join();
producerOdd.join();
consumer.join();
vector<int> expected( until );
std::iota( expected.begin(), expected.end(), 0 );
std::sort( actual.begin(), actual.end());
vector<int> duplicates = extractDuplicates( actual );
removeFromBothIfIdentical( expected, actual );
CHECK_EMPTY( duplicates, "Duplicate items" );
CHECK_EMPTY( expected, "Missing items" );
CHECK_EMPTY( actual, "Extra items" );
}
BOOST_AUTO_TEST_SUITE_END()
答案 0 :(得分:1)
下面我的多生产者,单消费者示例是用Ada编写的。我将其作为虚拟“伪代码”的来源提供给您考虑。该示例包含三个文件。
该示例实现了一个简单的数据记录器,该记录器具有多个生产者,一个共享缓冲区和一个用于记录生产者产生的字符串的使用者。
第一个文件是共享缓冲区的程序包规范。 Ada软件包规范为该软件包中定义的实体定义了API。在这种情况下,实体是受保护的缓冲区,是停止记录器的过程。
-----------------------------------------------------------------------
-- Asynchronous Data Logger
-----------------------------------------------------------------------
with Ada.Strings.Unbounded; use Ada.Strings.Unbounded;
package Async_Logger is
type Queue_Index is mod 256;
type Queue_T is array (Queue_Index) of Unbounded_String;
protected Buffer is
entry Put (Log_Entry : in String);
entry Get (Stamped_Entry : out Unbounded_String);
private
Queue : Queue_T;
P_Index : Queue_Index := 0;
G_Index : Queue_Index := 0;
Count : Natural := 0;
end Buffer;
procedure Stop_Logging;
end Async_Logger;
受保护缓冲区中的条目允许任务(即线程)写入缓冲区并从缓冲区中读取。这些条目会自动执行对缓冲区的所有必要的锁定控制。
缓冲区代码的实现和Stop_Logging过程在程序包主体中实现。进行日志记录的使用者也实现在任务主体中,从而使使用者对生产线程不可见。
with Ada.Calendar; use Ada.Calendar;
with Ada.Calendar.Formatting; use Ada.Calendar.Formatting;
with Ada.Text_IO; use Ada.Text_IO;
package body Async_Logger is
------------
-- Buffer --
------------
protected body Buffer is
---------
-- Put --
---------
entry Put (Log_Entry : in String) when Count < Queue_Index'Modulus is
T_Stamp : Time := Clock;
Value : Unbounded_String :=
To_Unbounded_String
(Image (Date => T_Stamp, Include_Time_Fraction => True) & " : " &
Log_Entry);
begin
Queue (P_Index) := Value;
P_Index := P_Index + 1;
Count := Count + 1;
end Put;
---------
-- Get --
---------
entry Get (Stamped_Entry : out Unbounded_String) when Count > 0 is
begin
Stamped_Entry := Queue (G_Index);
G_Index := G_Index + 1;
Count := Count - 1;
end Get;
end Buffer;
task Logger is
entry Stop;
end Logger;
task body Logger is
Phrase : Unbounded_String;
begin
loop
select
accept Stop;
exit;
else
select
Buffer.Get (Phrase);
Put_Line (To_String (Phrase));
or
delay 0.01;
end select;
end select;
end loop;
end Logger;
procedure Stop_Logging is
begin
Logger.Stop;
end Stop_Logging;
end Async_Logger;
Put条目具有保护条件,允许该条目仅在缓冲区未满时才执行。 Get条目具有保护条件,允许该条件仅在缓冲区为空时才执行。
名为Logger的任务是使用者任务。它一直运行到调用其Stop条目为止。
Stop_Logging过程调用记录器的Stop条目。
第三个文件是用于测试Async_Logger程序包的“主要”过程。该文件创建两个生产者P1和P2。这些生产者分别向缓冲区写入10条消息,然后退出。
with Async_Logger; use Async_Logger;
procedure Async_Test is
task P1;
task P2;
task body P1 is
begin
for I in 1..10 loop
Buffer.Put(I'Image);
delay 0.01;
end loop;
end P1;
task body P2 is
Num : Float := 0.0;
begin
for I in 1..10 loop
Buffer.Put(Num'Image);
Num := Num + 1.0;
delay 0.01;
end loop;
end P2;
begin
delay 0.2;
Stop_Logging;
end Async_Test;
Async_Test过程只需等待0.2秒,然后调用Stop_Logging。
该程序运行的输出为:
2019-02-11 18:35:01.83 : 1
2019-02-11 18:35:01.83 : 0.00000E+00
2019-02-11 18:35:01.85 : 1.00000E+00
2019-02-11 18:35:01.85 : 2
2019-02-11 18:35:01.87 : 3
2019-02-11 18:35:01.87 : 2.00000E+00
2019-02-11 18:35:01.88 : 3.00000E+00
2019-02-11 18:35:01.88 : 4
2019-02-11 18:35:01.90 : 5
2019-02-11 18:35:01.90 : 4.00000E+00
2019-02-11 18:35:01.92 : 6
2019-02-11 18:35:01.92 : 5.00000E+00
2019-02-11 18:35:01.93 : 6.00000E+00
2019-02-11 18:35:01.93 : 7
2019-02-11 18:35:01.95 : 7.00000E+00
2019-02-11 18:35:01.95 : 8
2019-02-11 18:35:01.96 : 8.00000E+00
2019-02-11 18:35:01.96 : 9
2019-02-11 18:35:01.98 : 10
2019-02-11 18:35:01.98 : 9.00000E+00
答案 1 :(得分:0)
您的推送功能缺少该行:
node->next = nullptr;
在顶部。
在评论中查看我的实现以及大量分析, 此处:https://github.com/CarloWood/ai-utils/blob/master/threading/MpscQueue.h