【问题标题】:How to ensure order of async operation calls?如何确保异步操作调用的顺序?
【发布时间】:2009-11-09 05:51:34
【问题描述】:

[这似乎是一个冗长的问题,但我已尽力使其尽可能清楚。请耐心等待并帮助我...]

我编写了一个支持异步操作的测试类。该操作只报告 4 个数字:

class AsyncDemoUsingAsyncOperations
{
    AsyncOperation asyncOp;
    bool isBusy;

    void NotifyStarted () {
        isBusy = true;
        Started (this, new EventArgs ());
    }

    void NotifyStopped () {
        isBusy = false;
        Stopped (this, new EventArgs ());
    }

    public void Start () {
        if (isBusy)
            throw new InvalidOperationException ("Already working you moron...");

        asyncOp = AsyncOperationManager.CreateOperation (null);
        ThreadPool.QueueUserWorkItem (new WaitCallback (StartOperation));
    }

    public event EventHandler Started = delegate { };
    public event EventHandler Stopped = delegate { };
    public event EventHandler<NewNumberEventArgs> NewNumber = delegate { };

    private void StartOperation (object state) {
        asyncOp.Post (args => NotifyStarted (), null);

        for (int i = 1; i < 5; i++)
            asyncOp.Post (args => NewNumber (this, args as NewNumberEventArgs), new NewNumberEventArgs (i));

        asyncOp.Post (args => NotifyStopped (), null);
    }
}

class NewNumberEventArgs: EventArgs
{
    public int Num { get; private set; }

    public NewNumberEventArgs (int num) {
        Num = num;
    }
}

然后我写了2个测试程序;一个作为控制台应用程序,另一个作为 Windows 窗体应用程序。当我反复调用 Start 时,Windows 窗体应用程序按预期工作:

但是控制台应用程序很难确保顺序:

由于我正在研究类库,我必须确保我的库在所有应用模型中都能正常工作(尚未在 ASP.NET 应用中测试)。所以我有以下问题:

  1. 我已经测试了足够多的时间,它似乎可以正常工作,但可以假设上述代码将始终在 Windows 窗体应用程序中工作吗?
  2. 它(订单)在控制台应用程序中无法正常工作的原因是什么?我该如何解决?
  3. 没有太多的 ASP.NET 经验。订单可以在 ASP.NET 应用程序中使用吗?

[编辑:如果有帮助,可以看到测试存根here。]

