[英]Is this lock-free .NET queue thread safe?
My question is, is the class included below for a single-reader single-writer queue class thread-safe? 我的问题是,下面包含的类对于单读者单编写器队列类线程安全吗? This kind of queue is called lock-free, even if it will block if the queue is filled. 这种队列称为无锁,即使队列已填满也会阻塞。 The data structure was inspired by Marc Gravell's implementation of a blocking queue here at StackOverflow. 数据结构的灵感来自Marc Gravell在StackOverflow 上实现的阻塞队列 。
The point of the structure is to allow a single thread to write data to the buffer, and another thread to read data. 结构的要点是允许单个线程将数据写入缓冲区,而另一个线程则读取数据。 All of this needs to happen as quickly as possible. 所有这些都需要尽快发生。
A similar data structure is described in an article at DDJ by Herb Sutter , except the implementation is in C++. Herb Sutter在DDJ的文章中描述了类似的数据结构,但实现是在C ++中。 Another difference is that I use a vanilla linked list, I use a linked list of arrays. 另一个区别是我使用了一个vanilla链表,我使用了一个链表的数组。
Rather than just including a snippet of code I include the whole thing with comment with a permissive open source license (MIT License 1.0) in case anyone finds it useful, and wants to use it (as-is or modified). 我不是仅仅包含一段代码,而是将所有内容与允许的开源许可证(MIT许可证1.0)一起包含,以防任何人发现它有用,并且想要使用它(原样或修改)。
This is related to other questions asked on Stack Overflow of how to create a blocking concurrent queues (see Creating a blockinq Queue in .NET and Thread-safe blocking queue implementation in .NET ). 这与Stack Overflow上有关如何创建阻塞并发队列的其他问题有关(请参阅在.NET中创建blockinq队列和在.NET中创建 线程安全阻塞队列 )。
Here is the code: 这是代码:
using System;
using System.Collections.Generic;
using System.Threading;
using System.Diagnostics;
namespace CollectionSandbox
{
/// This is a single reader / singler writer buffered queue implemented
/// with (almost) no locks. This implementation will block only if filled
/// up. The implementation is a linked-list of arrays.
/// It was inspired by the desire to create a non-blocking version
/// of the blocking queue implementation in C# by Marc Gravell
/// https://stackoverflow.com/questions/530211/creating-a-blocking-queuet-in-net/530228#530228
class SimpleSharedQueue<T> : IStreamBuffer<T>
{
/// Used to signal things are no longer full
ManualResetEvent canWrite = new ManualResetEvent(true);
/// This is the size of a buffer
const int BUFFER_SIZE = 512;
/// This is the maximum number of nodes.
const int MAX_NODE_COUNT = 100;
/// This marks the location to write new data to.
Cursor adder;
/// This marks the location to read new data from.
Cursor remover;
/// Indicates that no more data is going to be written to the node.
public bool completed = false;
/// A node is an array of data items, a pointer to the next item,
/// and in index of the number of occupied items
class Node
{
/// Where the data is stored.
public T[] data = new T[BUFFER_SIZE];
/// The number of data items currently stored in the node.
public Node next;
/// The number of data items currently stored in the node.
public int count;
/// Default constructor, only used for first node.
public Node()
{
count = 0;
}
/// Only ever called by the writer to add new Nodes to the scene
public Node(T x, Node prev)
{
data[0] = x;
count = 1;
// The previous node has to be safely updated to point to this node.
// A reader could looking at the point, while we set it, so this should be
// atomic.
Interlocked.Exchange(ref prev.next, this);
}
}
/// This is used to point to a location within a single node, and can perform
/// reads or writers. One cursor will only ever read, and another cursor will only
/// ever write.
class Cursor
{
/// Points to the parent Queue
public SimpleSharedQueue<T> q;
/// The current node
public Node node;
/// For a writer, this points to the position that the next item will be written to.
/// For a reader, this points to the position that the next item will be read from.
public int current = 0;
/// Creates a new cursor, pointing to the node
public Cursor(SimpleSharedQueue<T> q, Node node)
{
this.q = q;
this.node = node;
}
/// Used to push more data onto the queue
public void Write(T x)
{
Trace.Assert(current == node.count);
// Check whether we are at the node limit, and are going to need to allocate a new buffer.
if (current == BUFFER_SIZE)
{
// Check if the queue is full
if (q.IsFull())
{
// Signal the canWrite event to false
q.canWrite.Reset();
// Wait until the canWrite event is signaled
q.canWrite.WaitOne();
}
// create a new node
node = new Node(x, node);
current = 1;
}
else
{
// If the implementation is correct then the reader will never try to access this
// array location while we set it. This is because of the invariant that
// if reader and writer are at the same node:
// reader.current < node.count
// and
// writer.current = node.count
node.data[current++] = x;
// We have to use interlocked, to assure that we incremeent the count
// atomicalluy, because the reader could be reading it.
Interlocked.Increment(ref node.count);
}
}
/// Pulls data from the queue, returns false only if
/// there
public bool Read(ref T x)
{
while (true)
{
if (current < node.count)
{
x = node.data[current++];
return true;
}
else if ((current == BUFFER_SIZE) && (node.next != null))
{
// Move the current node to the next one.
// We know it is safe to do so.
// The old node will have no more references to it it
// and will be deleted by the garbage collector.
node = node.next;
// If there is a writer thread waiting on the Queue,
// then release it.
// Conceptually there is a "if (q.IsFull)", but we can't place it
// because that would lead to a Race condition.
q.canWrite.Set();
// point to the first spot
current = 0;
// One of the invariants is that every node created after the first,
// will have at least one item. So the following call is safe
x = node.data[current++];
return true;
}
// If we get here, we have read the most recently added data.
// We then check to see if the writer has finished producing data.
if (q.completed)
return false;
// If we get here there is no data waiting, and no flagging of the completed thread.
// Wait a millisecond. The system will also context switch.
// This will allow the writing thread some additional resources to pump out
// more data (especially if it iself is multithreaded)
Thread.Sleep(1);
}
}
}
/// Returns the number of nodes currently used.
private int NodeCount
{
get
{
int result = 0;
Node cur = null;
Interlocked.Exchange<Node>(ref cur, remover.node);
// Counts all nodes from the remover to the adder
// Not efficient, but this is not called often.
while (cur != null)
{
++result;
Interlocked.Exchange<Node>(ref cur, cur.next);
}
return result;
}
}
/// Construct the queue.
public SimpleSharedQueue()
{
Node root = new Node();
adder = new Cursor(this, root);
remover = new Cursor(this, root);
}
/// Indicate to the reader that no more data is going to be written.
public void MarkCompleted()
{
completed = true;
}
/// Read the next piece of data. Returns false if there is no more data.
public bool Read(ref T x)
{
return remover.Read(ref x);
}
/// Writes more data.
public void Write(T x)
{
adder.Write(x);
}
/// Tells us if there are too many nodes, and can't add anymore.
private bool IsFull()
{
return NodeCount == MAX_NODE_COUNT;
}
}
}
Microsoft Research CHESS应该被证明是一个测试实现的好工具。
The presence of Sleep()
makes a lock-free approach totally useless. Sleep()
的存在使得无锁方法完全没用。 The only reason to confront the complexities of a lock-free design is the need for absolute speed and to avoid the cost of Semaphores. 面对无锁设计复杂性的唯一理由是需要绝对速度并避免信号量的成本。 The use of Sleep(1) defeats that purpose totally. 睡眠(1)的使用完全击败了这个目的。
Given that I can't find any reference that the Interlocked.Exchange does Read or Write blocks, I would say not. 鉴于我找不到Interlocked.Exchange执行读取或写入块的任何引用,我会说不。 I would also question why you want to go lockless, as seldom gives enough benefits to counter it's complexity. 我也会质疑为什么你想要无锁,因为很少有足够的好处来抵消它的复杂性。
Microsoft had an excellent presentation at the 2009 GDC on this, and you can get the slides here . 微软在2009年的GDC上有一个很好的演示,你可以在这里获得幻灯片。
Beware of the double checked - single lock pattern (as in a link quoted above: http://www.yoda.arachsys.com/csharp/singleton.html ) 注意双重检查 - 单锁模式(如上面引用的链接: http : //www.yoda.arachsys.com/csharp/singleton.html )
Quoting verbatim from the "Modern C++ Design" by Andrei Alexandrescu 从Andrei Alexandrescu的“现代C ++设计”中逐字引用
I suspect it is not thread safe - imagine the following scenario: 我怀疑它不是线程安全的 - 想象一下以下场景:
two threads enter cursor.Write
. 两个线程进入cursor.Write
。 The first gets as far as line node = new Node(x, node);
第一个获取到line node = new Node(x, node);
in the true half of the if (current == BUFFER_SIZE)
statement (but let's also assume that current == BUFFER_SIZE
) so when 1 gets added to current
then another thread coming in would follow the other path through the if statement. 在if (current == BUFFER_SIZE)
语句的真正一半中(但是我们也假设current == BUFFER_SIZE
)所以当1被添加到current
另一个进入的线程将通过if语句跟随另一个路径。 Now imagine that thread 1 loses its time slice and thread 2 gets it, and proceeds to enter the if statement on the mistaken belief that the condition still held. 现在想象一下,线程1失去了它的时间片,线程2得到它,然后继续输入if语句,错误地认为条件仍然存在。 It should have entered the other path. 它应该进入另一条路径。
I haven't run this code either, so I'm not sure if my assumptions are possible in this code, but if they are (ie entering cursor.Write from multiple threads when current == BUFFER_SIZE
), then it may well be prone to concurrency errors. 我也没有运行这个代码,所以我不确定我的假设在这段代码中是否可行,但是如果它们是(即当current == BUFFER_SIZE
时从多个线程进入current == BUFFER_SIZE
),那么它可能很容易发生并发错误。
First, I wonder about the assumption in these two lines of sequential code: 首先,我想知道这两行顺序代码的假设:
node.data[current++] = x;
// We have to use interlocked, to assure that we incremeent the count
// atomicalluy, because the reader could be reading it.
Interlocked.Increment(ref node.count);
What is to say that the new value of node.data[] has been committed to this memory location? 可以说node.data []的新值已经提交到这个内存位置了吗? It is not stored in a volatile memory address and therefore can be cached if I understand it correctly? 它不存储在易失性存储器地址中,因此如果我理解正确,它可以被缓存吗? Doesn't this potentially lead to a 'dirty' read? 这不会导致'脏'读? There may be other places the same is true, but this one stood out at a glance. 可能还有其他地方也是如此,但这一个地方一目了然。
Second, multi-threaded code that contains the following: 第二,包含以下内容的多线程代码:
Thread.Sleep(int);
... is never a good sign. ......永远不是一个好兆头。 If it's required then the code is destined to fail, if it isn't required it's a waste. 如果需要,那么代码注定要失败,如果不需要它就是浪费。 I really wish they would remove this API entirely. 我真的希望他们完全删除这个API。 Realize that is a request to wait at least that amount of time. 意识到这是一个等待至少那段时间的请求。 With the overhead of context switching your almost certainly going to wait longer, a lot longer. 随着上下文切换的开销,你几乎肯定会等待更长时间。
Third, I completely don't understand the use of the Interlock API here. 第三,我完全不了解Interlock API在这里的使用。 Maybe I'm tired and just missing the point; 也许我累了,只是错过了重点; but I can't find the potential thread conflict on both threads reading & writing to the same variable? 但我找不到两个线程读取和写入同一个变量的潜在线程冲突? It would seem that the only use I could find for interlock exchange would be to modify the contents of node.data[] to fix #1 above. 似乎我能找到的互锁交换的唯一用途是修改node.data []的内容以修复上面的#1。
Lastly it would seem that the implementation is somewhat over-complicated. 最后,似乎实施有点过于复杂。 Am I missing the point of the whole Cursor/Node thing or is it basically doing the same thing as this class? 我错过了整个Cursor / Node的观点,还是基本上和这个类做同样的事情? (Note: I haven't tried it and I don't think this is thread safe either, just trying to boil down what I think your doing.) (注意:我没有尝试过,我也不认为这也是线程安全的,只是试图归结我认为你在做什么。)
class ReaderWriterQueue<T>
{
readonly AutoResetEvent _readComplete;
readonly T[] _buffer;
readonly int _maxBuffer;
int _readerPos, _writerPos;
public ReaderWriterQueue(int maxBuffer)
{
_readComplete = new AutoResetEvent(true);
_maxBuffer = maxBuffer;
_buffer = new T[_maxBuffer];
_readerPos = _writerPos = 0;
}
public int Next(int current) { return ++current == _maxBuffer ? 0 : current; }
public bool Read(ref T item)
{
if (_readerPos != _writerPos)
{
item = _buffer[_readerPos];
_readerPos = Next(_readerPos);
return true;
}
else
return false;
}
public void Write(T item)
{
int next = Next(_writerPos);
while (next == _readerPos)
_readComplete.WaitOne();
_buffer[next] = item;
_writerPos = next;
}
}
So am I totally off-base here and am failing to see the magic in the original class? 所以我完全偏离了这里,并没有看到原始课程中的魔力?
I must admit one thing, I despise Threading. 我必须承认一件事,我鄙视线程。 I've seen the best developers fail at it. 我见过最好的开发者失败了。 This article gives a great example on how hard it is to get threading right: 本文给出了一个很好的例子,说明如何正确处理线程: http://www.yoda.arachsys.com/csharp/singleton.html http://www.yoda.arachsys.com/csharp/singleton.html
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.