StephenCleary / AsyncEx

A helper library for async/await.
MIT License
3.49k stars 358 forks source link

Order of execution for returning async Task functions #241

Open TheRealAyCe opened 2 years ago

TheRealAyCe commented 2 years ago

I observed that in WPF, the following code has the "Background Stuff" complete before GetAsync() fully returns, but in AsyncContextThread (.Factory.Start(Start)) it always writes "Done" before the background stuff is complete. Why is that?


    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            Start();
        }

        async void Start()
        {
            Write("Start");
            var val = await GetAsync();
            Write("Done: " + val);
        }

        async Task<string> GetAsync()
        {
            Write("GetAsync");
            await Task.Yield();
            Write("Yield awaited");
            BackgroundStuff();
            Write("Returning...");
            return "abc";
        }

        async void BackgroundStuff()
        {
            Write("BG stuff started");
            await Task.Yield();
            Write("BG stuff done");
        }

        void Write(string text)
        {
            Thread.Sleep(1000);
            Debug.WriteLine(text);
        }
    }

This is a problem in WPF when you want to write code like this, where a "live object" is created that raises events on its own (like receiving messages from a connection). Here the first message is skipped, since it takes a cycle to return the ConnectAsync() result in WPF, but not in AsyncEx. So the Task.Yield() inside the EventLoop() does help not to raise the first event initially, but even though no async work is performed before returning the created LiveClass, it will miss the first event. A "solution" is to do multiple Task.Yield()s in the EventLoop() function before starting, to give code time to register to the events, or to forgo events and just pass delegates to ConnectAsync() to bind those before ConnectAsync() can return. Or to have a separate StartLoop() function that you need to call when you registered your event handlers.

            var liveClass = await LiveClass.ConnectAsync();
            liveClass.Counter += x => Debug.WriteLine("Got " + x);

    class LiveClass
    {
        public static async Task<LiveClass> ConnectAsync()
        {
            await Task.Delay(1);
            var inst = new LiveClass();
            return inst;
        }

        public event Action<int> Counter;

        public LiveClass()
        {
            EventLoop();
        }

        private async void EventLoop()
        {
            // yield so that events are not raised immediately
            await Task.Yield();

            int num = 0;
            while (true)
            {
                Counter?.Invoke(num);
                num++;
                await Task.Delay(1000);
            }
        }
    }

Some might also say that the fire-and-forget async void pattern here is bad, but what would be a good alternative? I can't await the "Task" of looping itself, since I want to start the object and get notified when something like a disconnect happens. I don't want to start a new thread, this is all supposed to execute in the same thread, so you need to start it inside a SynchronizationContext that is single-threaded of course, Console/ASP.NET wouldn't do.