0

var task = Task.Run(() => DoSomeStuff()).Result;

What happens here under the hood?

I made a tiny test:

using System;
using System.Threading.Tasks;

public class Program
{
    public static void Main()
    {
        var r = Task.Run( () => {Thread.Sleep(5000); return 123; }).Result;
        Console.WriteLine(r);
    }
}

It prints "123" after 5s. So does accessing any such property on Task act as a shortcut to calling Task.Wait() i.e. is this safe to do?


Previously my code called Task.Delay(5000) which returned "123" immediately. I fixed this in my question but leave this here as comments and answers reference it.

Mr. Boy
  • 60,845
  • 93
  • 320
  • 589
  • 1
    It blocks as if `.Wait()` was called. The test never *awaits* for `Task.Delay()`. No, accessing any property isn't a shortcut to `Wait()`. The behaviour of `Result` and `Wait()` are well defined - `Wait()` blocks when we don't want any results. `Result` blocks and returns the result – Panagiotis Kanavos Feb 05 '20 at 12:34
  • 1
    The important point that has not been made in an answer yet is: **Never use either Wait or Result unless you know that the task has already completed**. It is very easy to get into a situation where the task has scheduled work *onto the thread that you just put asleep*, and now the task will never complete and the thread will never wake up because the worker that will work on the task is sleeping until it is finished! Asynchrony is designed to be *asynchronous*; forcing it to be synchronous is dangerous. – Eric Lippert Feb 05 '20 at 16:07
  • 1
    @EricLippert only wait for things that have already happened? This seems rather unintuitive! – Mr. Boy Feb 05 '20 at 16:13
  • 1
    That comment really should be an answer. Will add one. – Eric Lippert Feb 05 '20 at 16:13

3 Answers3

6

You asked two questions. First, does accessing Result implicitly cause a synchronous Wait? Yes. But the more important question is:

is this safe to do?

It is not safe to do this.

It is very easy to get into a situation where the task that you are synchronously waiting on has scheduled work to run in the future before it completes onto the thread that you just put to sleep. Now we have a situation where the thread will not wake up until the sleeping thread does some work, which it never does because it is asleep.

If you already know that the task is complete then it is safe to synchronously wait for the result. If you do not, then it is not safe to synchronously wait.

Now, you might say, suppose I know through some other means that it is safe to wait synchronously for an incomplete task. Then is it safe to wait? Well, by the assumption of the question, yes, but it still might not be smart to wait. Remember, the whole point of asynchrony is to manage resources efficiently in a world of high latency. If you are synchronously waiting for an asynchronous task to complete then you are forcing a worker to sleep until another worker is done; the sleeping worker could be doing work! The whole point of asynchrony is to avoid situations where workers go idle, so don't force them to.

An await is an asynchronous wait. It is a wait that means "wait on running the rest of the current workflow until after this task is done, but find something to do while you are waiting". We did a lot of work to add it to the language, so use it!

Eric Lippert
  • 647,829
  • 179
  • 1,238
  • 2,067
  • I'm still trying to get my head around this, is a trivial example possible of how I can easily get into the situation you describe? – Mr. Boy Feb 05 '20 at 16:30
  • 1
    @Mr.Boy: Absolutely there is. Stephen gives some examples here: https://blog.stephencleary.com/2012/07/dont-block-on-async-code.html In general if you want cogent explanations for bad patterns in async, Stephen should be your go-to. – Eric Lippert Feb 05 '20 at 16:31
  • Thanks. I think I've read that before and I still don't understand it. I simply can't get my head around all this `async`/`await` stuff which is why I am using `Task` but deliberately _not_ using `await` only `Wait` because I feel like that's where I lose the thread (!) and I do not think it wise to write to a paradigm I cannot properly understand – Mr. Boy Feb 05 '20 at 16:48
  • 1
    @Mr.Boy the async/await is a complex technology with a simple interface. Hard work has been invested to make it behave correctly with the least effort possible. Just avoid all the fancy stuff. Avoid `async void`, avoid `Wait` and `Result`, avoid `ContinueWith`, avoid `Task.Run`, avoid fire and forget tasks, forget about `TaskScheduler`s, and just focus on using plain vanilla async/await everywhere that the platform provides you with a built-in async method. You'll notice that the right thing will happen automagically. – Theodor Zoulias Feb 05 '20 at 17:04
  • 1
    @Mr.Boy: You are 100% correct that you should not use this sharp tool until you have a good understanding of it. You are 100% wrong that trying to force an asynchronous workflow to become synchronous is the right thing to do in that situation. Learn how asynchrony works and then embrace it; don't take a hammer to it to force it to work like synchronous code. – Eric Lippert Feb 05 '20 at 18:33
  • @TheodorZoulias except now any code I produce cannot be used in our company-wide C# codebase because that predates `async` and like the GPL, `async` can't neatly be contained (as far as I understand it). – Mr. Boy Feb 05 '20 at 19:04
  • @EricLippert well I can write async code with threads and I thought I could with `Task` but it seems MS have decided all those things are now _bad_ and I have to learn a whole new paradigm in `async`. Is all the C# code everyone wrote previously suddenly wrong? It seems weird to me to force everyone to try and write asynchronous _programs_ when most programs are not asynchronous, but do a few things asynchronously. Like we're forcing everyone to learn how to write every application like it's a massively-scaling big-data, when most are not. – Mr. Boy Feb 05 '20 at 19:12
  • @EricLippert sure but do I actually need to use `async` at all? Is it not reasonable to use "classic Tasks" and eschew all this `await` stuff especially in an application which is _not_ inherently asynchronous and definitely isn't facing scale issues? Bearing in mind I come from a background of using `Thread` it seems a huge shift in thinking and I can't just take time out from work to learn this for a few days :) – Mr. Boy Feb 05 '20 at 19:20
  • We added asynchronous workflows to C# not in an effort to make your life harder, but rather to hand you a tool that actually solves the problems that real developers have. **Adding more workers does not efficiently manage high-latency workflows**; ten bakers can't bake a cake in five minutes. Threads are *workers*, which is the wrong level to manage *high-latency tasks*. – Eric Lippert Feb 05 '20 at 19:24
  • Now, as for your observation that "most programs are not asynchronous but do a few things asynchronously", that observation is a key error. The correct observation is "a program that does *anything* asynchronously is an asynchronous program and should be architected as one". There's no "a little bit asynchronous" any more than there is "a little bit pregnant"; either the workflow is asynchronous or it is not. – Eric Lippert Feb 05 '20 at 19:26
  • 2
    @Mr.Boy if you are comfortable with old school continuations (`ContinueWith`) and uncomfortable with async/await magic, then by all means you can keep using the old stuff. They are still working as well as ever. Just avoid mixing them with the new stuff. Combining classic TPL techniques with async/await is a recipe for frustration and problems. – Theodor Zoulias Feb 05 '20 at 20:51
  • Does this mean that a general solution to the problem of sync-over-async is to schedule a timer that periodically checks the state of a task and perform the continuation when the task is completed? Actively monitoring `Task.IsCompleted` seems to resolve the problem of calling asynchronous methods in synchronous code, then. – Luca Cremonesi Feb 10 '20 at 17:28
