如何确保异步操作调用的顺序?

时间:2009-11-09 05:51:34

标签: c# asynchronous

[这个出现是一个很好的问题,但我试图让它尽可能清晰。请耐心等待,帮助我...]

我编写了一个支持异步操作的测试类。该操作只会报告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窗体应用程序按预期工作:

alt text

但是控制台应用很难确保订单:

alt text

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

  1. 我已经测试了足够多次并且它似乎正在工作但是可以假设上面的代码将始终在Windows窗体应用程序中工作吗?
  2. 是什么原因(订单)在控制台应用中无法正常工作?我该如何解决?
  3. 对ASP.NET没有多少经验。订单是否适用于ASP.NET应用程序?
  4. [编辑:如果有帮助,可以看到测试存根here。]

3 个答案:

答案 0 :(得分:2)

除非我遗漏了某些东西,否则给出上面的代码我相信没有办法保证执行的顺序。我从未使用AsyncOperation和AsyncOperationManager类,但我查看了反射器,可以假设AsyncOperation.Post使用线程池异步执行给定的代码。

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

因此,鉴于你拥有的东西,你无法以你想要的方式控制秩序。有很多方法可以做到这一点,这篇文章在MSDN上是一个好看的地方。

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

答案 1 :(得分:1)

我使用队列,然后你可以按正确的顺序排队东西和出队东西。这为我解决了这个问题。

答案 2 :(得分:0)

documentation for AsyncOperation.Post州:

  

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

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

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

在控制台应用程序中,除非您手动安装自己的同步上下文,否则您将使用默认的非同步线程池版本。我不完全确定,但我不认为.net使得安装序列化同步上下文非常容易。我只是使用Nito.AsyncEx.AsyncContext中的Nito.AsyncEx nuget package来为我做这件事。基本上,如果你调用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线程或AsyncContext.Run()之类的内容中调用您的API。如果您尝试在使用AsyncOperation.Post()调用的委托内部使用同步原语和诸如此类的东西,则可能最终将线程池线程置于休眠状态,这在考虑性能时是一件坏事,并且在调用者时完全是冗余/浪费你的API已经正确设置了同步上下文。这也使得调用者能够决定,如果接收到无序的事情,那么它愿意同时处理事件并且不按顺序处理事件。这甚至可以根据您正在做的事情启用加速。或者您甚至可能决定在NewNumberEventArgs中添加类似序列号的内容,以防调用者想要并发,并且仍然需要能够在某些时候将事件组合成顺序。