Write an Async method that will await a bool - c#

I would like to write a method that will await for a variable to be set to true.
Here is the psudo code.
bool IsSomethingLoading = false
SomeData TheData;
public async Task<SomeData> GetTheData()
{
await IsSomethingLoading == true;
return TheData;
}
TheData will be set by a Prism Event along with the IsSomethingLoading variable.
I have a call to the GetTheData method, but I would like it to run async (right now it just returns null if the data is not ready. (That leads to other problems.)
Is there a way to do this?

In many situations like this what you need is a TaskCompletionSource.
You likely have a method that is able to generate the data at some point in time, but it doesn't use a task to do it. Perhaps there is a method that takes a callback which provides the result, or an event that is fired to indicate that there is a result, or simply code using a Thread or ThreadPool that you are not inclined to re-factor into using Task.Run.
public Task<SomeData> GetTheData()
{
TaskCompletionSource<SomeData> tcs = new TaskCompletionSource<SomeData>();
SomeObject worker = new SomeObject();
worker.WorkCompleted += result => tcs.SetResult(result);
worker.DoWork();
return tcs.Task;
}
While you may need/want to provide the TaskCompletionSource to the worker, or some other class, or in some other way expose it to a broader scope, I've found it's often not needed, even though it's a very powerful option when it's appropriate.
It's also possible that you can use Task.FromAsync to create a task based on an asynchronous operation and then either return that task directly, or await it in your code.

You could use a TaskCompletionSource as your signal, and await that:
TaskCompletionSource<bool> IsSomethingLoading = new TaskCompletionSource<bool>();
SomeData TheData;
public async Task<SomeData> GetTheData()
{
await IsSomethingLoading.Task;
return TheData;
}
And in your Prism event do:
IsSomethingLoading.SetResult(true);

This work for me:
while (IsLoading) await Task.Delay(100);

I propose a very simple solution but not the best to answer the original question, if you are not regarding at speed performance :
...
public volatile bool IsSomethingLoading = false;
...
public async Task<SomeData> GetTheData()
{
// Launch the task asynchronously without waiting the end
_ = Task.Factory.StartNew(() =>
{
// Get the data from elsewhere ...
});
// Wait the flag
await Task.Factory.StartNew(() =>
{
while (IsSomethingLoading)
{
Thread.Sleep(100);
}
});
return TheData;
}
Important note : #Theodor Zoulias proposed : IsSomethingLoading shall be declared with volatile keyword, to avoid compiler optimizations and potential multithread issues when accessing from other threads.
For further information about compilator omptimizations follow this article :
The C# Memory Model in Theory and Practice
I'm adding a full test code below :
XAML :
<Label x:Name="label1" Content="Label" HorizontalAlignment="Left" Margin="111,93,0,0" VerticalAlignment="Top" Grid.ColumnSpan="2" Height="48" Width="312"/>
Test Code :
public partial class MainWindow : Window
{
// volatile keyword shall be used to avoid compiler optimizations
// and potential multithread issues when accessing IsSomethingLoading
// from other threads.
private volatile bool IsSomethingLoading = false;
public MainWindow()
{
InitializeComponent();
_ = TestASyncTask();
}
private async Task<bool> TestASyncTask()
{
IsSomethingLoading = true;
label1.Content = "Doing background task";
// Launch the task asynchronously without waiting the end
_ = Task.Factory.StartNew(() =>
{
Thread.Sleep(2000);
IsSomethingLoading = false;
Thread.Sleep(5000);
HostController.Host.Invoke(new Action(() => label1.Content = "Background task terminated"));
});
label1.Content = "Waiting IsSomethingLoading ...";
// Wait the flag
await Task.Run(async () => { while (IsSomethingLoading) { await Task.Delay(100); }});
label1.Content = "Wait Finished";
return true;
}
}
/// <summary>
/// Main UI thread host controller dispatcher
/// </summary>
public static class HostController
{
/// <summary>
/// Main Host
/// </summary>
private static Dispatcher _host;
public static Dispatcher Host
{
get
{
if (_host == null)
{
if (Application.Current != null)
_host = Application.Current.Dispatcher;
else
_host = Dispatcher.CurrentDispatcher;
}
return _host;
}
}
}

Related

background task is blocking main thread in wpf .net core application

I have a background task in my software which should run indefinitely.
The code repeating the task (and possibly other tasks in the future) looks like this:
public class RunTicketTasks
{
public async Task RunBackgroundTasks()
{
AssignTickets assignTickets = new AssignTickets();
while (true)
{
await assignTickets.AssignTicketCreator();
await Task.Delay(5 * 60 * 1000);
}
}
}
At the same time I have the WPF UI MainWindow.xaml which is the application entry point as far as I know.
within public MainWindow I start the task like the following way:
public MainWindow()
{
InitializeComponent();
JiraBackgroundTasks.RunTicketTasks ticketTasks = new JiraBackgroundTasks.RunTicketTasks();
ticketTasks.RunBackgroundTasks();
}
Apparently, this starts the task on the same thread (I believe). The task is started and running successfully, but the UI is beeing blocked due to long running operations. Meanwhile, when I uncomment the last line ticketTasks.RunBackgroundTasks(); the UI just runs fine.
How do I start the task in the background so that my User Interface is still responsive?
EDIT:
The reason I started the task this way was because of exception handling.
I can successfully start the task on a different thread as suggested with Task.Run(async () => await ticketTasks.RunBackgroundTasks()); But then I will not receive any exception if something goes wrong.
How can I start the task not blocking the UI and still receive the exception details if an exception is thrown?
The Internet states the following method:
await Task.Run(() => ticketTasks.RunBackgroundTasks());
But this will not work because The await operator can only be used within an async method.
One way to do it is to wrap the task in an async event callback:
public partial class MainWindow : Window
{
// I mocked your missing types, replace with your own
class AssignTickets
{
public Task AssignTicketCreator() => Task.CompletedTask;
}
public class RunTicketTasks
{
public async Task RunBackgroundTasks()
{
AssignTickets assignTickets = new AssignTickets();
int x = 0;
while (true)
{
await assignTickets.AssignTicketCreator();
await Task.Delay(1000);
var isRunningOnDispatcher = Application.Current.Dispatcher ==
System.Windows.Threading.Dispatcher.FromThread(Thread.CurrentThread);
Trace.WriteLine($"One second passed, running on dispatcher: {isRunningOnDispatcher}");
// exception will also get thrown on the main thread
if (x++ > 3) throw new Exception("Hello!");
}
}
}
public MainWindow()
{
InitializeComponent();
// event fires on UI thread!
this.Loaded += async (sender, args) =>
{
try
{
await new RunTicketTasks().RunBackgroundTasks();
}
catch (Exception e)
{
MessageBox.Show(e.Message);
}
};
}
}

Keeping a responsive UI while using async/await and multithreading

I have an intense computation I'm doing, which includes code run in parallel. Inside the parallel methods we await calls to async methods. Because Parallel.For can't do that, we have some code based on channels.
The problem is that it seems to be blocking the UI thread, even though we're setting up the handler to avoid that. If I use Task.Delay(1) in the worker it seems to work, but that's only curing the symptom and not the problem.
How can I keep the UI thread from getting blocked?
Here's the code to the view model:
using Prism.Commands;
using Prism.Mvvm;
using Extensions.ParallelAsync;
using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace MVVMAwaitUiThread
{
public class MainWindowViewModel : BindableBase
{
public MainWindowViewModel()
{
DoSomethingGoodCommand = new DelegateCommand(DoSomethingGood);
DoSomethingBadCommand = new DelegateCommand(DoSomethingBad);
}
private ProgressViewModel _progressViewModel;
public ProgressViewModel ProgressViewModel
{
get => _progressViewModel;
set => SetProperty(ref _progressViewModel, value);
}
private bool _isBusy = false;
public bool IsBusy
{
get => _isBusy;
set => SetProperty(ref _isBusy, value);
}
private string _workText = "";
public string WorkText
{
get => _workText;
set => SetProperty(ref _workText, value);
}
public DelegateCommand DoSomethingGoodCommand { get; private set; }
public async void DoSomethingGood()
{
IsBusy = true;
try
{
ProgressViewModel = new ProgressViewModel();
double sum = await ReallyDoSomething(1, ProgressViewModel.Progress, ProgressViewModel.CancellationToken);
WorkText = $"Did work {DateTime.Now} -> {sum}.";
}
catch (OperationCanceledException)
{
// do nothing
}
finally
{
IsBusy = false;
}
}
public DelegateCommand DoSomethingBadCommand { get; private set; }
public async void DoSomethingBad()
{
IsBusy = true;
try
{
ProgressViewModel = new ProgressViewModel();
double sum = await ReallyDoSomething(0, ProgressViewModel.Progress, ProgressViewModel.CancellationToken);
WorkText = $"Did work {DateTime.Now} -> {sum}.";
}
catch (OperationCanceledException)
{
// do nothing
}
finally
{
IsBusy = false;
}
}
/// <summary>
/// Calling this with 0 doesn't work, but 1 does
/// </summary>
private async Task<double> ReallyDoSomething(int delay, IProgress<double> progress, CancellationToken cancellationToken)
{
const double maxIterations = 250;
const int sampleCount = 10;
const int maxDegreeOfParallelism = -1; // this doesn't seem to have any effect
const double totalIterations = sampleCount * maxIterations;
int completedIterations = 0;
ConcurrentBag<double> bag = new ConcurrentBag<double>();
// In reality, I have calculations that make calls to async/await methods, but each iteration can be parallel
// Can't make async calls in parallel.for, so this is what we have come up with
await ParallelChannelsAsync.ForAsync(0, sampleCount, maxDegreeOfParallelism, cancellationToken, Eval).ConfigureAwait(false);
async Task Eval(int seed, CancellationToken cancellationToken)
{
double sum = seed;
for (int i = 0; i < maxIterations; ++i)
{
sum += i * (i + 1.0); // simulate computation
await Task.Delay(delay); // simulate an async call
Interlocked.Increment(ref completedIterations);
progress?.Report(completedIterations / totalIterations);
cancellationToken.ThrowIfCancellationRequested();
}
bag.Add(sum / maxIterations);
};
return bag.Sum();
}
}
}
This is a (very-)simplified VS2019 project which demonstrates the problem:
https://drive.google.com/file/d/1ZB4r6QRu94hbxkz_qblkVQiQZCiNLN9i/view?usp=sharing
It's unclear what your code actually does but just because a method has a asynchronous API doesn't necessarily means that it is implemented to not block.
Consider the following method as an example. It appears to be async from a callers perspective but it clearly isn't:
public Task<int> MyNotSoAsyncMethod()
{
//I'm actually blocking...
Thread.Sleep(3000);
return Task.FromResult(0);
}
How can I keep the UI thread from getting blocked?
If you want to be sure not to block the calling thread regardless of the implementation of the method you are calling, then call it on a background thread. For example by creating a Task:
await Task.Run(DoSomethingThatMightBlock);
Using info from #JonasH and #mm8 and a lot of debugging, the problem is really that the UI thread is being starved by events fired from INotifyPropertyChanged.PropertyChanged because we're using MVVM.
We used two pieces to solve the problem. Where the main algorithm is called, we did need to use a Task.Run() to get it on a separate thread.
But we also had a lot of calls to IProgress.Report(), and the UI was actually doing more work than it needed to. In general, if you get multiple Reports() before the UI can draw it, then you really only need the last one. So we wrote this code to basically throw away Report() calls that are 'queued' up.
/// <summary>
/// Assuming a busy workload during progress reports, it's valuable to only keep the next most recent
/// progress value, rather than back pressuring the application with tons of out-dated progress values,
/// which can result in a locked up application.
/// </summary>
/// <typeparam name="T">Type of value to report.</typeparam>
public class ThrottledProgress<T> : IProgress<T>, IAsyncDisposable
{
private readonly IProgress<T> _progress;
private readonly Channel<T> _channel;
private Task _completion;
public ThrottledProgress(Action<T> handleReport)
{
_progress = new Progress<T>(handleReport);
_channel = Channel.CreateBounded<T>(new BoundedChannelOptions(1)
{
AllowSynchronousContinuations = false,
FullMode = BoundedChannelFullMode.DropOldest,
SingleReader = true,
SingleWriter = true
});
_completion = ConsumeAsync();
}
private async Task ConsumeAsync()
{
await foreach (T value in _channel.Reader.ReadAllAsync().ConfigureAwait(false))
{
_progress.Report(value);
}
}
void IProgress<T>.Report(T value)
{
_channel.Writer.TryWrite(value);
}
public async ValueTask DisposeAsync()
{
if (_completion is object)
{
_channel.Writer.TryComplete();
await _completion.ConfigureAwait(false);
_completion = null;
}
}
}

UWP Update UI From Async Worker

I am trying to implement a long-running background process, that periodically reports on its progress, to update the UI in a UWP app. How can I accomplish this? I have seen several helpful topics, but none have all of the pieces, and I have been unable to put them all together.
For example, consider a user who picks a very large file, and the app is reading in and/or operating on the data in the file. The user clicks a button, which populates a list stored on the page with data from the file the user picks.
PART 1
The page and button's click event handler look something like this:
public sealed partial class MyPage : Page
{
public List<DataRecord> DataRecords { get; set; }
private DateTime LastUpdate;
public MyPage()
{
this.InitializeComponent();
this.DataRecords = new List<DataRecord>();
this.LastUpdate = DateTime.Now;
// Subscribe to the event handler for updates.
MyStorageWrapper.MyEvent += this.UpdateUI;
}
private async void LoadButton_Click(object sender, RoutedEventArgs e)
{
StorageFile pickedFile = // … obtained from FileOpenPicker.
if (pickedFile != null)
{
this.DataRecords = await MyStorageWrapper.GetDataAsync(pickedFile);
}
}
private void UpdateUI(long lineCount)
{
// This time check prevents the UI from updating so frequently
// that it becomes unresponsive as a result.
DateTime now = DateTime.Now;
if ((now - this.LastUpdate).Milliseconds > 3000)
{
// This updates a textblock to display the count, but could also
// update a progress bar or progress ring in here.
this.MessageTextBlock.Text = "Count: " + lineCount;
this.LastUpdate = now;
}
}
}
Inside of the MyStorageWrapper class:
public static class MyStorageWrapper
{
public delegate void MyEventHandler(long lineCount);
public static event MyEventHandler MyEvent;
private static void RaiseMyEvent(long lineCount)
{
// Ensure that something is listening to the event.
if (MyStorageWrapper.MyEvent!= null)
{
// Call the listening event handlers.
MyStorageWrapper.MyEvent(lineCount);
}
}
public static async Task<List<DataRecord>> GetDataAsync(StorageFile file)
{
List<DataRecord> recordsList = new List<DataRecord>();
using (Stream stream = await file.OpenStreamForReadAsync())
{
using (StreamReader reader = new StreamReader(stream))
{
while (!reader.EndOfStream)
{
string line = reader.ReadLine();
// Does its parsing here, and constructs a single DataRecord …
recordsList.Add(dataRecord);
// Raises an event.
MyStorageWrapper.RaiseMyEvent(recordsList.Count);
}
}
}
return recordsList;
}
}
The code for the time check I got from following this.
As written, this code makes the app unresponsive with a large file (I tested on a text file on the order of about 8.5 million lines). I thought adding async and await to the GetDataAsync() call would prevent this? Does this not do its work on a thread aside from the UI thread? Through Debug mode in Visual Studio, I have verified the program is progressing as expected... it is just tying up the UI thread, making the app unresponsive (see this page from Microsoft about the UI thread and asynchronous programming).
PART 2
I have successfully implemented before an asynchronous, long-running process that runs on a separate thread AND still updates the UI periodically... but this solution does not allow for the return value - specifically the line from PART 1 that says:
this.DataRecords = await MyStorageWrapper.GetDataAsync(pickedFile);
My previous, successful implementation follows (most of the bodies cut out for brevity). Is there a way to adapt this to allow for return values?
In a Page class:
public sealed partial class MyPage : Page
{
public Generator MyGenerator { get; set; }
public MyPage()
{
this.InitializeComponent();
this.MyGenerator = new Generator();
}
private void StartButton_Click(object sender, RoutedEventArgs e)
{
this.MyGenerator.ProgressUpdate += async (s, f) => await Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, delegate ()
{
// Updates UI elements on the page from here.
}
this.MyGenerator.Start();
}
private void StopButton_Click(object sender, RoutedEventArgs e)
{
this.MyGenerator.Stop();
}
}
And in the Generator class:
public class Generator
{
private CancellationTokenSource cancellationTokenSource;
public event EventHandler<GeneratorStatus> ProgressUpdate;
public Generator()
{
this.cancellationTokenSource = new CancellationTokenSource();
}
public void Start()
{
Task task = Task.Run(() =>
{
while(true)
{
// Throw an Operation Cancelled exception if the task is cancelled.
this.cancellationTokenSource.Token.ThrowIfCancellationRequested();
// Does stuff here.
// Finally raise the event (assume that 'args' is the correct args and datatypes).
this.ProgressUpdate.Raise(this, new GeneratorStatus(args));
}
}, this.cancellationTokenSource.Token);
}
public void Stop()
{
this.cancellationTokenSource.Cancel();
}
}
Finally, there are two supporting classes for the ProgressUpdate event:
public class GeneratorStatus : EventArgs
{
// This class can contain a handful of properties; only one shown.
public int number { get; private set; }
public GeneratorStatus(int n)
{
this.number = n;
}
}
static class EventExtensions
{
public static void Raise(this EventHandler<GeneratorStatus> theEvent, object sender, GeneratorStatus args)
{
theEvent?.Invoke(sender, args);
}
}
It is key to understand that async/await does not directly say the awaited code will run on a different thread. When you do await GetDataAsync(pickedFile); the execution enters the GetDataAsync method still on the UI thread and continues there until await file.OpenStreamForReadAsync() is reached - and this is the only operation that will actually run asynchronously on a different thread (as file.OpenStreamForReadAsync is actually implemented this way).
However, once OpenStreamForReadAsync is completed (which will be really quick), await makes sure the execution returns to the same thread it started on - which means UI thread. So the actual expensive part of your code (reading the file in while) runs on UI thread.
You could marginally improve this by using reader.ReadLineAsync, but still, you will be returning to UI thread after each await.
ConfigureAwait(false)
The first trick you want to introduce to resolve this problem is ConfigureAwait(false).
Calling this on an asynchronous call tells the runtime that the execution does not have to return to the thread that originally called the asynchronous method - hence this can avoid returning execution to the UI thread. Great place to put it in your case is OpenStreamForReadAsync and ReadLineAsync calls:
public static async Task<List<DataRecord>> GetDataAsync(StorageFile file)
{
List<DataRecord> recordsList = new List<DataRecord>();
using (Stream stream = await file.OpenStreamForReadAsync().ConfigureAwait(false))
{
using (StreamReader reader = new StreamReader(stream))
{
while (!reader.EndOfStream)
{
string line = await reader.ReadLineAsync().ConfigureAwait(false);
// Does its parsing here, and constructs a single DataRecord …
recordsList.Add(dataRecord);
// Raises an event.
MyStorageWrapper.RaiseMyEvent(recordsList.Count);
}
}
}
return recordsList;
}
Dispatcher
Now you freed up your UI thread, but introduced yet another problem with the progress reporting. Because now MyStorageWrapper.RaiseMyEvent(recordsList.Count) runs on a different thread, you cannot update the UI in the UpdateUI method directly, as accessing UI elements from non-UI thread throws synchronization exception. Instead, you must use UI thread Dispatcher to make sure the code runs on the right thread.
In the constructor get reference to the UI thread Dispatcher:
private CoreDispatcher _dispatcher;
public MyPage()
{
this.InitializeComponent();
_dispatcher = Window.Current.Dispatcher;
...
}
Reason to do it ahead is that Window.Current is again accessible only from the UI thread, but the page constructor definitely runs there, so it is the ideal place to use.
Now rewrite UpdateUI as follows
private async void UpdateUI(long lineCount)
{
await _dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
{
// This time check prevents the UI from updating so frequently
// that it becomes unresponsive as a result.
DateTime now = DateTime.Now;
if ((now - this.LastUpdate).Milliseconds > 3000)
{
// This updates a textblock to display the count, but could also
// update a progress bar or progress ring in here.
this.MessageTextBlock.Text = "Count: " + lineCount;
this.LastUpdate = now;
}
});
}

