3

Supposing a Task is created and awaited multiple times from a single thread. Is the resume order FIFO?

Simplistic example: Is the Debug.Assert() really an invariant?

Task _longRunningTask; async void ButtonStartSomething_Click() { // Wait for any previous runs to complete before starting the next if (_longRunningTask != null) await _longRunningTask; // Check our invariant Debug.Assert(_longRunningTask == null, "This assumes awaits resume in FIFO order"); // Initialize _longRunningTask = Task.Delay(10000); // Yield and wait for completion await _longRunningTask; // Clean up _longRunningTask = null; } 

Initialize and Clean up are kept to a bare minimum for the sake of simplicity, but the general idea is that the previous Clean up MUST be complete before the next Initialize runs.

3
  • If it isn't single thread, it is definitely not completed/resumed in FIFO order, but since the question is on single thread.... do you try it out? Commented May 26, 2016 at 3:07
  • 4
    I don't see anything in the documentation that requires FIFO completion notifications. I would make _longRunningTask be the Task.Delay(10000) followed by a continuation that nulls out the _longRunningTask. That way, you are guaranteed that awaiting the _longRunningTask will not complete until the variable is nulled out. Commented May 26, 2016 at 3:10
  • Even if you test and get reliable FIFO continuations 100% of the time in your WinForms or WPF app, this is an unreasonable risk to take. What if someone takes your code and integrates it into a lib that can run in a console app or ASP.NET? What if they notice that your async chain includes a long-running blocking operation and wrap the whole thing in Task.Run? You're introducing a lot of brittleness just to save yourself a simple SemaphoreSlim.WaitAsync()/Release() combo. Commented May 26, 2016 at 3:37

3 Answers 3

2

The short answer is: no, it's not guaranteed.

Furthermore, you should not use ContinueWith; among other problems, it has a confusing default scheduler (more details on my blog). You should use await instead:

private async void ButtonStartSomething_Click() { // Wait for any previous runs to complete before starting the next if (_longRunningTask != null) await _longRunningTask; _longRunningTask = LongRunningTaskAsync(); await _longRunningTask; } private async Task LongRunningTaskAsync() { // Initialize await Task.Delay(10000); // Clean up _longRunningTask = null; } 

Note that this could still have "interesting" semantics if the button can be clicked many times while the tasks are still running.

The standard way to prevent the multiple-execution problem for UI applications is to disable the button:

private async void ButtonStartSomething_Click() { ButtonStartSomething.Enabled = false; await LongRunningTaskAsync(); ButtonStartSomething.Enabled = true; } private async Task LongRunningTaskAsync() { // Initialize await Task.Delay(10000); // Clean up } 

This forces your users into a one-operation-at-a-time queue.

Sign up to request clarification or add additional context in comments.

1 Comment

Also, if splitting up a function is unsightly (e.g. if there're lots of variables to bring across), an alternative is using anonymous functions: _longRunningTask = ((Func<Task>)(async () => { await Task.Delay(10); }))();
2

The order of execution is pre-defined, however there is potential race condition on _longRunningTask variable if ButtonStartSomething_Click() is called concurrently from more than one thread (not likely the case).

Alternatively, you can explicitly schedule tasks using a queue. As a bonus a work can be scheduled from non-async methods, too:

void ButtonStartSomething_Click() { _scheduler.Add(async() => { // Do something await Task.Delay(10000); // Do something else }); } Scheduler _scheduler; class Scheduler { public Scheduler() { _queue = new ConcurrentQueue<Func<Task>>(); _state = STATE_IDLE; } public void Add(Func<Task> func) { _queue.Enqueue(func); ScheduleIfNeeded(); } public Task Completion { get { var t = _messageLoopTask; if (t != null) { return t; } else { return Task.FromResult<bool>(true); } } } void ScheduleIfNeeded() { if (_queue.IsEmpty) { return; } if (Interlocked.CompareExchange(ref _state, STATE_RUNNING, STATE_IDLE) == STATE_IDLE) { _messageLoopTask = Task.Run(new Func<Task>(RunMessageLoop)); } } async Task RunMessageLoop() { Func<Task> item; while (_queue.TryDequeue(out item)) { await item(); } var oldState = Interlocked.Exchange(ref _state, STATE_IDLE); System.Diagnostics.Debug.Assert(oldState == STATE_RUNNING); if (!_queue.IsEmpty) { ScheduleIfNeeded(); } } volatile Task _messageLoopTask; ConcurrentQueue<Func<Task>> _queue; static int _state; const int STATE_IDLE = 0; const int STATE_RUNNING = 1; } 

Comments

1

Found the answer under Task.ContinueWith(). It appear to be: no

Presuming await is just Task.ContinueWith() under the hood, there's documentation for TaskContinuationOptions.PreferFairness that reads:

A hint to a TaskScheduler to schedule task in the order in which they were scheduled, so that tasks scheduled sooner are more likely to run sooner, and tasks scheduled later are more likely to run later.

(bold-facing added)

This suggests there's no guarantee of any sorts, inherent or otherwise.

Correct ways to do this

For the sake of someone like me (OP), here's a look at the more correct ways to do this.

Based on Stephen Cleary's answer:

private async void ButtonStartSomething_Click() { // Wait for any previous runs to complete before starting the next if (_longRunningTask != null) await _longRunningTask; // Initialize _longRunningTask = ((Func<Task>)(async () => { await Task.Delay(10); // Clean up _longRunningTask = null; }))(); // Yield and wait for completion await _longRunningTask; } 

Suggested by Raymond Chen's comment:

private async void ButtonStartSomething_Click() { // Wait for any previous runs to complete before starting the next if (_longRunningTask != null) await _longRunningTask; // Initialize _longRunningTask = Task.Delay(10000) .ContinueWith(task => { // Clean up _longRunningTask = null; }, TaskContinuationOptions.OnlyOnRanToCompletion); // Yield and wait for completion await _longRunningTask; } 

Suggested by Kirill Shlenskiy's comment:

readonly SemaphoreSlim _taskSemaphore = new SemaphoreSlim(1); async void ButtonStartSomething_Click() { // Wait for any previous runs to complete before starting the next await _taskSemaphore.WaitAsync(); try { // Do some initialization here // Yield and wait for completion await Task.Delay(10000); // Do any clean up here } finally { _taskSemaphore.Release(); } } 

(Please -1 or comment if I've messed something up in either.)

Handling exceptions

Using continuations made me realize one thing: awaiting at multiple places gets complicated really quickly if _longRunningTask can throw exceptions.

If I'm going to use continuations, it looks like I need to top it off by handling all exceptions within the continuation as well.

i.e.

_longRunningTask = Task.Delay(10000) .ContinueWith(task => { // Clean up _longRunningTask = null; }, TaskContinuationOptions.OnlyOnRanToCompletion); .ContinueWith(task => { // Consume or handle exceptions here }, TaskContinuationOptions.OnlyOnFaulted); // Yield and wait for completion await _longRunningTask; 

If I use a SemaphoreSlim, I can do the same thing in the try-catch, and have the added option of bubbling exceptions directly out of ButtonStartSomething_Click.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.