.NET中管理单独(单个)线程上任务队列的最佳方式

时间:2014-09-05 18:12:48

标签: c# .net asynchronous concurrency task-parallel-library

我知道异步编程多年来已经发生了很多变化。我有点尴尬,我让自己在34岁时就生气了,但我依靠StackOverflow让我加快速度。

我要做的是在一个单独的线程上管理“工作”队列,但是这样一次只能处理一个项目。我想在这个线程上发布工作,它不需要将任何内容传递给调用者。当然,我可以简单地启动一个新的Thread对象并让它在一个共享的Queue对象上循环,使用睡眠,中断,等待句柄等等。但是我知道事情从那以后变得更好。我们有BlockingCollectionTaskasync / await,更不用说可能会抽象出很多内容的NuGet包。

我知道“什么是最好的...”这些问题通常是不受欢迎的,所以我将通过说“使用内置的.NET机制来完成类似的事情的方式”来改写它优选。但是如果第三方NuGet包简化了一堆东西,它也是如此。

我认为TaskScheduler实例的最大并发数固定为1,但到目前为止,似乎可能没有那么笨重的方法了。

背景

具体来说,我在这种情况下尝试做的是在Web请求期间排队IP地理定位任务。相同的IP可能会多次排队等待地理位置,但是任务将知道如何检测到它并且如果已经解决则提前跳过。但是请求处理程序只是将这些() => LocateAddress(context.Request.UserHostAddress)调用放入队列中,让LocateAddress方法处理重复的工作检测。我正在使用的地理位置API不喜欢被请求轰炸,这就是我想一次将它限制为单个并发任务的原因。但是,如果允许通过简单的参数更改轻松扩展到更多并发任务,那将是很好的。

6 个答案:

答案 0 :(得分:42)

要创建异步单度并行工作队列,您只需创建一个SemaphoreSlim,初始化为一个,然后在启动请求之前获取该信号量时使用enqueing方法await工作

public class TaskQueue
{
    private SemaphoreSlim semaphore;
    public TaskQueue()
    {
        semaphore = new SemaphoreSlim(1);
    }

    public async Task<T> Enqueue<T>(Func<Task<T>> taskGenerator)
    {
        await semaphore.WaitAsync();
        try
        {
            return await taskGenerator();
        }
        finally
        {
            semaphore.Release();
        }
    }
    public async Task Enqueue(Func<Task> taskGenerator)
    {
        await semaphore.WaitAsync();
        try
        {
            await taskGenerator();
        }
        finally
        {
            semaphore.Release();
        }
    }
}

当然,要有一个固定的并行度而不是简单地将信号量初始化为其他数字。

答案 1 :(得分:16)

我认为最佳选择是使用TPL Dataflow&#39; ActionBlock

var actionBlock = new ActionBlock<string>(address =>
{
    if (!IsDuplicate(address))
    {
        LocateAddress(address);
    }
});

actionBlock.Post(context.Request.UserHostAddress);

TPL Dataflow是健壮的,线程安全的,async - 准备好并且非常可配置的基于actor的框架(可用作nuget)

这是一个更复杂案例的简单例子。我们假设你想:

  • 启用并发(仅限于可用内核)。
  • 限制队列大小(这样你就不会耗尽内存)。
  • 同时LocateAddress和队列插入async
  • 一小时后取消所有内容。

var actionBlock = new ActionBlock<string>(async address =>
{
    if (!IsDuplicate(address))
    {
        await LocateAddressAsync(address);
    }
}, new ExecutionDataflowBlockOptions
{
    BoundedCapacity = 10000,
    MaxDegreeOfParallelism = Environment.ProcessorCount,
    CancellationToken = new CancellationTokenSource(TimeSpan.FromHours(1)).Token
});

await actionBlock.SendAsync(context.Request.UserHostAddress);

答案 2 :(得分:11)

实际上,您不需要在一个线程中运行任务,您需要它们串行运行(一个接一个)和FIFO。 TPL并没有为此提供类,但这是我非常轻量级,无阻塞的测试实现。 https://github.com/Gentlee/SerialQueue

在那里也有@Servy实现,测试表明它比我的慢两倍并且它不能保证FIFO。

示例:

private readonly SerialQueue queue = new SerialQueue();

async Task SomeAsyncMethod()
{
    var result = await queue.Enqueue(DoSomething);
}

答案 3 :(得分:5)

使用BlockingCollection<Action>创建一个生产者/消费者模式,其中包含一个消费者(一次只能运行一件事)和一个或多个生产者。

首先在某处定义共享队列:

BlockingCollection<Action> queue = new BlockingCollection<Action>();

在您的消费者ThreadTask中,您可以从中获取:

//This will block until there's an item available
Action itemToRun = queue.Take()

然后从其他线程上的任意数量的生成器,只需添加到队列:

queue.Add(() => LocateAddress(context.Request.UserHostAddress));

答案 4 :(得分:4)

我在这里发布了另一种解决方案。老实说,我不确定这是否是一个好的解决方案。

我习惯于使用BlockingCollection来实现生产者/消费者模式,并使用专用线程来消耗这些项目。如果总是有数据传入,并且使用者线程不会坐在那里什么也不做,那就很好了。

我遇到了一种情况,其中一个应用程序想在另一个线程上发送电子邮件,但是电子邮件总数并不那么大。 我最初的解决方案是拥有一个专用的使用者线程(由Task.Run()创建),但是很多时候它只是坐在那里,什么也不做。

