4

I am reading a book "Terrell R. - Concurrency in .NET".

There is a nice code example:

Lazy<Task<Person>> person = new Lazy<Task<Person>>(
     async () =>
     {
         using (var cmd = new SqlCommand(cmdText, conn))
         using (var reader = await cmd.ExecuteReaderAsync())
         {
             // some code...
         }
     });

async Task<Person> FetchPerson()
{
    return await person.Value;
}

The author said:

Because the lambda expression is asynchronous, it can be executed on any thread that calls Value, and the expression will run within the context.

As i understand it, the Thread come to FetchPerson and is stuck in Lamda execution. Is that realy bad? What consequences?

As a solution, the author suggest to create a Task:

Lazy<Task<Person>> person = new Lazy<Task<Person>>(
      () => Task.Run(
        async () =>
        {
            using (var cmd = new SqlCommand(cmdText, conn))
            using (var reader = await cmd.ExecuteReaderAsync())
            {
                // some code...
            }
        }));

Is that really correct? This is an IO operation, but we steal CPU thread from Threadpool.

Tobias Tengler
  • 6,848
  • 4
  • 20
  • 34
Surgerer
  • 157
  • 9
  • Not sure why author suggests to use `Task.Run`, but when using async/await we never steal CPU thread. When we call `Task.Run` then sure it can be run on a thread pool, but only the first part of a method before first async. Once it hits async it returns back to the thread pool. Also, once the await finishes it returns back to the original thread context, so from the point of view of `FetchPerson` it doesn't really matter on which thread the code executed. – FCin Jan 09 '19 at 10:39

3 Answers3

2

Because the lambda expression is asynchronous, it can be executed on any thread that calls Value, and the expression will run within the context.

The lambda can be run from any thread (unless you're careful about what types of threads you let access the value of the Lazy), and as such it will be run in the context of that thread. That's not because it's asynchronous, it would be true even if it was synchronous that it'd run in the context of whatever thread happens to call it.

As i understand it, the Thread come to FetchPerson and is stuck in Lamda execution.

The lambda is asynchronous, as such it will (if implemented properly) return almost immediately. That's what it means to be asynchronous, as such it won't block the calling thread.

Is that realy bad? What consequences?

If you implement your asynchronous method incorrectly, and have it doing long running synchronous work, then yeah, you're blocking that thread/context. If you don't, you aren't.

Additionally, by default all of the continuations in your asynchronous methods will run in the original context (if it has a SynchonrizationContext at all). In your case your code almost certainly doesn't rely on re-using that context (because you don't know what contexts your caller might have, I can't imagine you wrote the rest of the code to use it). Given that, you can call .ConfigureAwait(false) on anything that you await, so that you don't use the current context for those continuations. This is simply a minor performance improvement in order to not waste time scheduling work on the original context, waiting for anything else that needs it, or making anything else wait on this code when unnecessarily.

As a solution, the author suggest to create a Task: [...] Is that really correct?

It won't break anything. It will schedule the work to run in a thread pool thread, rather than the original context. That's going to have some extra overhead to start with. You can accomplish approximately the same thing with lower overhead by simply adding ConfigureAwait(false) to everything that you await.

This is an IO operation, but we steal CPU thread from Threadpool.

That snippet will start the IO operation on a thread pool thread. Because the method is still asynchronous it will return it to the pool as soon as it starts it, and get a new thread from the pool to start running again after each await. The latter is likely appropriate for this situation, but moving the code to start the initial asynchronous operation to a thread pool thread is just adding overhead for no real value (because it's such a short operation you'll spend more effort scheduling it on a thread pool thread than just running it).

Servy
  • 202,030
  • 26
  • 332
  • 449
1

It is true that the first thread to access Value will execute the lambda. Lazy is unaware of async and tasks entirely. It will just run that delegate.

The delegate in this example will run on the calling thread until an await is hit. Then it will return a Task, that Task goes into the lazy and the lazy is entirely done at this point.

The rest of that task will run like any other task. It will respect the SynchronizationContext and TaskScheduler that were set when the await happened (this is part of await behavior). This can indeed lead to that code running in an unexpected context such as the UI thread.

Task.Run is a way to avoid that. It moves the code to the thread pool giving it a certain context. The overhead consists in queuing that work to the pool. The pool task will end at the fist await. So this is not async-over-sync. No blocking is introduced. The only change is on what thread CPU-based work happens (now deterministically on the thread pool).

It is fine to do this. It is an easy, maintainable, low-risk solution to a practical problem. There are different opinions about whether it is worth to do this or not. The overhead, in all likelyhood, will not matter. I personally am very sympathetic to this kind of code.

If you are sure that all callers of Value run in a suitable context then you don't need this. But if you make a mistake it's a serious bug. So you can argue that it is better to defensively insert Task.Run. Be pragmatic and do what works.

Also note, that Task.Run is async aware (so to speak). The task that it returns will essentially unwrap the inner task (unlike Task.Factory.StartNew). So it'S safe to nest tasks like it is done here.

usr
  • 168,620
  • 35
  • 240
  • 369
0

I totally don't understand why Terrell R. suggests to use Task.Run. It has no added value whatsoever. In both cases the lambda will get scheduled to the Thread pool. Since it contains IO operations, the worker thread from the thread pool will get released after the IO call; when the IO call completes, the next statement will continue on an arbitrary thread from the thread pool.

Seems that the author writes:

the expression will run within the context

Yes, the execution of the IO calls will start within the context of the caller, but will finish in an arbitrary context, unless you call .ConfigureAwait.

Nick
  • 4,787
  • 2
  • 18
  • 24
  • 1
    Why would the lambda would "get scheduled to the Thread pool" if you don't call `Task.Run`? There is no additional thread involved here (without the use of `Task.Run`). – mm8 Jan 09 '19 at 15:01
  • The lambda will start on the current thread, but up to the first `await`. After that it is up to the scheduler. – Nick Jan 09 '19 at 15:52
  • Yes. So the thread pool is not involved assuming the current thread has a `SynchronizationContext`. – mm8 Jan 09 '19 at 16:00