我知道异步编程多年来已经发生了很多变化。我有点尴尬,我让自己在34岁时就生气了,但我依靠StackOverflow让我加快速度。
我要做的是在一个单独的线程上管理“工作”队列,但是这样一次只能处理一个项目。我想在这个线程上发布工作,它不需要将任何内容传递给调用者。当然,我可以简单地启动一个新的Thread
对象并让它在一个共享的Queue
对象上循环,使用睡眠,中断,等待句柄等等。但是我知道事情从那以后变得更好。我们有BlockingCollection
,Task
,async
/ await
,更不用说可能会抽象出很多内容的NuGet包。
我知道“什么是最好的...”这些问题通常是不受欢迎的,所以我将通过说“使用内置的.NET机制来完成类似的事情的方式”来改写它优选。但是如果第三方NuGet包简化了一堆东西,它也是如此。
我认为TaskScheduler
实例的最大并发数固定为1,但到目前为止,似乎可能没有那么笨重的方法了。
背景
具体来说,我在这种情况下尝试做的是在Web请求期间排队IP地理定位任务。相同的IP可能会多次排队等待地理位置,但是任务将知道如何检测到它并且如果已经解决则提前跳过。但是请求处理程序只是将这些() => LocateAddress(context.Request.UserHostAddress)
调用放入队列中,让LocateAddress
方法处理重复的工作检测。我正在使用的地理位置API不喜欢被请求轰炸,这就是我想一次将它限制为单个并发任务的原因。但是,如果允许通过简单的参数更改轻松扩展到更多并发任务,那将是很好的。
答案 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>();
在您的消费者Thread
或Task
中,您可以从中获取:
//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)
我的图书馆,它可以:
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());
}
}