旧解决方案:

private readonly BlockingCollection<EmailData> _Emails =
    new BlockingCollection<EmailData>(new ConcurrentQueue<EmailData>());

// producer can add data here
public void Add(EmailData emailData)
{
    _Emails.Add(emailData);
}

public void Run()
{
    // create a consumer thread
    Task.Run(() => 
    {
        foreach (var emailData in _Emails.GetConsumingEnumerable())
        {
            SendEmail(emailData);
        }
    });
}

// sending email implementation
private void SendEmail(EmailData emailData)
{
    throw new NotImplementedException();
}

如您所见,如果没有足够的电子邮件要发送(这是我的情况),那么消费者线程将把大部分电子邮件都花在那儿,而什么也不做。

我将实现更改为:

// create an empty task
private Task _SendEmailTask = Task.Run(() => {});

// caller will dispatch the email to here
// continuewith will use a thread pool thread (different to
// _SendEmailTask thread) to send this email
private void Add(EmailData emailData)
{
    _SendEmailTask = _SendEmailTask.ContinueWith((t) =>
    {
        SendEmail(emailData);
    });
}

// actual implementation
private void SendEmail(EmailData emailData)
{
    throw new NotImplementedException();
}

它不再是生产者/消费者模式,但是那里没有线程并且什么也不做,相反,每次发送电子邮件时,它将使用线程池线程来完成。

答案 5 :(得分:0)

我的图书馆,它可以:

  1. 在队列列表中随机运行
  2. 多队列
  3. 先运行优先级
  4. 重新排队
  5. 事件所有队列已完成
  6. 取消运行或取消等待运行
  7. 将事件分配给UI线程

public interface IQueue
  {
    bool IsPrioritize { get; }
    bool ReQueue { get; }
    /// <summary>
    /// Dont use async
    /// </summary>
    /// <returns></returns>
    Task DoWork();
    bool CheckEquals(IQueue queue);
    void Cancel();
  }

  public delegate void QueueComplete<T>(T queue) where T : IQueue;
  public delegate void RunComplete();
  public class TaskQueue<T> where T : IQueue
  {
    readonly List<T> Queues = new List<T>();
    readonly List<T> Runnings = new List<T>();

    [Browsable(false), DefaultValue((string)null)]
    public Dispatcher Dispatcher { get; set; }
    public event RunComplete OnRunComplete;
    public event QueueComplete<T> OnQueueComplete;
    int _MaxRun = 1;
    public int MaxRun
    {
      get { return _MaxRun; }
      set
      {
        bool flag = value > _MaxRun;
        _MaxRun = value;
        if (flag && Queues.Count != 0) RunNewQueue();
      }
    }
    public int RunningCount
    {
      get { return Runnings.Count; }
    }
    public int QueueCount
    {
      get { return Queues.Count; }
    }

    public bool RunRandom { get; set; } = false;

    //need lock Queues first
    void StartQueue(T queue)
    {
      if (null != queue)
      {
        Queues.Remove(queue);
        lock (Runnings) Runnings.Add(queue);
        queue.DoWork().ContinueWith(ContinueTaskResult, queue);
      }
    }

    void RunNewQueue()
    {
      lock (Queues)//Prioritize
      {
        foreach (var q in Queues.Where(x => x.IsPrioritize)) StartQueue(q);
      }

      if (Runnings.Count >= MaxRun) return;//other
      else if (Queues.Count == 0)
      {
        if (Runnings.Count == 0 && OnRunComplete != null)
        {
          if (Dispatcher != null && !Dispatcher.CheckAccess()) Dispatcher.Invoke(OnRunComplete);
          else OnRunComplete.Invoke();//on completed
        }
        else return;
      }
      else
      {
        lock (Queues)
        {
          T queue;
          if (RunRandom) queue = Queues.OrderBy(x => Guid.NewGuid()).FirstOrDefault();
          else queue = Queues.FirstOrDefault();
          StartQueue(queue);
        }
        if (Queues.Count > 0 && Runnings.Count < MaxRun) RunNewQueue();
      }
    }

    void ContinueTaskResult(Task Result, object queue_obj) => QueueCompleted((T)queue_obj);

    void QueueCompleted(T queue)
    {
      lock (Runnings) Runnings.Remove(queue);
      if (queue.ReQueue) lock (Queues) Queues.Add(queue);
      if (OnQueueComplete != null)
      {
        if (Dispatcher != null && !Dispatcher.CheckAccess()) Dispatcher.Invoke(OnQueueComplete, queue);
        else OnQueueComplete.Invoke(queue);
      }
      RunNewQueue();
    }

    public void Add(T queue)
    {
      if (null == queue) throw new ArgumentNullException(nameof(queue));
      lock (Queues) Queues.Add(queue);
      RunNewQueue();
    }

    public void Cancel(T queue)
    {
      if (null == queue) throw new ArgumentNullException(nameof(queue));
      lock (Queues) Queues.RemoveAll(o => o.CheckEquals(queue));
      lock (Runnings) Runnings.ForEach(o => { if (o.CheckEquals(queue)) o.Cancel(); });
    }

    public void Reset(T queue)
    {
      if (null == queue) throw new ArgumentNullException(nameof(queue));
      Cancel(queue);
      Add(queue);
    }

    public void ShutDown()
    {
      MaxRun = 0;
      lock (Queues) Queues.Clear();
      lock (Runnings) Runnings.ForEach(o => o.Cancel());
    }
  }