AutoResetEvent未正确阻止

时间:2010-08-25 17:01:08

标签: c# .net multithreading autoresetevent

我有一个线程,它创建可变数量的工作线程并在它们之间分配任务。这可以通过传递一个 TaskQueue 对象来解决,您将在下面看到它的实现。

这些工作线程只是迭代它们给出的 TaskQueue 对象,执行每个任务。

private class TaskQueue : IEnumerable<Task>
{
    public int Count
    {
        get
        {
            lock(this.tasks)
            {
                return this.tasks.Count;
            }
        }
    }

    private readonly Queue<Task> tasks = new Queue<Task>();
    private readonly AutoResetEvent taskWaitHandle = new AutoResetEvent(false);

    private bool isFinishing = false;
    private bool isFinished = false;

    public void Enqueue(Task task)
    {
        Log.Trace("Entering Enqueue, lock...");
        lock(this.tasks)
        {
            Log.Trace("Adding task, current count = {0}...", Count);
            this.tasks.Enqueue(task);

            if (Count == 1)
            {
                Log.Trace("Count = 1, so setting the wait handle...");
                this.taskWaitHandle.Set();
            }
        }
        Log.Trace("Exiting enqueue...");
    }

    public Task Dequeue()
    {
        Log.Trace("Entering Dequeue...");
        if (Count == 0)
        {
            if (this.isFinishing)
            {
                Log.Trace("Finishing (before waiting) - isCompleted set, returning empty task.");
                this.isFinished = true;
                return new Task();
            }

            Log.Trace("Count = 0, lets wait for a task...");
            this.taskWaitHandle.WaitOne();
            Log.Trace("Wait handle let us through, Count = {0}, IsFinishing = {1}, Returned = {2}", Count, this.isFinishing);

            if(this.isFinishing)
            {
                Log.Trace("Finishing - isCompleted set, returning empty task.");
                this.isFinished = true;
                return new Task();
            }
        }

        Log.Trace("Entering task lock...");
        lock(this.tasks)
        {
            Log.Trace("Entered task lock, about to dequeue next item, Count = {0}", Count);
            return this.tasks.Dequeue();
        }
    }

    public void Finish()
    {
        Log.Trace("Setting TaskQueue state to isFinishing = true and setting wait handle...");
        this.isFinishing = true;

        if (Count == 0)
        {
            this.taskWaitHandle.Set();
        }
    }

    public IEnumerator<Task> GetEnumerator()
    {
        while(true)
        {
            Task t = Dequeue();
            if(this.isFinished)
            {
                yield break;
            }

            yield return t;
        }
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
}

正如您所看到的,我正在使用 AutoResetEvent 对象来确保工作线程不会过早退出,即在获取任何任务之前。

简而言之:

  • 主线程通过 Enqeueue 将任务分配给线程 - 将任务添加到其TaskQueue
  • 主线程通过调用TaskQueue的 Finish ()方法通知线程不再执行任务
  • 工作线程通过调用TaskQueue的 Dequeue ()方法检索分配给它的下一个任务

问题是 Dequeue ()方法经常抛出InvalidOperationException ,表示Queue为空。正如您所看到的,我添加了一些日志记录,结果是, AutoResetEvent 不会阻止 Dequeue(),即使没有对其 Set()方法。

据我了解,调用AutoResetEvent.Set()将允许等待线程继续(之前调用AutoResetEvent.WaitOne()),然后自动调用AutoResetEvent.Reset(),阻止下一个服务员。

那有什么不对?我弄错了吗?我某处有错误吗? 我现在坐在这上面3个小时,但我无法弄清楚出了什么问题。 请帮帮我!

非常感谢!

3 个答案:

答案 0 :(得分:6)

您的出列代码不正确。你在锁定下检查计数,然后通过裤子的接缝飞行,然后你希望任务有一些东西。释放锁定时,您无法保留假设:)。您的计数检查和任务。取消必须在锁定下发生:

bool TryDequeue(out Tasks task)
{
  task = null;
  lock (this.tasks) {
    if (0 < tasks.Count) {
      task = tasks.Dequeue();
    }
  }
  if (null == task) {
    Log.Trace ("Queue was empty");
  }
  return null != task;
 }

你的Enqueue()代码同样充满了问题。您的Enqueue / Dequeue不能确保进度(即使队列中有项目,您也会将已排队的线程阻塞等待)。您对Enqueue()的签名是错误的。总的来说你的帖子是非常糟糕的代码。坦率地说,我认为你试图咀嚼的比你在这里咬的更多......哦,永远不会锁定

我强烈建议您使用ConcurrentQueue

如果您无法访问.Net 4.0,那么这是一个让您入门的实现:

public class ConcurrentQueue<T>:IEnumerable<T>
{
    volatile bool fFinished = false;
    ManualResetEvent eventAdded = new ManualResetEvent(false);
    private Queue<T> queue = new Queue<T>();
    private object syncRoot = new object();

    public void SetFinished()
    {
        lock (syncRoot)
        {
            fFinished = true;
            eventAdded.Set();
        }
    }

    public void Enqueue(T t)
    {
        Debug.Assert (false == fFinished);
        lock (syncRoot)
        {
            queue.Enqueue(t);
            eventAdded.Set();
        }
    }

    private bool Dequeue(out T t)
    {
        do
        {
            lock (syncRoot)
            {
                if (0 < queue.Count)
                {
                    t = queue.Dequeue();
                    return true;
                }
                if (false == fFinished)
                {
                    eventAdded.Reset ();
                }
            }
            if (false == fFinished)
            {
                eventAdded.WaitOne();
            }
            else
            {
                break;
            }
        } while (true);
        t = default(T);
        return false;
    }


    public IEnumerator<T> GetEnumerator()
    {
        T t;
        while (Dequeue(out t))
        {
            yield return t;
        }
    }

    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
}

答案 1 :(得分:3)

我的更详细的答案正在等待,但我只是想指出一些非常重要的事情。

如果您使用的是.NET 3.5,可以使用ConcurrentQueue<T>Rx extensions library中包含一个backport,可用于.NET 3.5。

由于您需要阻止行为,因此需要将ConcurrentQueue<T>包裹在BlockingCollection<T>中(也可作为Rx的一部分使用)。

答案 2 :(得分:1)

看起来您正在尝试复制阻止队列。一个已经存在于.NET 4.0 BCL中的BlockingCollection。如果.NET 4.0不是您的选项,那么您可以使用此代码。它使用Monitor.WaitMonitor.Pulse方法代替AutoResetEvent

public class BlockingCollection<T>
{
    private Queue<T> m_Queue = new Queue<T>();

    public T Take() // Dequeue
    {
        lock (m_Queue)
        {
            while (m_Queue.Count <= 0)
            {
                Monitor.Wait(m_Queue);
            }
            return m_Queue.Dequeue();
        }
    }

    public void Add(T data) // Enqueue
    {
        lock (m_Queue)
        {
            m_Queue.Enqueue(data);
            Monitor.Pulse(m_Queue);
        }
    }
}

<强>更新

相当肯定如果您希望它对多个生产者和多个消费者来说是线程安全的,那么就不可能使用AutoResetEvent实现生产者 - 消费者队列(我是如果有人可以提出一个反例,那就准备证明是错的。当然,你会看到互联网上的例子,但他们都错了。实际上,one such attempt by Microsoft存在缺陷,因为队列可以获得live-locked