Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Order of execution for returning async Task functions #241

Open
TheRealAyCe opened this issue Oct 8, 2021 · 0 comments
Open

Order of execution for returning async Task functions #241

TheRealAyCe opened this issue Oct 8, 2021 · 0 comments
Assignees

Comments

@TheRealAyCe
Copy link

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.

@StephenCleary StephenCleary self-assigned this Nov 15, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants