Fill in the blanks: an async exercise

Spread the love

A Task-based async operation can either complete synchronously (i.e. return a completed Task right away) or asynchronously (the more common case). Given two sequentially awaited operations, there can thus be four possible behavior combinations:

Task 1 Task 2
sync sync
sync async
async sync
async async

Here is a class that implements this execution pattern:

        public sealed class TwoTasks
        {
            private readonly IList<string> seq;
            private readonly TaskCompletionSource<bool> t1;
            private readonly TaskCompletionSource<bool> t2;

            public TwoTasks(IList<string> seq)
            {
                this.seq = seq;
                this.t1 = new TaskCompletionSource<bool>();
                this.t2 = new TaskCompletionSource<bool>();
            }

            public void Complete1() => this.t1.SetResult(true);

            public void Complete2() => this.t2.SetResult(true);

            public async Task DoAsync()
            {
                this.seq.Add("start");

                await this.t1.Task;

                this.seq.Add("middle");

                await this.t2.Task;

                this.seq.Add("end");
            }
        }

And now four unit tests, one for each completion combination, with blanked out assertions left as an exercise for the reader.

        [Fact]
        public void SyncThenSyncTest()
        {
            List<string> seq = new List<string>();
            TwoTasks t = new TwoTasks(seq);

            t.Complete1();
            t.Complete2();

            Task task = t.DoAsync();

            TaskShouldBe(task, /* #1 */);
            SeqShouldBe(seq, /* #2 */);
        }

        [Fact]
        public void SyncThenAsyncTest()
        {
            List<string> seq = new List<string>();
            TwoTasks t = new TwoTasks(seq);

            t.Complete1();

            Task task = t.DoAsync();

            TaskShouldBe(task, /* #3 */);
            SeqShouldBe(seq, /* #4 */);

            t.Complete2();

            TaskShouldBe(task, /* #5 */);
            SeqShouldBe(seq, /* #6 */);
        }

        [Fact]
        public void AsyncThenSyncTest()
        {
            List<string> seq = new List<string>();
            TwoTasks t = new TwoTasks(seq);

            t.Complete2();

            Task task = t.DoAsync();

            TaskShouldBe(task, /* #7 */);
            SeqShouldBe(seq, /* #8 */);

            t.Complete1();

            TaskShouldBe(task, /* #9 */);
            SeqShouldBe(seq, /* #10 */);
        }

        [Fact]
        public void AsyncThenAsyncTest()
        {
            List<string> seq = new List<string>();
            TwoTasks t = new TwoTasks(seq);

            Task task = t.DoAsync();

            TaskShouldBe(task, /* #11 */);
            SeqShouldBe(seq, /* #12 */);

            t.Complete1();

            TaskShouldBe(task, /* #13 */);
            SeqShouldBe(seq, /* #14 */);

            t.Complete2();

            TaskShouldBe(task, /* #15 */);
            SeqShouldBe(seq, /* #16 */);
        }

        private static void SeqShouldBe(IList<string> seq, params string[] expected)
        {
            seq.Should().ContainInOrder(expected).And.HaveCount(expected.Length);
        }

        private static void TaskShouldBe(Task task, TaskStatus expected)
        {
            task.Status.Should().Be(expected);
        }

Now for your challenge: what values should appear in each of the assertions? Read on for the answers…

.

.

.

.

.

.

.

Sync + Sync

#1 – TaskStatus.RanToCompletion
#2 – "start", "middle", "end"

Recall that in an async method, execution proceeds “up until the first await expression on an awaitable instance that has not yet completed, at which point the invocation returns to the caller.” [MSDN] In this case, we have two tasks that have already run to completion, so the method will return back to the caller having fully completed.

Sync + Async

#3 – TaskStatus.WaitingForActivation
#4 – "start", "middle"
#5 – TaskStatus.RanToCompletion
#6 – "start", "middle", "end"

The status of a pending async operation based on TaskCompletionSource would be WaitingForActivation. (Refer to Stephen Toub’s post “The meaning of TaskStatus” for more details.) In this case, the first task still completes synchronously but the second task is pending. Accordingly, control is transferred back to the test method while having already finished the first step. Later, when Complete2 is called, the entire operation finishes.

Async + Sync

#7 – TaskStatus.WaitingForActivation
#8 – "start"
#9 – TaskStatus.RanToCompletion
#10 – "start", "middle", "end"

Since the first task has not completed, only the “start” step will have been executed by the time control is transferred back to the caller. Since the second task already completed synchronously, completing the first task is enough to complete the entire method.

Async + Async

#11 – TaskStatus.WaitingForActivation
#12 – "start"
#13 – TaskStatus.WaitingForActivation
#14 – "start", "middle"
#15 – TaskStatus.RanToCompletion
#16 – "start", "middle", "end"

Based on the previous answers, you can probably guess what happens here. Completing the first task moves us from “start” to “middle.” Completing the second task then completes the entire operation.

.

.

.

.

.

How did you fare on this async quiz? Give yourself an “A for effort” if you read this far (and an A+ if you got the right answers).

Leave a Reply

Your email address will not be published. Required fields are marked *