Method blocking UI thread

I have recently created a helper method to throttle the speed of property changed event in order to save the data to my local database with a given delay. The only problem I got with it is that the method is apparently not running asynchronously and blocking my UI thread.
To test it, I have attached the event in my SettingsViewModel to the PercentageSliderViewModel:
public class SettingsViewModel : BaseVM
{
public PercentageSliderViewModel Activity { get; set; }
[...]
public SettingsViewModel() {
Activity.PropertyChanged += CreateThrottledCallback(Save, 1000);
}
[...]
public async Task Save()
{
// Save data
}
}
This is my helper method:
public PropertyChangedEventHandler CreateThrottledCallback(
Func<Task> callback, int throttle = 1000)
{
bool throttling = false;
bool callFinal = false;
return async(s, e) =>
{
if (throttling)
{
callFinal = true;
return;
}
throttling = true;
await callback?.Invoke();
await Task.Delay(throttle).ContinueWith(_ => throttling = false);
if (callFinal)
{
await callback?.Invoke().ContinueWith(_ => callFinal = false);
}
};
}
Even though the throttling is working fine, when I move the slider from left to right and the callback occurs it is 'freezing' for a small amount of time.
Debugging shows that it is running on the UI thread.
How can run CreateThrottledCallback method asynchronously so it wont block my main thread then?
It's unclear what your Save method really does and whether it's actually a truly asynchronous method. You could try to execute it an a background thread in your CreateThrottledCallback method:
if (callback != null)
await Task.Run(() => callback.Invoke());
As it has nothing to do with UI, you can use ConfigureAwait(false). This will avoid capturing the current synchronization context and the task will run on TaskScheduler.Default thread pool context.
await callback().ConfigureAwait(false);