3

So does accessing any such property on Task act as a shortcut to calling Task.Wait()?

Yes.

From the docs:

Accessing the [Result] property's get accessor blocks the calling thread until the asynchronous operation is complete; it is equivalent to calling the Wait method.

However, your test doesn't do what you think it does.

Task.Delay(..) returns a Task which completes after the specified amount of time. It doesn't block the calling thread.

So () => { Task.Delay(5000); return 123; } simply creates a new Task (which will complete in 5 seconds), then throws it away and immediately returns 123.

You can either:

  1. Block the calling thread, by doing Task.Delay(5000).Wait() (which does the same thing as Thread.Sleep(5000))
  2. Asynchronously wait for the Task returned from Task.Delay to complete: Task.Run(async () => { await Task.Delay(5000); return 123; })
canton7
  • 37,633
  • 3
  • 64
  • 77
  • A neat feature to run and wait on a Task as a 1-liner then. Thanks for the good answer – Mr. Boy Feb 05 '20 at 15:35
  • True, it's mainly for setting up a `Task` oriented code-base and testing with the simple cases. I was trying to think if it makes _any_ difference? – Mr. Boy Feb 05 '20 at 15:44
2

The test doesn't wait for Task.Delay() so it returns immediatelly. It should be :

var r = Task.Run(async  () => { await Task.Delay(5000); return 123; }).Result;

The behavior of Result is well defined - if the task hasn't completed, it blocks until it does. Accessing other Task properties doesn't block

Panagiotis Kanavos
  • 120,703
  • 13
  • 188
  • 236
  • Yeah I was just trying to make a `Task` that simulated something taking a few seconds. – Mr. Boy Feb 05 '20 at 15:16
  • That's what `Task.Delay()` does. You have to await that task to observe that delay though – Panagiotis Kanavos Feb 05 '20 at 15:18
  • seems an overly complicated way to do that though rather than `Thread.Sleep()`? – Mr. Boy Feb 05 '20 at 15:34
  • 2
    @Mr.Boy: Never call `Thread.Sleep` with a non-zero argument for anything except test cases that simulate work, like you are doing here. Certainly never use it in production code to institute a delay. If you had a workflow in a real office with real workers, you would never say "This mail should go out on the first of next month. Mary, fall asleep and set an alarm for next month and then send the mail; we'll keep paying your salary and benefits while you sleep". Workers are expensive resources and the whole point of asynchrony is to *not* put them to sleep! – Eric Lippert Feb 05 '20 at 16:17
  • 1
    @EricLippert: I've seen a few people (e.g., [Joe Duffy](http://joeduffyblog.com/2006/08/22/priorityinduced-starvation-why-sleep1-is-better-than-sleep0-and-the-windows-balance-set-manager/)) recommend `Thread.Sleep(1)` over `Thread.Sleep(0)` for this purpose. The difference is that `Thread.Sleep(1)` unconditionally removes the thread from the scheduler, allowing time slice for other threads, even if those threads have lower priority. – Brian Feb 05 '20 at 19:47
  • @Brian: Good point; my comment was not intended to be a comprehensive list of corner cases. This illustrates a flaw in API design; if it is *important* to have a way to express "yield this thread to a lower pri thread" then why on earth is the API for that `Thread.Sleep(1);` instead of `Thread.YieldToLowerPriority();` ?!? APIs should not be designed for a priesthood with knowledge of the implementation details, *particularly* where threading is concerned. – Eric Lippert Feb 05 '20 at 19:57
  • 2
    @Brian: And yes, "always take Joe's advice" is one of my base rules for multithreaded programming, right after "don't do it at all if you can avoid it". :) – Eric Lippert Feb 05 '20 at 19:58
  • 2
    Nevermind. [Everything I/Joe said is outdated](https://stackoverflow.com/a/16584612/18192). – Brian Feb 05 '20 at 20:40
  • @Brian: Which nicely illustrates my point that the API design is bad, and that we ought not to program against the implementation details but rather the stated contract. – Eric Lippert Feb 06 '20 at 00:51