对队列任务使用锁是一种好习惯吗?
Is it a good practice to use locks for queuing tasks?
我需要将一些可以随时到达的请求放入队列中,这样每个任务仅在前一个任务结束时才开始。问题是,为此目的使用锁定是个好主意吗?它是否有任何不良影响,是否会导致我预期的排队行为?
更具体地说,考虑代码:
private int MyTask() {
...
}
private object someLock = new object();
public Task<int> DoMyTask() {
return Task.Run(() =>
{
lock (someLock)
{
return MyTask();
}
});
}
public void CallMyTask() {
var result = await DoMyTask();
}
请注意 CallMyTask()
将随时调用,可能同时调用。
我认为锁定几乎是您自己实现此目的的唯一方法,但如果您使用 blocking collection and a concurrent queue,.NET 框架应该能够为您完成此操作。 Blocking 集合为您提供了线程安全的 producer/consumer 模式的实现。
这是一个按顺序打印数字的例子。
class Program
{
private static BlockingCollection<Task> m_BlockingCollection = new BlockingCollection<Task>(new ConcurrentQueue<Task>());
private static int Counter;
static async Task Main(string[] args)
{
Task.Run(ProcessQueue); //Don't await for this demo!
Task.Run(AddStuffToQueue); //Don't await for this demo!
Console.ReadLine();
m_BlockingCollection.CompleteAdding();
while (!m_BlockingCollection.IsAddingCompleted)
Thread.Sleep(5);
}
private static void AddStuffToQueue()
{
while(true)
m_BlockingCollection.Add(new Task(() => Console.WriteLine(Interlocked.Increment(ref Counter))));
}
private static async Task ProcessQueue()
{
while (!m_BlockingCollection.IsCompleted && m_BlockingCollection.TryTake(out Task task))
ProcessTask(task);
}
private static void ProcessTask(Task task)
{
task.RunSynchronously();
}
}
这可能不是一个完美的例子,但我相信你明白了。 producer/consumer 包装并发队列,因此任务按先进先出 (FIFO) 执行。
阻塞集合可以有多个消费者,但如果您希望一次处理一个事物,那么只需要一个消费者就足够了。
希望对您有所帮助!
is it a good idea to use locking for this purpose? Does it have any bad effects, and do the queuing behavior that I expect result from this?
锁定在这里不是一个好的解决方案。不良影响是它会阻塞线程池线程 从工作在队列中的时间 直到工作完成。因此,如果您的代码排队 1000 个请求,它将调用 Task.Run
1000 次,并可能用完该数量的线程池线程,每个线程都在等待锁。
此外,锁并不是严格意义上的 FIFO。他们只是大多数-sorta-FIFO。这是因为严格 FIFO 锁会导致其他问题,例如锁车队; the links in this issue have some great discussion about lock "fairness" (i.e., FIFO behavior).
所以,我推荐一个真正的队列。您可以使用 TPL Dataflow 中的 ActionBlock<T>
作为真正的队列。由于您的请求有 个结果 ,您可以使用 TaskCompletionSource<T>
作为入队代码以获得结果。 TaskCompletionSource<T>
是一个 "asynchronous signal" - 在这种情况下,我们使用它来通知调用代码 他们的特定 请求已通过队列并已执行。
private ActionBlock<TaskCompletionSource<int>> queue =
new ActionBlock<TaskCompletionSource<int>>(tcs =>
{
try { tcs.TrySetResult(MyTask()); }
catch (Exception ex) { tcs.TrySetException(ex); }
});
每次我们发送一个TaskCompletionSource<T>
到这个queue
,它会运行MyTask()
并捕获结果(无论是成功还是异常),都会通过TaskCompletionSource<T>
.
的结果
然后我们可以这样使用它:
public Task<int> DoMyTask() {
var tcs = new TaskCompletionSource<int>();
queue.Post(tcs);
return tcs.Task;
}
public void CallMyTask() {
var result = await DoMyTask();
}
我需要将一些可以随时到达的请求放入队列中,这样每个任务仅在前一个任务结束时才开始。问题是,为此目的使用锁定是个好主意吗?它是否有任何不良影响,是否会导致我预期的排队行为?
更具体地说,考虑代码:
private int MyTask() {
...
}
private object someLock = new object();
public Task<int> DoMyTask() {
return Task.Run(() =>
{
lock (someLock)
{
return MyTask();
}
});
}
public void CallMyTask() {
var result = await DoMyTask();
}
请注意 CallMyTask()
将随时调用,可能同时调用。
我认为锁定几乎是您自己实现此目的的唯一方法,但如果您使用 blocking collection and a concurrent queue,.NET 框架应该能够为您完成此操作。 Blocking 集合为您提供了线程安全的 producer/consumer 模式的实现。
这是一个按顺序打印数字的例子。
class Program
{
private static BlockingCollection<Task> m_BlockingCollection = new BlockingCollection<Task>(new ConcurrentQueue<Task>());
private static int Counter;
static async Task Main(string[] args)
{
Task.Run(ProcessQueue); //Don't await for this demo!
Task.Run(AddStuffToQueue); //Don't await for this demo!
Console.ReadLine();
m_BlockingCollection.CompleteAdding();
while (!m_BlockingCollection.IsAddingCompleted)
Thread.Sleep(5);
}
private static void AddStuffToQueue()
{
while(true)
m_BlockingCollection.Add(new Task(() => Console.WriteLine(Interlocked.Increment(ref Counter))));
}
private static async Task ProcessQueue()
{
while (!m_BlockingCollection.IsCompleted && m_BlockingCollection.TryTake(out Task task))
ProcessTask(task);
}
private static void ProcessTask(Task task)
{
task.RunSynchronously();
}
}
这可能不是一个完美的例子,但我相信你明白了。 producer/consumer 包装并发队列,因此任务按先进先出 (FIFO) 执行。
阻塞集合可以有多个消费者,但如果您希望一次处理一个事物,那么只需要一个消费者就足够了。
希望对您有所帮助!
is it a good idea to use locking for this purpose? Does it have any bad effects, and do the queuing behavior that I expect result from this?
锁定在这里不是一个好的解决方案。不良影响是它会阻塞线程池线程 从工作在队列中的时间 直到工作完成。因此,如果您的代码排队 1000 个请求,它将调用 Task.Run
1000 次,并可能用完该数量的线程池线程,每个线程都在等待锁。
此外,锁并不是严格意义上的 FIFO。他们只是大多数-sorta-FIFO。这是因为严格 FIFO 锁会导致其他问题,例如锁车队; the links in this issue have some great discussion about lock "fairness" (i.e., FIFO behavior).
所以,我推荐一个真正的队列。您可以使用 TPL Dataflow 中的 ActionBlock<T>
作为真正的队列。由于您的请求有 个结果 ,您可以使用 TaskCompletionSource<T>
作为入队代码以获得结果。 TaskCompletionSource<T>
是一个 "asynchronous signal" - 在这种情况下,我们使用它来通知调用代码 他们的特定 请求已通过队列并已执行。
private ActionBlock<TaskCompletionSource<int>> queue =
new ActionBlock<TaskCompletionSource<int>>(tcs =>
{
try { tcs.TrySetResult(MyTask()); }
catch (Exception ex) { tcs.TrySetException(ex); }
});
每次我们发送一个TaskCompletionSource<T>
到这个queue
,它会运行MyTask()
并捕获结果(无论是成功还是异常),都会通过TaskCompletionSource<T>
.
然后我们可以这样使用它:
public Task<int> DoMyTask() {
var tcs = new TaskCompletionSource<int>();
queue.Post(tcs);
return tcs.Task;
}
public void CallMyTask() {
var result = await DoMyTask();
}