-3

I tested two apps (.NET6.0 Win10).

App1:

namespace TestConsoleApp;
class Program
{
    static bool Flag;
    static async void Wd()
    {
        Console.WriteLine("Wd starts " + DateTime.Now.ToString("ss.ffff"));
        Console.WriteLine("Wd Thread ID " + Thread.CurrentThread.ManagedThreadId);
        await Task.Delay(5000);
        Flag = false;
        Console.WriteLine("Wd ends " + DateTime.Now.ToString("ss.ffff"));
        Console.WriteLine("Wd Thread ID " + Thread.CurrentThread.ManagedThreadId) ;
    }
    static void Main()
    {
        int i = 60;
        Flag = true;
        Console.WriteLine("Start " + DateTime.Now.ToString("ss.ffff"));
        Console.WriteLine("Main Thread ID " + Thread.CurrentThread.ManagedThreadId);
        Wd();
        while (i-- > 0 && Flag) Task.Delay(100).Wait();
        Console.WriteLine("End " + i.ToString() + " " + DateTime.Now.ToString("ss.ffff"));
    }
}

Output 1:

Start 21.2212

Main Thread ID 1

Wd starts 22.0262

Wd Thread ID 1

Wd ends 27.0454

Wd Thread ID 5

End 19 27.0904

It performed as expected (the watchdog breaks the while loop in 5 s). A new thread is created implicitly at the expected point.

App2:

namespace WinFormsAppTaskDelay;
internal static class Program
{
    [STAThread]
    static void Main()
    {
        ApplicationConfiguration.Initialize();
        Application.Run(new Form1());
    }
}
class Form1:Form
{
    bool Flag;
    async void Wd()
    {
        label4.Text = $"Wd start{DateTime.Now:ss.ffff}";
        label7.Text = $"Thread ID {Thread.CurrentThread.ManagedThreadId}";
        await Task.Delay(5000); Flag = false;
        label9.Text = $"Thread ID {Thread.CurrentThread.ManagedThreadId}";
        label2.Text = $"Wd exit {DateTime.Now:ss.ffff}";
    }

    public Form1()
    {
        InitializeComponent(); int i = 60; Flag = true;
        label1.Text = $"Start {DateTime.Now:ss.ffff}";
        label6.Text = $"Thread ID {Thread.CurrentThread.ManagedThreadId}";
        Wd();
        //Task.Run(Wd);
        label5.Text = $"Check point {DateTime.Now:ss.ffff}";
        label8.Text = $"Thread ID {Thread.CurrentThread.ManagedThreadId}";
        while (i-- > 0 && Flag) Task.Delay(100).Wait();
        label3.Text = $"End {i} {DateTime.Now:ss.ffff}";
    }

    private System.ComponentModel.IContainer components = null;
    protected override void Dispose(bool disposing)
    {
        if (disposing && (components != null)) { components.Dispose(); }
        base.Dispose(disposing);
    }
    private void InitializeComponent()
    {
        label1 = new Label(); label2 = new Label(); label3 = new Label(); label4 = new Label(); label5 = new Label();
        label6 = new Label(); label7 = new Label(); label8 = new Label(); label9 = new Label();
        SuspendLayout();
        label1.Location = new Point(33, 25); label1.Text = label1.Name = "label1";
        label1.Size = new Size(38, 15); label1.TabIndex = 0;
        label2.Location = new Point(33, 141); label2.Text = label2.Name = "label2";
        label2.Size = new Size(38, 15); label2.TabIndex = 1;
        label3.Location = new Point(33, 187); label3.Text = label3.Name = "label3";
        label3.Size = new Size(38, 15); label3.TabIndex = 2;
        label4.Location = new Point(33, 60); label4.Text = label4.Name = "label4";
        label4.Size = new Size(38, 15); label4.TabIndex = 3;
        label5.Location = new Point(33, 100); label5.Text = label5.Name = "label5";
        label5.Size = new Size(38, 15); label5.TabIndex = 4;
        label6.Location = new Point(227, 22); label6.Text = label6.Name = "label6";
        label6.Size = new Size(38, 15); label6.TabIndex = 5;
        label7.Location = new Point(227, 60); label7.Text = label7.Name = "label7";
        label7.Size = new Size(38, 15); label7.TabIndex = 6;
        label8.Location = new Point(227, 100); label8.Text = label8.Name = "label8";
        label8.Size = new Size(38, 15); label8.TabIndex = 7;
        label9.Location = new Point(227, 141); label9.Text = label9.Name = "label9";
        label9.Size = new Size(38, 15); label9.TabIndex = 8;
        label1.AutoSize = label2.AutoSize = label3.AutoSize = label4.AutoSize = label5.AutoSize =
            label6.AutoSize = label7.AutoSize = label8.AutoSize = label9.AutoSize = true;
        AutoScaleDimensions = new SizeF(7F, 15F); AutoScaleMode = AutoScaleMode.Font; ClientSize = new Size(460, 233);
        Controls.Add(label9); Controls.Add(label8); Controls.Add(label7); Controls.Add(label6); Controls.Add(label5);
        Controls.Add(label4); Controls.Add(label3); Controls.Add(label2); Controls.Add(label1);
        Text = Name = "Form1"; ResumeLayout(false); PerformLayout();
    }
    private Label label1; private Label label2; private Label label3; private Label label4; private Label label5;
    private Label label6; private Label label7; private Label label8; private Label label9;
}

