我试图在.NET中实现一个线程化队列,但是当我通过测试运行它时遇到了一些麻烦。
允许实现放弃使用线程的一些复杂性,因为它强制只有一个线程会将项放入队列中,并且只有一个线程会将它们取出(这是设计)。
问题在于,Take()有时会跳过一个项目,好像它从来没有出现过,在我的测试中我会得到"预期:736但是:737"。我无法在此代码中的任何地方看到会发生这种影响; Put只会放在最后一个项目之后(因此它不应该直接影响this.m_Head)并且Take正在使用Interlocked.Exchange从头部获取项目。
此实施如何允许问题发生?
实施
using System;
using System.Threading;
#pragma warning disable 420
namespace Tychaia.Threading
{
public class TaskPipeline<T>
{
private int? m_InputThread;
private int? m_OutputThread;
private volatile TaskPipelineEntry<T> m_Head;
/// <summary>
/// Creates a new TaskPipeline with the current thread being
/// considered to be the input side of the pipeline. The
/// output thread should call Connect().
/// </summary>
public TaskPipeline()
{
this.m_InputThread = Thread.CurrentThread.ManagedThreadId;
this.m_OutputThread = null;
}
/// <summary>
/// Connects the current thread as the output of the pipeline.
/// </summary>
public void Connect()
{
if (this.m_OutputThread != null)
throw new InvalidOperationException("TaskPipeline can only have one output thread connected.");
this.m_OutputThread = Thread.CurrentThread.ManagedThreadId;
}
/// <summary>
/// Puts an item into the queue to be processed.
/// </summary>
/// <param name="value">Value.</param>
public void Put(T value)
{
if (this.m_InputThread != Thread.CurrentThread.ManagedThreadId)
throw new InvalidOperationException("Only the input thread may place items into TaskPipeline.");
// Walk the queued items until we find one that
// has Next set to null.
var head = this.m_Head;
while (head != null)
{
if (head.Next != null)
head = head.Next;
if (head.Next == null)
break;
}
if (head == null)
this.m_Head = new TaskPipelineEntry<T> { Value = value };
else
head.Next = new TaskPipelineEntry<T> { Value = value };
}
/// <summary>
/// Takes the next item from the pipeline, or blocks until an item
/// is recieved.
/// </summary>
/// <returns>The next item.</returns>
public T Take()
{
if (this.m_OutputThread != Thread.CurrentThread.ManagedThreadId)
throw new InvalidOperationException("Only the output thread may retrieve items from TaskPipeline.");
// Wait until there is an item to take.
var spin = new SpinWait();
while (this.m_Head == null)
spin.SpinOnce();
// Return the item and exchange the current head with
// the next item, all in an atomic operation.
return Interlocked.Exchange(ref this.m_Head, this.m_Head.Next).Value;
}
}
}
#pragma warning restore 420
测试失败:
[Test]
public void TestPipelineParallelTo100()
{
var random = new Random();
var pipeline = new TaskPipeline<int>();
var success = true;
int expected = 0, actual = 0;
ThreadStart processor = () =>
{
pipeline.Connect();
for (int i = 0; i < 100; i++)
{
var v = pipeline.Take();
if (v != i)
{
success = false;
expected = i;
actual = v;
break;
}
Thread.Sleep(random.Next(1, 10));
}
};
var thread = new Thread(processor);
thread.Start();
for (int i = 0; i < 100; i++)
{
pipeline.Put(i);
Thread.Sleep(random.Next(1, 10));
}
thread.Join();
if (!success)
Assert.AreEqual(expected, actual);
}
答案 0 :(得分:0)
如果在m_Head.Next
中读取Take
后传递给Interlocked.Exchange(ref this.m_Head, this.m_Head.Next)
,则指定m_Head
的值,指针将会丢失,因为访问它的唯一方法是通过{{1 }}
Take
读取m_Head.Next
(==null
)Put
撰写m_Head.Next
(!=null
)Take
撰写m_Head
(==null
) 编辑这应该有用。我使用了非空的标记值和Interlocked.CompareExchange
来确保Put
不会尝试重用Take
已删除的条目。
修改2:调整为Take
。
编辑3:我认为如果标识的尾部为goto retry;
,我仍需要在Put
中添加Entry.Sentinel
。
using System;
using System.Threading;
#pragma warning disable 420
namespace Tychaia.Threading
{
public class TaskPipeline<T>
{
private int? m_InputThread;
private int? m_OutputThread;
private volatile Entry m_Head;
private sealed class Entry
{
public static readonly Entry Sentinel = new Entry(default(T));
public readonly T Value;
public Entry Next;
public Entry(T value)
{
Value = value;
Next = null;
}
}
/// <summary>
/// Creates a new TaskPipeline with the current thread being
/// considered to be the input side of the pipeline. The
/// output thread should call Connect().
/// </summary>
public TaskPipeline()
{
this.m_InputThread = Thread.CurrentThread.ManagedThreadId;
this.m_OutputThread = null;
}
/// <summary>
/// Connects the current thread as the output of the pipeline.
/// </summary>
public void Connect()
{
if (this.m_OutputThread != null)
throw new InvalidOperationException("TaskPipeline can only have one output thread connected.");
this.m_OutputThread = Thread.CurrentThread.ManagedThreadId;
}
/// <summary>
/// Puts an item into the queue to be processed.
/// </summary>
/// <param name="value">Value.</param>
public void Put(T value)
{
if (this.m_InputThread != Thread.CurrentThread.ManagedThreadId)
throw new InvalidOperationException("Only the input thread may place items into TaskPipeline.");
retry:
// Walk the queued items until we find one that
// has Next set to null.
var head = this.m_Head;
while (head != null)
{
if (head.Next != null)
head = head.Next;
if (head.Next == null)
break;
}
if (head == null)
{
if (Interlocked.CompareExchange(ref m_Head, new Entry(value), null) != null)
goto retry;
}
else
{
if (Interlocked.CompareExchange(ref head.Next, new Entry(value), null) != null)
goto retry;
}
}
/// <summary>
/// Takes the next item from the pipeline, or blocks until an item
/// is recieved.
/// </summary>
/// <returns>The next item.</returns>
public T Take()
{
if (this.m_OutputThread != Thread.CurrentThread.ManagedThreadId)
throw new InvalidOperationException("Only the output thread may retrieve items from TaskPipeline.");
// Wait until there is an item to take.
var spin = new SpinWait();
while (this.m_Head == null)
spin.SpinOnce();
// Return the item and exchange the current head with
// the next item, all in an atomic operation.
Entry head = m_Head;
retry:
Entry next = head.Next;
// replace m_Head.Next with a non-null sentinel to ensure Put won't try to reuse it
if (Interlocked.CompareExchange(ref head.Next, Entry.Sentinel, next) != next)
goto retry;
m_Head = next;
return head.Value;
}
}
}