串行任务执行器;这个线程安全吗?

时间:2010-09-08 18:42:31

标签: c# multithreading synchronization thread-safety task

我创建了一个类,我使用ThreadPool作为执行手段,允许异步顺序执行任务。我的想法是,我将在后台运行串行任务的多个实例,但我不希望每个实例都有一个单独的专用线程。我想检查的是这个类是否真的是线程安全的。这是相当简短的,所以我想我会由专家在这里运行它,以防我遗漏了一些明显的东西。我省略了一些针对不同Action类型的一些方便重载。

/// <summary>
/// This class wraps ThreadPool.QueueUserWorkItem, but providing guaranteed ordering of queued tasks for this instance.
/// Only one task in the queue will execute at a time, with the order of execution matching the order of addition.
/// This is designed as a lighter-weight alternative to using a dedicated Thread for processing of sequential tasks.
/// </summary>
public sealed class SerialAsyncTasker
{
    private readonly Queue<Action> mTasks = new Queue<Action>();
    private bool mTaskExecuting;

    /// <summary>
    /// Queue a new task for asynchronous execution on the thread pool.
    /// </summary>
    /// <param name="task">Task to execute</param>
    public void QueueTask(Action task)
    {
        if (task == null) throw new ArgumentNullException("task");

        lock (mTasks)
        {
            bool isFirstTask = (mTasks.Count == 0);
            mTasks.Enqueue(task);

            //Only start executing the task if this is the first task
            //Additional tasks will be executed normally as part of sequencing
            if (isFirstTask && !mTaskExecuting)
                RunNextTask();
        }
    }

    /// <summary>
    /// Clear all queued tasks.  Any task currently executing will continue to execute.
    /// </summary>
    public void Clear()
    {
        lock (mTasks)
        {
            mTasks.Clear();
        }
    }

    /// <summary>
    /// Wait until all currently queued tasks have completed executing.
    /// If no tasks are queued, this method will return immediately.
    /// This method does not prevent the race condition of a second thread 
    /// queueing a task while one thread is entering the wait;
    /// if this is required, it must be synchronized externally.
    /// </summary>
    public void WaitUntilAllComplete()
    {
        lock (mTasks)
        {
            while (mTasks.Count > 0 || mTaskExecuting)
                Monitor.Wait(mTasks);
        }
    }

    private void RunTask(Object state)
    {
        var task = (Action)state;
        task();
        mTaskExecuting = false;
        RunNextTask();
    }

    private void RunNextTask()
    {
        lock (mTasks)
        {
            if (mTasks.Count > 0)
            {
                mTaskExecuting = true;
                var task = mTasks.Dequeue();
                ThreadPool.QueueUserWorkItem(RunTask, task);
            }
            else
            {
                //If anybody is waiting for tasks to be complete, let them know
                Monitor.PulseAll(mTasks);
            }
        }
    }
}

更新:我修改了代码以修复Simon所指出的主要错误。现在通过单元测试,但我仍然欢迎观察。

5 个答案:

答案 0 :(得分:2)

不要这样做。 (或者至少避免建立自己的东西。)

使用System.Threading.Tasks内容(.NET 4.0中的新增功能)。创建一个Task [](大小取决于您想要的并行任务数),让他们在等待BlockingCollection时从CancellationToken读取工作项。您的WaitForAll实施会触发您的令牌,并致电Task.WaitAll(Task[]),这将阻止所有任务完成。

答案 1 :(得分:1)

这是我的第二个答案,假设您无法使用.NET 4.0(并希望对现有代码进行评论)。

QueueTask将第一个任务排入队列,获取isFirstTask = true,然后启动一个新线程。但是,另一个线程可能会在第一个线程正在处理时排队,而Count == 0 =&gt; isFirstTask = true,然后生成另一个线程。

此外,如果任务执行抛出异常(可能不一定会使所有内容崩溃,具体取决于异常处理),WaitUntilAllComplete将无限期挂起,从而导致它跳过对RunNextTask()的调用。

你的WaitUntilAllComplete只是等待,直到没有更多的排队任务,而不是当前正在执行的任务正在执行(它们可能只是在ThreadPool中排队)或完成。

答案 2 :(得分:1)

它内置于4.0

How to: Create a Task Scheduler That Limits the Degree of Concurrency

您还可以使用自定义调度程序来实现默认调度程序不提供的功能,例如严格的先进先出(FIFO)执行顺序。以下示例演示如何创建自定义任务计划程序。此调度程序允许您指定并发度。

答案 3 :(得分:0)

我在您的SerialAsyncTasker课程中看到了一些问题,但听起来您可能已经很好地掌握了这些问题,因此我不会详细介绍该主题(我可能会更详细地编辑我的答案)后来)。您在评论中指出您不能使用.NET 4.0功能,也不能使用Reactive Extensions backport。我建议您在专用线程上使用生产者 - 消费者模式和单个消费者。这完全符合您按顺序异步执行任务的要求。

注意:您必须强化代码以支持正常关闭,处理异常等。

public class SerialAsyncTasker
{
  private BlockingCollection<Action> m_Queue = new BlockingCollection<Action>();

  public SerialAsyncTasker()
  {
    var thread = new Thread(
      () =>
      {
        while (true)
        {
          Action task = m_Queue.Take();
          task();
        }
      });
    thread.IsBackground = true;
    thread.Start();
  }

  public void QueueTask(Action task)
  {
    m_Queue.Add(task);
  }
}

太糟糕了,您无法使用.NET 4.0 BCL或Reactive Extension下载中的BlockingCollection,但不用担心。实际上自己实现它并不太难。您可以使用Stephen Toub's blocking queue作为起点,只需重命名一些内容。

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

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

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

答案 4 :(得分:0)

public class ParallelExcecuter
{
    private readonly BlockingCollection<Task> _workItemHolder;

    public ParallelExcecuter(int maxDegreeOfParallelism)
    {
        _workItemHolder = new BlockingCollection<Task>(maxDegreeOfParallelism);
    }

    public void Submit(Action action)
    {
        _workItemHolder.Add(Task.Run(action).ContinueWith(t =>
        {
            _workItemHolder.Take();
        }));

    }

    public void WaitUntilWorkDone()
    {
        while (_workItemHolder.Count < 0)
        {
            Monitor.Wait(_workItemHolder);
        }
    }
}