Reputation: 19
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<T>
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?
Upvotes: 1
Views: 3119
Reputation: 43846
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 currentSynchronizationContext
at the time of construction, the callbacks will be invoked on theThreadPool
.
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.
Upvotes: 1
Reputation: 604
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;
}
}
}
Upvotes: 2