从同步上下文中调用异步方法

时间:2014-04-11 21:05:28

标签: c# .net asynchronous async-await

我在我的代码中通过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();
}

当你有一个允许事情并行发生的消息循环并且你有异步方法时,就有机会最小化延迟。基本上,我想在这个例子中完成的是最小化延迟和空闲时间。虽然我实际上不确定如何调用与从队列中到达的消息相关联的命令。

更具体地说,如果命令调用需要执行服务请求,那么调用中的延迟可能会用于获取下一条消息。类似的东西。我完全可以通过将事情排成队列并自己协调来完成这项工作,但我希望看到这项工作只需要一些异步/等待的东西。

2 个答案:

答案 0 :(得分:5)

  

虽然我可以让我的大部分方法都异步,但我觉得我对何时必须这样做有基本的了解。

从最低级别开始。听起来你已经有了一个开始,但如果你在最低级别寻找更多,那么经验法则是基于I / O的任何事情都应该async(例如{{1} }})。

然后是重复HttpClient感染的问题。您希望使用异步方法,因此可以使用async调用它们。所以该方法必须是await。所以它的所有来电者都必须使用async,因此它们也必须是await等。

  

如果我构建一个消息循环,我想如何控制并行度,那么我如何调用我的程序的第一个异步方法呢?

最容易让框架负责这个。例如,您可以从WebAPI操作返回async,框架可以理解。类似地,UI应用程序具有内置的消息循环,Task<T>将自然地使用。

如果您遇到框架不理解async或者有内置消息循环(通常是控制台应用程序或Win32服务)的情况,您可以使用AsyncContext type in my AsyncEx libraryTask只需在当前线程上安装一个“主循环”(与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(基于任务的异步)模式在您的代码中传播。这可能意味着相当多的重构(或重新设计)。理想情况下,您应该被允许将其分解为多个部分,并在努力实现使程序更加异步的总体目标时取得进展。

本身就很危险的事情是以同步方式调用异步代码。我们的意思是调用WaitResult方法。这些可能导致死锁。解决类似问题的一种方法是使用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任务。但是,如果我这样做并在任何任务上调用WaitResult,它将使程序死锁,因为它将阻止唯一可用的消息循环。