Multithreading MPSC队列:竞争条件

Multithreading MPSC队列:竞争条件,multithreading,c++17,race-condition,producer-consumer,task-queue,Multithreading,C++17,Race Condition,Producer Consumer,Task Queue,我试图实现一个基于Dmitry Vyukov的无锁多生产者单消费者队列 到目前为止,我编写的单一测试几乎可以正常工作。但消费者通常只遗漏一项,第一项或第二项。有时,消费者会错过大约一半的输入 现在,它不是无锁的。每次使用new操作符时,它都会锁定,但我希望它能够正常工作,并在使用分配器之前编写一些更详尽的测试 // src/MpscQueue.hpp #pragma once #include <memory> #include <atomic> #include &

我试图实现一个基于Dmitry Vyukov的无锁多生产者单消费者队列

到目前为止,我编写的单一测试几乎可以正常工作。但消费者通常只遗漏一项,第一项或第二项。有时,消费者会错过大约一半的输入

现在,它不是无锁的。每次使用
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()
//src/MpscQueue.hpp
#布拉格语一次
#包括
#包括
#包括
/**
*改编自http://www.1024cores.net/home/lock-free-algorithms/queues/intrusive-mpsc-node-based-queue
*@tparam T
*/
模板
类MpscQueue{
公众:
MpscQueue(){
stub.next.store(nullptr);
总库(和存根);
尾=&stub;
}
无效推力(常数T&T){
侵位(t);
}
无效推送(T&T){
炮位(std::move(t));
}
模板
空侵位(Args…Args){
自动节点=新节点{std::make_unique(std::forward(args)…),nullptr};
推送(节点);
}
/**
*从队列中返回项目并返回指向该项目的唯一指针。
*
*如果队列为空,则返回设置为nullptr的唯一指针
*
*@将唯一的ptr返回到弹出的项目
*/
std::unique_ptr pop(){
节点*tailCopy=tail;
Node*next=tailCopy->next.load();
自动完成=[&](){
尾=下一个;
std::unique_ptr p(tailCopy);//返回后释放节点内存
返回标准::移动(尾部->值);
};
如果(tailCopy==&存根){
if(next==nullptr)返回nullptr;
尾=下一个;
tailCopy=next;
下一步=下一步->下一步;
}
if(next)返回std::move(finalize());
if(tail!=head.load())返回null ptr;
推送(&stub);
下一步=尾拷贝->下一步;
返回next?std::move(finalize()):nullptr;
}
私人:
结构节点{
std::唯一的ptr值;
std::原子下一步;
};
无效推送(节点*节点){
Node*prev=头交换(Node);
上一步->下一步=节点;
}
节点存根;
原子头;
节点*尾部;
};
//test/main.cpp
#pragma-clang诊断推送
#pragma ide诊断忽略“OCUnusedMacroInspection”
#定义升压测试模块测试模块
#pragma-clang诊断流行语
#包括
//test/utils.hpp
#布拉格语一次
#包括
模板
void removeFromBothIfIdentical(std::vector&a、std::vector&b){
尺寸i=0;
尺寸j=0;
而(ib[j])++j;
}
}
名称空间标准{
模板

std::ostream&operator下面我的多生产者、单消费者示例是用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_日志记录过程的实现在包体中实现。执行日志记录的使用者也在任务体中实现,使使用者对生产线程不可见

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;
Async_测试过程只需等待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

您的推送功能缺少该行:

node->next = nullptr;
在顶端

请参见我的实施以及评论中的大量分析,
此处:

您在哪个处理器体系结构上运行测试?@SegFault,i64。特别是Intel(R)Core(TM)i5-7500 CPU@3.40GHzDid您的机器上的测试通过了吗?我以前从未与ada合作过,我不太了解它如何同步所有东西的细节。队列是线程安全结构吗?它是阻塞吗?此外,我的目标是永远不会阻塞,除非它获得内存来创建新节点。ada保护类型和保护类型cted对象是线程安全的。每个条目自动处理锁定和解锁。每个条目上的保护条件也会导致调用任务暂停,直到保护条件计算为True。在多生产者单消费者模式中需要阻塞,因为每个生产者只有在没有其他生产者的情况下才能写入共享缓冲区写入共享缓冲区,当生产者向缓冲区写入时,消费者无法从共享缓冲区读取。这些行为会产生竞争条件和损坏的缓冲状态。在受保护的条目内的代码可以与C或C++中的关键部分进行比较。所有这些操作必须以防止竞争条件的方式执行。。在我上面的示例中,缓冲区包含一个队列以及P_索引、C_索引和计数。这些都是状态变量,必须与缓冲区上的每个操作一致。如果两个生产者修改同一队列元素,则结果是数据损坏或丢失。类似地,两个试图同时更新P_索引和计数的生产者将l产生不可预测的结果。链接已断开。谢谢!我最近将线程相关实用程序移动到另一个git submodu
node->next = nullptr;