Return a Task from a method with type void

I would like to create a task to run serial commands on. At this time I do not need to return anything from the method that is doing the work. This will probably change later, but I am now curious as to how this.
This is what I have. I would like to use a separate method for the task instead of creating an anonymous action. I have tried returning void, with the result of "void can not be explicitly converted to a Task". I have also tried. Task<void>. The Last thing I have tried is returning a Task, but I receive, error "Not all Code paths return a value" and "Can not implicily convert void to type task"
In the pass I have used a Thread to accomplish this, but I'd like to use Tasks this time around.
internal class Hardware
{
private EventHandler<SequenceDoneEventArgs> SequenceDone;
private List<Step> Steps;
private System.IO.Ports.SerialPort comport = null;
private Task SequenceTask;
private CancellationTokenSource RequestStopSource;
private CancellationToken RequestStopToken;
private void Initialize()
{
comport = new System.IO.Ports.SerialPort("COM2", 115200, System.IO.Ports.Parity.None,8);
comport.DataReceived += Comport_DataReceived;
}
public async void RunSequence()
{
if (comport == null)
{
Initialize();
}
if (!comport.IsOpen)
{
comport.Open();
}
RequestStopSource = new CancellationTokenSource();
RequestStopToken = RequestStopSource.Token;
SequenceTask = await Task.Run(() => { doSequence(); });
}
private Task doSequence()
{
//** Run Sequence stuff here
}
}
ETA:
In the end this is my the complete solution
internal class Hardware
{
private EventHandler<SequenceDoneEventArgs> SequenceDone;
private List<Step> Steps;
private System.IO.Ports.SerialPort comport = null;
private Task SequenceTask;
private CancellationTokenSource RequestStopSource;
private CancellationToken RequestStopToken;
private void Initialize()
{
comport = new System.IO.Ports.SerialPort("COM2", 115200, System.IO.Ports.Parity.None,8);
comport.DataReceived += Comport_DataReceived;
}
public async void RunSequence()
{
if (comport == null)
{
Initialize();
}
if (!comport.IsOpen)
{
comport.Open();
}
RequestStopSource = new CancellationTokenSource();
RequestStopToken = RequestStopSource.Token;
SequenceTask = await Task.Factory.StartNew(async () => { await doSequence(); });
}
private Task doSequence()
{
//** Run Sequence stuff here
//return null;
return Task.CompletedTask;
}
}
Just mark doSequence as async (assuming it uses await):
private async Task doSequence()
Also, it's a good idea to return this Task in the delegate you pass to Task.Run:
SequenceTask = await Task.Run(() => doSequence());
I would like to create a task to run serial commands on.
This leads me to believe that using async and Task may not be the best solution for your scenario. I suggest you look into TPL Dataflow.
SequenceTask = await Task.Factory.StartNew(async() => { await doSequence(); });
Also your RunSequence() should return Task instead of void.
Actually if you await the Task this should result in the same:
SequenceTask = await doSequence();

Categories

Resources