我在我的代码中通过HTTP调用服务(最终使用HttpClient.SendAsync方法)。然后从WebAPI控制器操作调用此代码。大多数情况下,它工作正常(测试通过),但是当我在IIS上部署时,我遇到了死锁,因为异步方法调用的调用者已被阻止,并且继续无法在该线程上继续直到它完成(它赢得了#39;吨)。
虽然我可以让我的大多数方法都不同步,但我并不觉得我对我何时必须这样做有基本的了解。
例如,让我说我的大多数方法都是异步的(因为它们最终调用其他异步服务方法)如果我构建了一个消息循环,我将如何调用我的程序的第一个异步方法我想要控制并行度?
由于HttpClient没有任何同步方法,如果我有一个不是async
知道的抽象,我可以安全地假设做什么?我已经了解了ConfigureAwait(false)
,但我并不了解它的作用。在异步调用之后设置它是很奇怪的。对我来说,感觉就像一场等待发生的比赛......无论多么不可能......
WebAPI示例:
public HttpResponseMessage Get()
{
var userContext = contextService.GetUserContext(); // <-- synchronous
return ...
}
// Some IUserContextService implementation
public IUserContext GetUserContext()
{
var httpClient = new HttpClient();
var result = httpClient.GetAsync(...).Result; // <-- I really don't care if this is asynchronous or not
return new HttpUserContext(result);
}
消息循环示例:
var mq = new MessageQueue();
// we then run say 8 tasks that do this
for (;;)
{
var m = mq.Get();
var c = GetCommand(m);
c.InvokeAsync().Wait();
m.Delete();
}
当你有一个允许事情并行发生的消息循环并且你有异步方法时,就有机会最小化延迟。基本上,我想在这个例子中完成的是最小化延迟和空闲时间。虽然我实际上不确定如何调用与从队列中到达的消息相关联的命令。
更具体地说,如果命令调用需要执行服务请求,那么调用中的延迟可能会用于获取下一条消息。类似的东西。我完全可以通过将事情排成队列并自己协调来完成这项工作,但我希望看到这项工作只需要一些异步/等待的东西。
答案 0 :(得分:5)
虽然我可以让我的大部分方法都异步,但我觉得我对何时必须这样做有基本的了解。
从最低级别开始。听起来你已经有了一个开始,但如果你在最低级别寻找更多,那么经验法则是基于I / O的任何事情都应该async
(例如{{1} }})。
然后是重复HttpClient
感染的问题。您希望使用异步方法,因此可以使用async
调用它们。所以该方法必须是await
。所以它的所有来电者都必须使用async
,因此它们也必须是await
等。
如果我构建一个消息循环,我想如何控制并行度,那么我如何调用我的程序的第一个异步方法呢?
最容易让框架负责这个。例如,您可以从WebAPI操作返回async
,框架可以理解。类似地,UI应用程序具有内置的消息循环,Task<T>
将自然地使用。
如果您遇到框架不理解async
或者有内置消息循环(通常是控制台应用程序或Win32服务)的情况,您可以使用AsyncContext
type in my AsyncEx
library。 Task
只需在当前线程上安装一个“主循环”(与AsyncContext
兼容)。
由于HttpClient没有任何同步方法,如果我有一个不能同步识别的抽象,我可以安全地假设做什么?
正确的方法是改变抽象。不要试图阻止异步代码;我在博客上详细描述了common deadlock scenario。
通过使其成为async
友好来改变抽象。例如,将async
更改为IUserContext IUserContextService.GetUserContext()
。
我已经读过有关ConfigureAwait(false)但我真的不明白它的作用。我很奇怪它是在异步调用之后设置的。
您可能会发现我的async
intro有帮助。我不会在这个答案中多说Task<IUserContext> IUserContextService.GetUserContextAsync()
,因为我认为这不能直接适用于这个问题的一个好的解决方案(但我不是说它很糟糕;它实际上应该除非无法使用它,否则请使用它。)
请记住,ConfigureAwait
是一个具有优先级规则的运算符。起初感觉很神奇,但实际上并没有那么多。这段代码:
async
与此代码完全相同:
var result = await httpClient.GetAsync(url).ConfigureAwait(false);
var asyncOperation = httpClient.GetAsync(url).ConfigureAwait(false);
var result = await asyncOperation;
代码中通常没有竞争条件,因为 - 即使该方法是异步 - 它也是顺序。该方法可以在async
暂停,并且在await
完成后才会恢复。
当你有一个允许并行发生的消息循环并且你有异步方法时,就有机会最小化延迟。
这是你第二次提到“并行”的“消息循环”,但我认为你真正想要的是让多个(异步)消费者在同一个队列中工作,对吗?这对于await
来说很容易(请注意,在这个示例中,单个线程上只有一个消息循环;当所有内容都是异步时,通常只需要这些):
async
你可能也对TPL Dataflow感兴趣。 Dataflow是一个理解并与await tasks.WhenAll(ConsumerAsync(), ConsumerAsync(), ConsumerAsync());
async Task ConsumerAsync()
{
for (;;) // TODO: consider a CancellationToken for orderly shutdown
{
var m = await mq.ReceiveAsync();
var c = GetCommand(m);
await c.InvokeAsync();
m.Delete();
}
}
// Extension method
public static Task<Message> ReceiveAsync(this MessageQueue mq)
{
return Task<Message>.Factory.FromAsync(mq.BeginReceive, mq.EndReceive, null);
}
代码配合良好的库,并且内置了很好的并行选项。
答案 1 :(得分:1)
虽然我很欣赏社区成员的洞察力,但我很难表达我想要做的事情的意图,但却非常有助于获得有关问题的建议。有了这个,我最终得到了以下代码。
public class AsyncOperatingContext
{
struct Continuation
{
private readonly SendOrPostCallback d;
private readonly object state;
public Continuation(SendOrPostCallback d, object state)
{
this.d = d;
this.state = state;
}
public void Run()
{
d(state);
}
}
class BlockingSynchronizationContext : SynchronizationContext
{
readonly BlockingCollection<Continuation> _workQueue;
public BlockingSynchronizationContext(BlockingCollection<Continuation> workQueue)
{
_workQueue = workQueue;
}
public override void Post(SendOrPostCallback d, object state)
{
_workQueue.TryAdd(new Continuation(d, state));
}
}
/// <summary>
/// Gets the recommended max degree of parallelism. (Your main program message loop could use this value.)
/// </summary>
public static int MaxDegreeOfParallelism { get { return Environment.ProcessorCount; } }
#region Helper methods
/// <summary>
/// Run an async task. This method will block execution (and use the calling thread as a worker thread) until the async task has completed.
/// </summary>
public static T Run<T>(Func<Task<T>> main, int degreeOfParallelism = 1)
{
var asyncOperatingContext = new AsyncOperatingContext();
asyncOperatingContext.DegreeOfParallelism = degreeOfParallelism;
return asyncOperatingContext.RunMain(main);
}
/// <summary>
/// Run an async task. This method will block execution (and use the calling thread as a worker thread) until the async task has completed.
/// </summary>
public static void Run(Func<Task> main, int degreeOfParallelism = 1)
{
var asyncOperatingContext = new AsyncOperatingContext();
asyncOperatingContext.DegreeOfParallelism = degreeOfParallelism;
asyncOperatingContext.RunMain(main);
}
#endregion
private readonly BlockingCollection<Continuation> _workQueue;
public int DegreeOfParallelism { get; set; }
public AsyncOperatingContext()
{
_workQueue = new BlockingCollection<Continuation>();
}
/// <summary>
/// Initialize the current thread's SynchronizationContext so that work is scheduled to run through this AsyncOperatingContext.
/// </summary>
protected void InitializeSynchronizationContext()
{
SynchronizationContext.SetSynchronizationContext(new BlockingSynchronizationContext(_workQueue));
}
protected void RunMessageLoop()
{
while (!_workQueue.IsCompleted)
{
Continuation continuation;
if (_workQueue.TryTake(out continuation, Timeout.Infinite))
{
continuation.Run();
}
}
}
protected T RunMain<T>(Func<Task<T>> main)
{
var degreeOfParallelism = DegreeOfParallelism;
if (!((1 <= degreeOfParallelism) & (degreeOfParallelism <= 5000))) // sanity check
{
throw new ArgumentOutOfRangeException("DegreeOfParallelism must be between 1 and 5000.", "DegreeOfParallelism");
}
var currentSynchronizationContext = SynchronizationContext.Current;
InitializeSynchronizationContext(); // must set SynchronizationContext before main() task is scheduled
var mainTask = main(); // schedule "main" task
mainTask.ContinueWith(task => _workQueue.CompleteAdding());
// for single threading we don't need worker threads so we don't use any
// otherwise (for increased parallelism) we simply launch X worker threads
if (degreeOfParallelism > 1)
{
for (int i = 1; i < degreeOfParallelism; i++)
{
ThreadPool.QueueUserWorkItem(_ => {
// do we really need to restore the SynchronizationContext here as well?
InitializeSynchronizationContext();
RunMessageLoop();
});
}
}
RunMessageLoop();
SynchronizationContext.SetSynchronizationContext(currentSynchronizationContext); // restore
return mainTask.Result;
}
protected void RunMain(Func<Task> main)
{
// The return value doesn't matter here
RunMain(async () => { await main(); return 0; });
}
}
这个课程已经完成,它做了一些我发现很难掌握的事情。
作为一般建议,您应该允许TAP(基于任务的异步)模式在您的代码中传播。这可能意味着相当多的重构(或重新设计)。理想情况下,您应该被允许将其分解为多个部分,并在努力实现使程序更加异步的总体目标时取得进展。
本身就很危险的事情是以同步方式调用异步代码。我们的意思是调用Wait
或Result
方法。这些可能导致死锁。解决类似问题的一种方法是使用AsyncOperatingContext.Run
方法。它将使用当前线程运行消息循环,直到异步调用完成。它会暂时替换与当前线程关联的SynchronizationContext
。
注意:我不知道这是否足够,或者如果您允许以这种方式交换
SynchronizationContext
,假设您可以,这应该可行。我已经被ASP.NET死锁问题所困扰,这可能会起到解决方法的作用。
最后,我发现自己在询问Main(string[])
背景下async
的相应等价物是什么?结果证明这是消息循环。
我发现有两件事可以解决这个async
机制。
SynchronizationContext.Post
和消息循环。在我的AsyncOperatingContext
中,我提供了一个非常简单的消息循环:
protected void RunMessageLoop()
{
while (!_workQueue.IsCompleted)
{
Continuation continuation;
if (_workQueue.TryTake(out continuation, Timeout.Infinite))
{
continuation.Run();
}
}
}
我的SynchronizationContext.Post
因此变为:
public override void Post(SendOrPostCallback d, object state)
{
_workQueue.TryAdd(new Continuation(d, state));
}
我们的入口点,基本上相当于同步上下文中的async
main(来自原始源的简化版本):
SynchronizationContext.SetSynchronizationContext(new BlockingSynchronizationContext(_workQueue));
var mainTask = main(); // schedule "main" task
mainTask.ContinueWith(task => _workQueue.CompleteAdding());
RunMessageLoop();
return mainTask.Result;
所有这些都是代价高昂的,我们不能仅仅用这个替换对async
方法的调用,但它确实允许我们快速创建所需的工具,以便在需要的地方继续编写async
代码必须处理整个计划。从这个实现中也很清楚工作线程的位置以及程序的影响并发性。
我看着这个,想一想,这就是Node.js的作用。虽然JavaScript没有C#当前所做的那种漂亮的async / await语言支持。
作为一个额外的好处,我可以完全控制并行度,如果我想,我可以完全单线程运行我的async
任务。但是,如果我这样做并在任何任务上调用Wait
或Result
,它将使程序死锁,因为它将阻止唯一可用的消息循环。