【问题讨论】:

  • 输出值后是否可以像刷新 StdIO(在异步“子”中)一样简单?
  • @lexu:无法真正理解您的评论。你能用代码详细说明吗? (我附上了控制台和表单测试存根,如果你愿意看到它们)
  • 抱歉,没有代码。我已经看到异步/多线程中的输出表现得像你描述的那样,当客户端没有被强制立即刷新它的输出缓冲区时。您的 GUI 会对“信号/事件”接收做出反应,但来自客户端的控制台输出可能会延迟一小部分,从而使其他进程有机会首先写入控制台。 (这可以通过输出高分辨率计时器和计数器 1..4 来“调试/可视化”。但是,这是一个有根据的猜测..

标签: c# asynchronous


【解决方案1】:

除非我遗漏了什么,否则鉴于上面的代码,我相信无法保证执行顺序。我从未使用过 AsyncOperation 和 AsyncOperationManager 类,但我查看了反射器,可以假设 AsyncOperation.Post 使用线程池异步执行给定的代码。

这意味着在您提供的示例中,4 个任务将非常快地连续同步地排队到线程池中。然后,线程池将按 FIFO 顺序(先进先出)将任务出列,但完全有可能在较早的线程之前调度较晚的线程之一或在较早的线程完成其工作之前完成较晚的线程。

因此,鉴于您所拥有的,无法以您想要的方式控制顺序。有很多方法可以做到这一点,MSDN 上的这篇文章是一个很好的参考。

http://msdn.microsoft.com/en-us/magazine/dd419664.aspx

【讨论】:

    【解决方案2】:

    我使用队列,然后您可以按正确的顺序将内容入队和出列。这为我解决了这个问题。

    【讨论】:

      【解决方案3】:

      documentation for AsyncOperation.Post 声明:

      控制台应用程序不会同步 Post 调用的执行。这可能会导致 ProgressChanged 事件无序引发。如果您希望序列化执行 Post 调用,请实现并安装 System.Threading.SynchronizationContext 类。

      我认为这正是您所看到的行为。基本上,如果想要订阅来自异步事件的通知的代码想要按顺序接收更新,它必须确保安装了同步上下文并且您的 AsyncOperationManager.CreateOperation() 调用在该上下文中运行。如果使用异步事件的代码不关心以正确的顺序接收它们,它只需要避免安装同步上下文,这将导致使用“默认”上下文(它只是将调用直接排队到线程池,这可能按照它想要的任何顺序执行它们)。

      在您的应用程序的 GUI 版本中,如果您从 UI 线程调用您的 API,您将自动拥有一个同步上下文。这个上下文被连接起来使用 UI 的消息队列系统,该系统保证发布的消息是按顺序和连续处理的(即不是同时)。

      在控制台应用程序中,除非您手动安装自己的同步上下文,否则您将使用默认的非同步线程池版本。我不太确定,但我不认为 .net 使得安装序列化同步上下文变得非常容易。我只是使用Nito.AsyncEx nuget package 中的Nito.AsyncEx.AsyncContext 为我做这件事。基本上,如果您调用Nito.AsyncEx.AsyncContext.Run(MyMethod),它将捕获当前线程并运行一个事件循环,其中MyMethod 作为该事件循环中的第一个“处理程序”。如果MyMethod 调用创建AsyncOperation 的东西,则该操作会增加“正在进行的操作”计数器,并且该循环将继续,直到通过AsyncOperation.PostOperationCompletedAsyncOperation.OperationCompleted 完成操作。就像 UI 线程提供的同步上下文一样,AsyncContext 会将来自AsyncOperation.Post() 的帖子按接收顺序排列并在其事件循环中连续运行。

      下面是一个如何在演示异步操作中使用AsyncContext 的示例:

      class Program
      {
          static void Main(string[] args)
          {
              Console.WriteLine("Starting SynchronizationContext");
              Nito.AsyncEx.AsyncContext.Run(Run);
              Console.WriteLine("SynchronizationContext finished");
          }
      
          // This method is run like it is a UI callback. I.e., it has a
          // single-threaded event-loop-based synchronization context which
          // processes asynchronous callbacks.
          static Task Run()
          {
              var remainingTasks = new Queue<Action>();
              Action startNextTask = () =>
              {
                  if (remainingTasks.Any())
                      remainingTasks.Dequeue()();
              };
      
              foreach (var i in Enumerable.Range(0, 4))
              {
                  remainingTasks.Enqueue(
                      () =>
                      {
                          var demoOperation = new AsyncDemoUsingAsyncOperations();
                          demoOperation.Started += (sender, e) => Console.WriteLine("Started");
                          demoOperation.NewNumber += (sender, e) => Console.WriteLine($"Received number {e.Num}");
                          demoOperation.Stopped += (sender, e) =>
                          {
                              // The AsyncDemoUsingAsyncOperation has a bug where it fails to call
                              // AsyncOperation.OperationCompleted(). Do that for it. If we don’t,
                              // the AsyncContext will never exit because there are outstanding unfinished
                              // asynchronous operations.
                              ((AsyncOperation)typeof(AsyncDemoUsingAsyncOperations).GetField("asyncOp", BindingFlags.NonPublic|BindingFlags.Instance).GetValue(demoOperation)).OperationCompleted();
      
                              Console.WriteLine("Stopped");
      
                              // Start the next task.
                              startNextTask();
                          };
                          demoOperation.Start();
                      });
              }
      
              // Start the first one.
              startNextTask();
      
              // AsyncContext requires us to return a Task because that is its
              // normal use case.
              return Nito.AsyncEx.TaskConstants.Completed;
          }
      }
      

      有输出:

      Starting SynchronizationContext
      Started
      Received number 1
      Received number 2
      Received number 3
      Received number 4
      Stopped
      Started
      Received number 1
      Received number 2
      Received number 3
      Received number 4
      Stopped
      Started
      Received number 1
      Received number 2
      Received number 3
      Received number 4
      Stopped
      Started
      Received number 1
      Received number 2
      Received number 3
      Received number 4
      Stopped
      SynchronizationContext finished
      

      请注意,在我的示例代码中,我解决了AsyncDemoUsingAsyncOperations 中的一个错误,您可能应该修复它:当您的操作停止时,您永远不会调用AsyncOperation.OperationCompletedAsyncOperation.PostOperationCompleted。这会导致AsyncContext.Run() 永远挂起,因为它正在等待未完成的操作完成。你应该确保你的异步操作完成——即使是在错误的情况下。否则你可能会在其他地方遇到类似的问题。

      另外,我的演示代码模仿您在 winforms 和控制台示例中显示的输出,等待每个操作完成,然后再开始下一个操作。这种方式违背了异步编码的意义。你可能会说my code could be greatly simplified by starting all four tasks at once。每个单独的任务都会以正确的顺序接收其回调,但它们都会同时进行。

      推荐

      由于AsyncOperation 的工作方式和使用方式,异步 API 的调用者负责使用此模式来决定是否要接收事件是否有序。如果你打算使用AsyncOperation,你应该记录异步事件只会被调用者按顺序接收如果调用者有一个强制序列化的同步上下文,并建议调用者调用你的UI 线程上的 API 或类似 AsyncContext.Run() 的 API。如果您尝试在使用AsyncOperation.Post() 调用的委托中使用同步原语等,您最终可能会将线程池线程置于睡眠状态,这在考虑性能时是一件坏事,并且在调用者时完全是冗余/浪费API 已经正确设置了同步上下文。这也使调用者能够决定,如果可以接受乱序接收,它是否愿意同时处理和乱序处理事件。这甚至可以根据您正在做的事情来实现加速。或者,您甚至可以决定在您的NewNumberEventArgs 中加入序列号之类的内容,以防调用者既需要并发,又需要在某个时间点将事件按顺序组装起来。

      【讨论】:

        猜你喜欢
        • 2019-08-26
        • 1970-01-01
        • 2019-02-24
        • 1970-01-01
        • 2022-01-05
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2017-11-03
        相关资源
        最近更新 更多