Intro

Some time ago I attended a job interview (as a candidate!). After some chit-chat and non-technical questions, I got to the technical part. They presented a couple of code fragments and asked me what the output will be.

I personally think this type of questions suck. You don’t have a computer? Just run the damn thing, and you’ll know, why do you need to ask me. But that’s a rant for another time.

Anyway, I answered most of the questions, but one got me pretty angry. The code used Tasks and Thread.Sleep in combination. I thought I knew these topics pretty well, until that day.

After the interview, I decided to research this topic a bit more, and I’ll tell you what I found out!

The code

number

Firstly, we have this small Number class, which just stores an int value.

do the math

Then we have this AsyncTask class with DoTheMath method. It also has DoubleValue method, which is marked as async and return a Task. Cool, cool.

main

Lastly, we have this Main, where we just run the DoTheMath method.

The question

The interviewer asked me what number will be printed out, after we run this program. Running it? What do you mean? This code is so bad, that before running it I’d definitely refactor it first.

There are a couple of things wrong in this code.

  • We use Thread.Sleep(3000) in an async method.

    This blocks the current thread, preventing it from doing some other useful work in the meantime.

    If you need to execute some logic after given time, in an async method you should use Task.Delay method, which doesn’t block the thread.

  • This DoubleValue method is not awaited.

    not awaited

    It could result in the program finishing, before the method fully completes.

    Always use await keyword when working with async methods. If you need 'Fire and Forget' mechanism, take a look at this SafeFireAndForget method from this AsyncAwaitBestPractices nuget package.

But, let’s assume we cannot change this code. What was my answer then?

I don’t know, maybe the program would print 8. I’m not sure on which thread Thread.Sleep will run. I would have to run it to fully understand it.

So let’s run it!

Running the code

Attempt 1

Firstly let’s run the code as it is, without debugging.

a1

Okay, 11 it is. The first number, 3 got doubled, and 5 did not. 6 + 5 is 11.

Makes sense I guess.

Attempt 2

Now, let’s run it with debugger attached.

a2

Now it’s 16, so both numbers got doubled.

Attempt 3 (Bonus)

I assume this weirdness has to do something with the Thread.Sleep, so let’s remove it and run the code 100 times without the debugger.

a3

Okay, now it’s 8 or 11 or sometimes even 16.

r4

This one seems almost totally random.

My attempt at explanation

Async/Await talk

I won’t be giving you a full deep dive on how async await works inside. I’ll only explain what’s needed for this post. For more information, check out this talk from NDC conference.

How the code really looks

Firstly, let’s see how our code looks after it’s 'compiled'. We’ll paste it into this website: https://sharplab.io and read the generated version.

This is all the code generated for the DoubleValue method.

decomp

I know it’s a lot, but bear with me.

Reading from the top, we have a new class generated for this method, which is a state machine. It is used to keep track of the state of this async method. The state is represented by the num variable. In the first iteration of this state machine, we schedule some work to be run on something called ThreadPool, and return the task. Thanks to this state machine, the runtime will know where to jump back in the method, when the scheduled work is finished. By where I mean the else block in our example.

Attempt 1 explained

Now that we know how it looks on the inside, we could try to reason why it works as it works.

Let’s see how it works step by step.

CodeSteps
code
do the math compiled
code compiled
  1. We enter the DoTheMath method.

  2. We create two numbers, one with value 3, and one with 5.

  3. We enter the DoubleValue method.

    1. We set the state of the state machine to -1 and start it.

    2. In MoveNext we enter the if because num != 0.

    3. We sleep for 3 seconds in the main thread.

    4. Using Task.Run we schedule some work to ThreadPool (probably a different thread).

    5. Since the work is not completed yet we return from MoveNext.

    6. And lastly we return a Task.

  4. We enter another DoubleValue and do the same steps (including waiting 3 seconds).

  5. We print the sum of the numbers.

The important observation is that this second Thread.Sleep(3000) guarantees that the first action of doubling completes before printing the sum. The second action is not awaited in any way, so it doesn’t have the chance to finish before printing the sum. And that’s why we see 11.

Attempt 2

Okay but why when the debugger is attached we see 16? Well, I’m not sure to be honest. I can only assume that it’s because the debugger makes the program slower. When it’s slowed down, the second doubling action has the time to finish on a separate thread, before we print the number to the console.

Attempt 3 explained

Without the Thread.Sleep, we basically have a race condition between first doubling, second doubling, and printing to the console. The order of them finishing is quite random, so the result is also quite random.

Conclusion

Despite not liking this task as a job interview question, I enjoyed digging deeper into async programming because of it. And I do recommend attending job interview for this sole purpose, even if you’re not looking to change your job any soon!