调用从同步上下文异步方法 [英] Calling async methods from a synchronous context

查看:427
本文介绍了调用从同步上下文异步方法的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我打电话通过HTTP服务(最终使用HttpClient.SendAsync方法)在我的code。这code,然后叫成从的WebAPI控制器动作。大多数情况下,它工作正常(测试通过),但是当我部署在说IIS,我经历了僵局,因为异步方法调用的调用者已被封锁,并继续将无法继续在该线程,直到它完成(它不会)

I'm calling a service over HTTP (ultimately using the HttpClient.SendAsync method) from within my code. This code is then called into from a WebAPI controller action. Mostly, it works fine (tests pass) but then when I deploy on say IIS, I experience a deadlock because caller of the async method call has been blocked and the continuation cannot proceed on that thread until it finishes (which it won't).

虽然我可以让我的大多数方法异步,我不觉得我有,当我必须这样做,有一个基本的了解。

While I could make most of my methods async I don't feel as if I have a basic understanding of when I'd must do this.

例如,假设我没有让我的大多数方法异步(因为他们最终调用其它异步服务方法),我怎么会那么调用我的程序的第一个异步方法,如果我建说,我需要一些控制消息循环的并行程度?

For example, let's say I did make most of my methods async (since they ultimately call other async service methods) how would I then invoke the first async method of my program if I built say a message loop where I want some control of the degree of parallelism?

由于HttpClient的没有任何同步的方法,我能放心地presume做的,如果我有一个抽象的概念,是不是异步知道?我读过关于 ConfigureAwait(假),但我真的不明白它做什么。很奇怪,我认为它的异步调用后设置。对我来说,感觉就好像一场比赛即将发生......但不太可能......

Since the HttpClient doesn't have any synchronous methods, what can I safely presume to do if I have an abstraction that isn't async aware? I've read about the ConfigureAwait(false) but I don't really understand what it does. It's strange to me that it's set after the async invocation. To me that feels as if a race waiting to happen... however unlikely...

的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);
}

消息循环,例如:

Message loop example:

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();
}

当你有一个消息循环,让事情发生在平行的,你有异步方法,有一个机会,以减少等待时间。基本上,我要完成在这种情况下是最大限度地减少延迟和空闲时间。虽然我其实不能确定如何调用成同到达从队列的消息相关的命令。

When you have a message loop that allow things to happen in parallel and you have asynchronous methods, there's a opportunity to minimize latency. Basically, what I want to accomplish in this instance is to minimize latency and idle time. Though I'm actually unsure as to how to invoke into the command that's associated with the message that arrives off the queue.

具体而言,如果在命令调用需要做的服务请求,那里将成为可用于获得下一个消息调用延时。像这样的东西。我可以结束了东西,队列和协调这个自己完全做到这一点简单的,但我希望看到这项工作只是一些异步/等待的东西。

To be more specific, if the command invocation needs to do service requests there's going to be latency in the invocation that could be used to get the next message. Stuff like that. I can totally do this simply by wrapping up things in queues and coordinating this myself but I'd like to see this work with just some async/await stuff.

推荐答案

虽然我AP preciate来自社区成员的洞察力,它总是很难EX preSS的我想要做的,但巨大的意图乐于助人,向他咨询关于围绕这一问题的情况。就这样,我终究抵下列code。

While I appreciate the insight from community members it's always difficult to express the intent of what I'm trying to do but tremendously helpful to get advice about circumstances surrounding the problem. With that, I eventually arrived that the following code.

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; });
  }
}

这个类是完整的,它做了几件事情,我发现很难把握。

This class is complete and it does a couple of things that I found difficult to grasp.

作为一般建议,应允许TAP(任务型异步)模式通过你的code传播。这可能意味着相当多的重构(或重新设计)。理想情况下,你应该被允许打破这种成片,并取得进展,你对着使你的程序更加异步的总体目标而努力。

As general advice you should allow the TAP (task-based asynchronous) pattern to propagate through your code. This may imply quite a bit of refactoring (or redesign). Ideally you should be allowed to break this up into pieces and make progress as you work towards to overall goal of making your program more asynchronous.

东西是内在的危险做的是异步调用code无情地以同步的方式。通过这一点,我们的意思是调用等待结果的方法。这可能会导致死锁。要解决类似的东西的一种方法是使用 AsyncOperatingContext.Run 方法。它将使用当前线程运行一个消息循环,直到异步调用完成。它会换出任何的SynchronizationContext 与当前线程关联暂时这样做。

Something that's inherently dangerous to do is to call asynchronous code callously in an synchronous fashion. By this we mean invoking the Wait or Result methods. These can lead to deadlocks. One way to work around something like that is to use the AsyncOperatingContext.Run method. It will use the current thread to run a message loop until the asynchronous call is complete. It will swap out whatever SynchronizationContext is associated with the current thread temporarily to do so.

注意:我不知道这是不够的,如果你被允许调换回的SynchronizationContext 这样一来,假设可以的话,这应该工作。我已经被咬伤的ASP.NET死锁问题,这可能可能功能作为一种解决办法。

Note: I don't know if this is enough, or if you are allowed to swap back the SynchronizationContext this way, assuming that you can, this should work. I've already been bitten by the ASP.NET deadlock issue and this could possibly function as a workaround.

最后,我发现自己问的问题,什么是主(字符串[])异步背景?原来,这就是消息循环。

Lastly, I found myself asking the question, what is the corresponding equivalent of Main(string[]) in an async context? Turns out that's the message loop.

我所发现的是,有两件事情,使这项异步机械。

What I've found is that there are two things that make out this async machinery.

SynchronizationContext.Post 消息循环。在我的 AsyncOperatingContext 我提供了一个非常简单的消息循环:

SynchronizationContext.Post and the message loop. In my AsyncOperatingContext I provide a very simple message loop:

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));
}

和我们的切入点,一个异步主要从基本同步上下文(从原始来源的简化版本),相当于:

And our entry point, basically the equivalent of an async main from synchronous context (simplified version from original source):

SynchronizationContext.SetSynchronizationContext(new BlockingSynchronizationContext(_workQueue));
var mainTask = main(); // schedule "main" task
mainTask.ContinueWith(task => _workQueue.CompleteAdding());
RunMessageLoop();
return mainTask.Result;

这一切都是昂贵的,我们不能只是去替换调用异步方法与此,但它确实让我们相当快速创建,保持写作所需的设施异步 $ C $在需要的地方,而无需处理整个程序下进行。这也由此实现,其中的工作线程去非常明确的程序如何影响并发性。

All of this is costly and we can't just go replace calls to async methods with this but it does allow us to rather quickly create the facilities required to keep writing async code where needed without having to deal with the whole program. It's also very clear from this implementation where the worker threads go and how the impact concurrency of your program.

我看这一点,并认为对自己,叶氏,这就是Node.js的是怎么做的。虽然JavaScript没有这个漂亮的异步/等待语言支持C#现在一样。

I look at this and think to myself, yeap, that's how Node.js does it. Though JavaScript does not have this nice async/await language support that C# currently does.

作为额外的奖励,我有并行度的完全控制,如果我愿意,我可以运行我的异步任务完全单线程的。不过,如果我这样做,并呼吁等待结果的任何任务,它就会死锁的程序,因为它会阻止唯一的消息循环使用。

As an added bonus, I have complete control of the degree of parallelism, and if I want, I can run my async tasks completely single threaded. Though, If I do so and call Wait or Result on any task, it will deadlock the program because it will block the only message loop available.

这篇关于调用从同步上下文异步方法的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