C# NET中线程化队列实现的问题

C# NET中线程化队列实现的问题,c#,multithreading,unit-testing,mono,C#,Multithreading,Unit Testing,Mono,我试图在.NET中实现一个线程队列,但在运行它进行测试时遇到了一些问题 该实现允许避免线程的一些复杂性,因为它强制只有一个线程将项目放入队列,只有一个线程将项目取出(这是设计的) 问题是,有时Take()会跳过一个项目,就好像它从来没有出现过一样,在我的测试中,我会得到“预期的:736,但实际上是:737”。我看不出在这段代码的任何地方会出现这种效果;Put将只放在最后一个项目之后(因此它不应该直接影响此.m_头部),Take使用联锁.Exchange从头部获取项目 此实现如何允许问题发生 实施

我试图在.NET中实现一个线程队列,但在运行它进行测试时遇到了一些问题

该实现允许避免线程的一些复杂性,因为它强制只有一个线程将项目放入队列,只有一个线程将项目取出(这是设计的)

问题是,有时Take()会跳过一个项目,就好像它从来没有出现过一样,在我的测试中,我会得到“预期的:736,但实际上是:737”。我看不出在这段代码的任何地方会出现这种效果;Put将只放在最后一个项目之后(因此它不应该直接影响此.m_头部),Take使用联锁.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);
}
使用系统;
使用系统线程;
#pragma警告禁用420
名称空间Tychaia.Threading
{
公共类任务管道
{
私有int?m_输入线程;
私有int?m_输出线程;
私有易失性TaskPipelineEntry m_头;
/// 
///创建一个新的TaskPipeline,当前线程为
///被认为是管道的输入端。该
///输出线程应调用Connect()。
/// 
公共任务管道()
{
this.m_InputThread=Thread.CurrentThread.ManagedThreadId;
this.m_OutputThread=null;
}
/// 
///连接当前线程作为管道的输出。
/// 
公共void Connect()
{
if(this.m_OutputThread!=null)
抛出新的InvalidOperationException(“TaskPipeline只能连接一个输出线程”);
this.m_OutputThread=Thread.CurrentThread.ManagedThreadId;
}
/// 
///将项目放入要处理的队列。
/// 
///价值观。
公开作废认沽期权(T值)
{
if(this.m_InputThread!=Thread.CurrentThread.ManagedThreadId)
抛出新的InvalidOperationException(“只有输入线程可以将项目放入TaskPipeline。”);
//遍历排队的项目,直到找到一个
//已将Next设置为null。
var head=此m_head;
while(head!=null)
{
if(head.Next!=null)
头=头。下一步;
if(head.Next==null)
打破
}
if(head==null)
this.m_Head=new TaskPipelineEntry{Value=Value};
其他的
head.Next=new TaskPipelineEntry{Value=Value};
}
/// 
///从管道中获取下一个项目,或阻塞到某个项目
///收到了。
/// 
///下一项。
公众不接受
{
if(this.m_OutputThread!=Thread.CurrentThread.ManagedThreadId)
抛出新的InvalidOperationException(“只有输出线程可以从TaskPipeline检索项”);
//等待,直到有一个项目采取。
var spin=new SpinWait();
while(this.m_Head==null)
spin.SpinOnce();
//返回项目并与交换当前标头
//下一项,全部在原子操作中。
返回联锁交换(参考this.m_Head,this.m_Head.Next)值;
}
}
}
#pragma警告恢复420
测试失败:

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);
}
[测试]
公共void TestPipelineParallelTo100()
{
var random=新的random();
var pipeline=new TaskPipeline();
var成功=真;
int预期值=0,实际值=0;
ThreadStart处理器=()=>
{
管道连接();
对于(int i=0;i<100;i++)
{
var v=pipeline.Take();
如果(v!=i)
{
成功=错误;
预期=i;
实际值=v;
打破
}
睡眠(random.Next(1,10));
}
};
var线程=新线程(处理器);
thread.Start();
对于(int i=0;i<100;i++)
{
管道铺设(i);
睡眠(random.Next(1,10));
}
thread.Join();
如果(!成功)
断言.AreEqual(预期、实际);
}

如果在
中读取
m\u Head.Next
后指定
m\u Head.Next的值,将其传递到
联锁交换(参考this.m\u Head,this.m\u Head.Next)
,指针将丢失,因为访问它的唯一方法是通过
m\u Head

  • Take
    读取
    m_头。下一步
    ==null
  • Put
    写入
    m_头。下一步
    !=null
  • Take
    写入
    m_头
    ==null
编辑:这应该可以。我使用了一个非空的sentinel值,并且
是互锁的。比较Exchange
以确保
Put
不会尝试重用
Take
已删除的条目

编辑2:调整到
获取

编辑3:我认为我仍然需要添加一个
转到重试Put
中输入code>

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;
        }
    }
}
使用系统;
使用系统线程;
#pragma警告禁用420
名称空间Tychaia.Threading
{
公共类任务管道
{
私有int?m_输入线程;
私有int?m_输出线程;
私人易变分录m_头;
私人密封类条目
{
公共静态只读条目Sentinel=新条目(默认值(T));
公共只读T值;
下一步公开入场;
公共入口(T值)
{
价值=价值;
Next=null;
}
}
///