0

I want to do a simple thing (assuming that ContinueWith is thread-safe):

readonly Task _task = Task.CompletedTask;

// thread A: conditionally prolong the task
if(condition)
    _task.ContinueWith(o => ...);

// thread B: await for everything
await _task;

Problem: in above code await _task immediately returns, disregards if there are inner tasks.

Extending requirement that _task can be prolonged by multiple ContinueWith, how would I await for all of them to finish?


Of course I can try to do it in old thread-safe way:

Task _task = Task.CompletedTask;
readonly object _lock = new object();

// thread A
if(condition)
    _lock(_lock)
        _task = _task.ContinueWith(o => ...); // storing the latest inner task

// thread B
lock(_lock)
    await _task;

Or by storing tasks in thread-safe collection and using Task.WaitAll.

But I was curious if there is an easy fix to a first snippet?

Sinatr
  • 20,892
  • 15
  • 90
  • 319
  • 2
    You have to await the task *returned* from the call to `ContinueWith()`, so you would have to do something like `_task = _task.ContinueWith(o => ...);` (You don't need to put a lock around assigning a reference by the way - that's an atomic operation if that's all you're doing.) – Matthew Watson Nov 18 '19 at 10:00
  • @MatthewWatson, *"You don't need to put a lock"*.. ow.. good I decide to ask. Was hoping for some *magic* with tasks, but without `lock` second snippet is same good. – Sinatr Nov 18 '19 at 10:03
  • 1
    You can specify `TaskCreationOptions.AttachedToParent` in your `ContinueWith` call, to make the parent task complete only when the child task completes. – canton7 Nov 18 '19 at 10:15
  • 1
    You might want to run Continuation with the `TaskContinuationOptions.ExecuteSynchronously` parameter. – Alexander Petrov Nov 18 '19 at 10:15
  • 1
    And you absolutely *do* need a lock when assigning to `_task`, since `_task` is written from one thread and read from another. Otherwise Thread B might read the value of the `_task` field before Thread A re-assigns it, and might cache that value even after Thread A has re-assigned it. – canton7 Nov 18 '19 at 10:17
  • 1
    If possible, the way using `await` is neater: `async Task Prolong() { if (condition) await ...; }` then `_task = Prolong()` and `await _task`. – canton7 Nov 18 '19 at 10:19
  • @canton7 No, putting a lock just around a reference assignment does nothing, since a reference assignment is an atomic operation. – Matthew Watson Nov 18 '19 at 10:22
  • 1
    @MatthewWatson It inserts a memory barrier. See [Eric Lippert](http://web.archive.org/web/20160304015304/http://blog.coverity.com/2014/03/12/can-skip-lock-reading-integer/) – canton7 Nov 18 '19 at 10:22
  • 1
    @canton7 A memory barrier isn't needed for atomic reads and writes though - there's no danger of a torn read. For a lock to be useful here it should be placed around more than a simple reference assignment. The lock as written doesn't help with synchronising thread A and thread B at all. – Matthew Watson Nov 18 '19 at 10:24
  • 1
    @MatthewWatson The danger here isn't tearing. The danger here is re-ordering. Without the lock, Thread A is allowed to re-order the write to `_task` as late as it likes, and Thread B is allowed to re-order the read of `_task` as early as it likes. I'm assuming that OP has some logic to ensure that Thread B only checks `_task` after Thread A has written to it, but without a memory barrier their logic might not hold. See the link in my previous comment for an expert's opinion on why you should never elide locks around field accesses (I fixed the broken link in my previous comment) – canton7 Nov 18 '19 at 10:27
  • @canton7 I suppose I wasn't very clear. The issue here is that adding the lock as per the second code snippet doesn't fix the race condition. Thread `B` could acquire the lock before thread `A` does, in which case it will await the task before it was continued. Then when the `Thread B` finishes the await and releases the lock, thread `A` will enter the lock and issue the continuation - which will never be awaited. – Matthew Watson Nov 18 '19 at 10:39
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/202558/discussion-between-canton7-and-matthew-watson). – canton7 Nov 18 '19 at 10:41
  • 1
    (We agreed that your code, as it stands, has a race condition (where Thread B reads `_task` before Thread A writes to it) which the lock doesn't help to solve, but that locks are in general needed when accessing fields from multiple threads, even when those fields can't tear, to prevent re-ordering) – canton7 Nov 18 '19 at 10:48
  • *disregards if there are inner tasks* These are not called inner tasks, they are called continuations. – Theodor Zoulias Nov 18 '19 at 11:23
  • Without an explanation, it would be a bad answer. I don't speak much English. Feel free to write the answer yourself. – Alexander Petrov Nov 18 '19 at 11:40
  • The `Task` objects are [thread safe](https://docs.microsoft.com/en-us/dotnet/api/system.threading.tasks.task#thread-safety), in the sense that you can access them from multiple threads concurrently without fear that their internal state will become corrupted. It doesn't mean that a single `Task` variable can be shared by multiple threads without synchronization. A variable is not an object, is a reference to an object, and the same variable can point to different objects at different times. Synchronizing access to shared state is vital for ensuring the correctness of a multithreaded program. – Theodor Zoulias Nov 18 '19 at 11:45
  • @AlexanderPetrov, I was wrong, `ExecuteSynchronously` is not an option. It will block A, which is totally unwanted effect. – Sinatr Nov 18 '19 at 14:12

2 Answers2

2

Your old version is OK, except await while keeping the lock; you should copy that _task to a local variable while holding the lock, release the lock, and only then await

But that ContinueWith workflow is IMO not the best way to implement that logic. ContinueWith was introduced in .NET 4.0, before async-await became part of the language.

In modern C#, I think it’s better to do something like this:

static async Task runTasks()
{
    if( condition1 )
        await handle1();
    if( condition2 )
        await handle2();
}
readonly Task _task = runTasks();

Or if you want them to run in parallel:

IEnumerable<Task> parallelTasks()
{
    if( condition1 )
        yield return handle1();
    if( condition2 )
        yield return handle2();
}
readonly Task _task = Task.WhenAll( parallelTasks() );

P.S. If the conditions are changing dynamically, for the sequential version you should copy their values to local variables, before the first await.

Update: So you are saying your thread A prolongs that task in response to some events which happen dynamically? If yes, your old method is OK, it’s simple and it works, just fix that lock(){ await ... } concurrency bug.

Theoretically, the cleaner approach might be something like a reactor pattern, but it’s way more complicated unfortunately. Here’s my open source example of something slightly similar. You don’t need the concurrency limit semaphore nor the second queue, but you need to expose a property of type Task, initialize it with CompletedTask, replace with a new task created by TaskCompletionSource<bool> from the post method when the first one is posted, and complete that task in runAsyncProcessor method once there’re no more posted tasks.

Soonts
  • 20,079
  • 9
  • 57
  • 130
  • Organizing logic in dedicated method is clever design indeed (+1). But in this case I'd have to add more members to class to be able to evaluate conditions inside single method. My initial idea was to update `_task` inside A directly (without having to store `_wasAOk` or something) to check later only `_task` inside B with `await`. – Sinatr Nov 18 '19 at 11:32
-1

In your original code await _task; returns immediately because task is completed. You need to do

_task = _task.ContinueWith(...);

You don't have to use locks. You are working in the same thread. In your "locks" version of the code, you do use _task = _task.ContinueWith(...) and that is why it works, not because of the locks.


EDIT: Just saw that you want to do _task = _task.ContinueWith(...); in one thread and _task = _task.ContinueWith(...).

This is quite bad as design. You should never combine threads and tasks. You should either go for a task-only solution, or for a thread-only solution.

If you go for a task-only solution (recommended), just create a good sequence of work tasks and then on each task decide how to proceed based on the result of the previous task(s).

If you go for a thread-only solution, you might want to use ManualResetEvent handles to synchronize your tasks.

Nick
  • 4,787
  • 2
  • 18
  • 24
  • 1
    Op is *not* "working in the same thread" -- `_tasks` is written from Thread A, and read from Thread B – canton7 Nov 18 '19 at 10:23
  • The first paragraph is summary of comments, which I would be ok to upvote. But I am disagree with every sententence of second paragraph. Any reference to *"never combine threads and tasks"*? Btw, when saying another thread I don't mean specifically creating thread, it could be an UI event adding something or another task. – Sinatr Nov 18 '19 at 11:39
  • @Sinatr, you have right to disagree, not meaning you are correct. _"never combine threads and tasks"_ - tasks can continue on any thread, even `await` can continue on another thread if there is no sync context. Do you start now to understand what I mean? Again, the solid approach is to build a proper sequence of tasks, not make a monster spaghetti. – Nick Nov 18 '19 at 12:00