1

I am getting unexpected behavior that I would like to shed some light on. I've created a simple example to demonstrate the problem. I call an async function using Task.Run, which will continuously generate results, and uses IProgress to deliver updates to the UI. But I want to wait until after the UI actually updates to continue, so I tried using TaskCompletionSource as suggested in some other posts (this seemed somewhat similar: Is it possible to await an event instead of another async method?.) I'm expecting the initial Task.Run to wait, but what is happening is the await happening inside seems to move it onward and "END" happens after the first iteration. Start() is the entry point:

public TaskCompletionSource<bool> tcs;

public async void Start()
{
    var progressIndicator = new Progress<List<int>>(ReportProgress);

    Debug.Write("BEGIN\r");
    await Task.Run(() => this.StartDataPush(progressIndicator));
    Debug.Write("END\r");
}

private void ReportProgress(List<int> obj)
{
    foreach (int item in obj)
    {
        Debug.Write(item + " ");
    }
    Debug.Write("\r");
    Thread.Sleep(500);

    tcs.TrySetResult(true);
}

private async void StartDataPush(IProgress<List<int>> progressIndicator)
{
    List<int> myList = new List<int>();

    for (int i = 0; i < 3; i++)
    {
        tcs = new TaskCompletionSource<bool>();

        myList.Add(i);
        Debug.Write("Step " + i + "\r");

        progressIndicator.Report(myList);

        await this.tcs.Task;
    }
}

With this I get:

BEGIN
Step 0
0 
END
Step 1
0 1 
Step 2
0 1 2 

instead of what I want to get which is:

BEGIN
Step 0
0 
Step 1
0 1 
Step 2
0 1 2 
END

I'm assuming I am misunderstanding something about Tasks and await and how they work. I do want StartDataPush to be a separate thread, and my understanding is that it is. My end use is somewhat more complex as it involves heavy calculation, updating to a WPF UI and events signaling back that it completed, but the mechanics are the same. How can I achieve what I'm trying to do?

Cloudless
  • 19
  • 3
  • 1
    You cannot meaningfully await an `async void`. Make it `async Task`. – GSerg Jan 16 '20 at 21:11
  • There was a comment about why `Progress.Report` would not [run as expected](https://stackoverflow.com/q/46498581/11683) when called. I said it was wrong, and it was deleted. I believe *I* was wrong about that. – GSerg Jan 16 '20 at 22:59

2 Answers2

2

I'm not fully understanding the goal you are trying to achieve. But the issue is StartDataPush returning void. The only time an async should return void is if it is an event handler otherwise it needs to return Task.

The following would achieve what you expected in terms of output

public partial class MainWindow : Window
{
    public TaskCompletionSource<bool> tcs;

    public MainWindow()
    {
        InitializeComponent();
    }

    private async void ButtonBase_OnClick(object sender, RoutedEventArgs e)
    {
        var progressIndicator = new Progress<List<int>>(ReportProgress);

        Debug.Write("BEGIN\r");
        await StartDataPush(progressIndicator);
        Debug.Write("END\r");
    }


    private void ReportProgress(List<int> obj)
    {
        foreach (int item in obj)
        {
            Debug.Write(item + " ");
        }
        Debug.Write("\r");
        Thread.Sleep(500);

        tcs.TrySetResult(true);
    }

    private async Task StartDataPush(IProgress<List<int>> progressIndicator)
    {
        List<int> myList = new List<int>();

        for (int i = 0; i < 3; i++)
        {
            tcs = new TaskCompletionSource<bool>();

            myList.Add(i);
            Debug.Write("Step " + i + "\r");

            progressIndicator.Report(myList);

            await this.tcs.Task;
        }
    }
}
Joe H
  • 584
  • 3
  • 14
  • There's also no need in the entire `tcs` business. `progressIndicator.Report(myList)` runs synchronously, the `tcs.TrySetResult(true);` and `await this.tcs.Task;` can be removed. – GSerg Jan 16 '20 at 21:40
  • For OPs benefit, the reason asynchronous methods must be `Task` is because `Task` _is_ the awaitable. `async`/`await` are simply keywords which tell the compiler to create an `IAsyncStateMachine` – ColinM Jan 16 '20 at 21:43
  • Thank you, that works! What I was trying to do was not have the code advance to the "END" write before the looping was complete in the called async function. This code worked before I added in the TaskCompletetionSource (with a sleep in the loop). There is no point in this sample code but it makes sense in the actual application. @ColinM thanks you answered my next question. I'm surprised it worked before that change. – Cloudless Jan 16 '20 at 21:49
  • 1
    @GSerg if I remove the `tcs` portions I get BEGIN Step 0 Step 1 Step 2 0 1 2 0 1 2 0 1 2 END as an output. I'm trying to report each value as it's added before the next value is actually added. – Cloudless Jan 16 '20 at 21:53
  • @Cloudless Given the `await StartDataPush(progressIndicator);` (without `Task.Run`), you can remove the `tcs` and `Thread.Sleep` and use `await Task.Yield();` instead, right after `progressIndicator.Report(myList);`. – GSerg Jan 17 '20 at 08:54
1

According to the documentation of the Progress<T> class:

Any handler provided to the constructor is invoked through a SynchronizationContext instance captured when the instance is constructed. If there is no current SynchronizationContext at the time of construction, the callbacks will be invoked on the ThreadPool.

The phrase "is invoked through a SynchronizationContext" is a bit vague. What it actually happens is that the method SynchronizationContext.Post is invoked.

When overridden in a derived class, dispatches an asynchronous message to a synchronization context.

The word asynchronous is the key here. In your case you want the reports to occur synchronously (Send), not asynchronously (Post), and the Progress<T> class offers no configuration on whether it invokes the Send or Post method of the captured SynchronizationContext.

Luckily implementing a synchronous IProgress<T> is trivial:

public class SynchronousProgress<T> : IProgress<T>
{
    private readonly Action<T> _handler;
    private readonly SynchronizationContext _synchronizationContext;
    public SynchronousProgress(Action<T> handler)
    {
        ArgumentNullException.ThrowIfNull(handler);
        _handler = handler;
        _synchronizationContext = SynchronizationContext.Current;
    }

    public void Report(T value)
    {
        if (_synchronizationContext is not null)
        {
            _synchronizationContext.Send(s => _handler((T)s), value);
        }
        else
        {
            _handler(value);
        }
    }
}

Just use the SynchronousProgress class instead of the built-in Progress, and you'll no longer need to do tricks with the TaskCompletionSource class.

I should make it clear that the method SynchronousProgress.Report is a blocking method. The caller of this method will block until the handler invocation has been completed.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104