Output 2: App2 output 1

Here the watchdog method doesn't work as expected. I expect the behavior same as in app1. We can see the new thread is not created for Wd.

With the changed constructor

        //Wd();

        Task.Run(Wd);

it works differently (as expected), the new thread is created and the watchdog is fired, but the async method works as a new thread, not as the true async one.

Output 3: App2 output 2

The output 2 behavior is still unclear for me. Async/await is expected to work, but no.

rotabor
  • 561
  • 2
  • 10
  • 1
    Related: [Call asynchronous method in constructor?](https://stackoverflow.com/questions/23048285/call-asynchronous-method-in-constructor) – Hans Kesting Oct 10 '22 at 13:48
  • 1
    In Winforms, the code after `await Task.Delay()` will be run on the original thread (look up "synchronization context", see also https://stackoverflow.com/questions/62681749/usage-of-configureawait-in-net). You could avoid this by appending `.ConfigureAwait(false)`, but then you cannot use any of the control's properties because you are on the wrong thread. What exactly do you want to accomplish? – Klaus Gütter Oct 10 '22 at 13:56
  • Now I want to look at the product documentation (.NET) where it's stated – rotabor Oct 10 '22 at 14:03
  • @Klaus Gütter, why do you like to explain difficult things that can be explained simply? The task continuation for the rest right after the await operator of the async method is created by the current TaskSheduler. Since the main UI method runs in the single-threaded apartment ([STATread] attribute), the continuation is created in the same thread and run within it as scheduled. 'You could avoid this by appending .ConfigureAwait(false)...'. In [MTAThread] (default) the new thread will be created to continue the async method. – rotabor Oct 10 '22 at 18:32
  • 1
    @rotabor - No new thread is created. – Enigmativity Oct 12 '22 at 04:32
  • You cannot update any UI element on a non-UI thread, so calling `label9.Text = $"Thread ID {Thread.CurrentThread.ManagedThreadId}";` is pointless as the only thread it can run on safely is the UI thread. You need to ensure, when you use `async` code, that you are accessing, creating, and updating UI elements on the UI thread only. If you don't all bets are off to how the code runs. – Enigmativity Oct 12 '22 at 04:35
  • @Enigmativity Do you think no new thread here: 'Wd Thread ID 1 Wd ends 27.0454 Wd Thread ID 5'? – rotabor Oct 13 '22 at 11:12
  • @rotabor - There is no new thread. `Task.Run` uses threads from the thread-pool. – Enigmativity Oct 15 '22 at 05:49
  • @Enigmativity You are deliberately changing the subject. We talked about threads used by Wd, and the new thread was involved regardless of where is it from: newly created or taken from the pool. Ok, just let's talk not about new thread but about another one which is not the previously (earlier) used thread. – rotabor Oct 15 '22 at 08:49
  • @rotabor - I feel it is misleading for you to have said "new thread" when there isn't one. I didn't change the subject. – Enigmativity Oct 15 '22 at 22:43

2 Answers2

1

The difference is the synchronization context (or lack thereof). After await, the synchronization context will define how the rest of the method runs.

The synchronization context of WinForms is the UI thread.

A console application does not have a synchronization context, so the rest of the method can be run on any ThreadPool thread.

You can find a list of the default SynchronizationContext for different application types in the MSDN article Parallel Computing - It's All About the SynchronizationContext.

You can use .ConfigureAwait(false) to specifically tell it that you don't need to resume on the synchronization context:

await Task.Delay(5000).ConfigureAwait(false);

And in that case, the WinForms application will behave the same as the console app.

Some people like to use ConfigureAwait(false) just about everywhere they can. I suggest you don't get into that habit (except if you're writing a third-party library), for reasons I wrote about here: .NET: Don’t use ConfigureAwait(false)

The Microsoft documentation for async/await is a great introduction. Read through that series of articles, which starts here: https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/async/

Gabriel Luci
  • 38,328
  • 4
  • 55
  • 84
  • Good answer, thanks. The problem with the product documentation is that the implementation of the async/await is very hidden. Just pay attention on the fact that this 'great introduction' is written for console app where the presented concept works perfectly as per spec. And nothing about that it can behave different, and it's programmable. Generally this concept becomes useless in the single-threaded apartment. Just run tasks to execute the code asynchronously (in parallel), then Invoke the code to bring results at the form. – rotabor Oct 13 '22 at 08:19
  • '.NET: Don’t use ConfigureAwait(false)' is seeing true, but the opposite statement is seeing true too. How to resolve this contradiction? Just every body needs to exactly know what it do and which tools (APIs) are available. There is no an universal solution for every case. – rotabor Oct 13 '22 at 08:26
  • 'The difference is the synchronization context (or lack thereof)' - the synchronization context is always available – rotabor Oct 13 '22 at 08:29
  • However, this discussion helped me a lot. – rotabor Oct 13 '22 at 08:41
  • @rotabor you can print at any time the [`SynchronizationContext.Current`](https://docs.microsoft.com/en-us/dotnet/api/system.threading.synchronizationcontext.current), and see if a `SynchronizationContext` is installed on the current thread or not. – Theodor Zoulias Oct 13 '22 at 10:11
  • *"Just run tasks to execute the code asynchronously (in parallel)"* - I think this is your misunderstanding. Asynchronous != parallel. Code can all run asynchronously on the same thread. – Gabriel Luci Oct 13 '22 at 12:18
  • *"'.NET: Don’t use ConfigureAwait(false)' is seeing true, but the opposite statement is seeing true too."* - What do you mean by "is seeing true"? – Gabriel Luci Oct 13 '22 at 12:18
  • *"Code can all run asynchronously on the same thread."* -- Gabriel this statement could confuse people. Pure asynchronous operations [are not running on threads](https://blog.stephencleary.com/2013/11/there-is-no-thread.html). Maybe you intended to say "asynchronous operations can complete and then continue on the same thread that they started"? – Theodor Zoulias Oct 13 '22 at 22:39
  • 1
    @TheodorZoulias Yes, that is a more accurate way to put it. Thank you! – Gabriel Luci Oct 14 '22 at 01:36
  • @GabrielLuci Asynchronous means (applied to subj) 'in undefined order with undefined time interval'. It can be more in parallel rather than sequentially. As soon as few tasks are scheduled in a single thread they become synchronous. – rotabor Oct 14 '22 at 17:23
  • @rotabor Where did you see that definition? Asynchronous means that the thread is not blocked while waiting for something else. The tasks being scheduled are the *resumption* of code that was waiting for something else. – Gabriel Luci Oct 14 '22 at 17:31
  • Parallel means two pieces of code running at the same time. That can only be done with multiple threads. It's about how code *runs*. Asynchronous means not blocking while waiting. It's about how code *waits*. The waiting happens asynchronously. The resuming *can* happen in parallel, but not necessarily so. – Gabriel Luci Oct 14 '22 at 17:34
  • @GabrielLuci about definition from https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/async/: 'That's the goal of this syntax: enable code that reads like a sequence of statements, but executes in a much more complicated order based on external resource allocation and when tasks are complete'. My definition is just an engineering definition. Yes, https://en.wikipedia.org/wiki/Asynchrony_(computer_programming) tells something about 'without the program blocking to wait for results', but it's not a main concern – rotabor Oct 14 '22 at 18:05
  • (cont) At the same time it tells 'Doing so provides a degree of parallelism'. It's for Asynchronous ?= parallel – rotabor Oct 14 '22 at 18:06
  • *"'without the program blocking to wait for results', but it's not a main concern"* - I'm not sure I follow you here. Not blocking while waiting for results *is* the main concern. It's the entire purpose of asynchronous programming. – Gabriel Luci Oct 14 '22 at 18:26
  • Since results are not mandatory (do you know about void I guess) waiting for them can't be a main concern – rotabor Oct 14 '22 at 18:37
  • Related question: [What is the difference between concurrency, parallelism and asynchronous methods?](https://stackoverflow.com/questions/4844637/what-is-the-difference-between-concurrency-parallelism-and-asynchronous-methods) The definitions of parallelism and concurrency are clear cut. On the contrary the definition of asynchronous is much more vague. – Theodor Zoulias Oct 15 '22 at 01:37
0

The way to perform a task asynchronously in the form:

    ...
    label4.Text = $"Wd start{DateTime.Now:ss.ffff}";
    label7.Text = $"Thread ID {Thread.CurrentThread.ManagedThreadId}";
    Task.Run(Wd);
    ...

    void Wd()
    {
        Task.Delay(5000).Wait(); Flag = false;
        Invoke(ProvideResults, Thread.CurrentThread.ManagedThreadId, DateTime.Now);
    }
    void ProvideResults(int thrid, DateTime dt) { label9.Text = $"Thread ID {thrid}"; label2.Text = $"Wd exit {dt:ss.ffff}"; }

'label.Text = ...' is a sample code here.

In this solution the context is explicitly defined by creating a task and then come back to form's context by Invoke.

rotabor
  • 561
  • 2
  • 10