I saw this example at the end of Stephen's book.
This code can be accessed by more than one thread.
static int _simpleValue;
static readonly Lazy<Task<int>> MySharedAsyncInteger = new Lazy<Task<int>>(
async () =>
{
await Task.Delay(TimeSpan.FromSeconds(2)).ConfigureAwait(false);
return _simpleValue++;
});
async Task GetSharedIntegerAsync()
{
int sharedValue = await MySharedAsyncInteger.Value;
}
No matter how many parts of the code call Value simultaneously, the
Task<int>
is only created once and returned to all callers.
But then he says :
If there are different thread types that may call
Value
(e.g., a UI thread and a thread-pool thread, or two different ASP.NET request threads), then it may be better to always execute the asynchronous delegate on a thread-pool thread.
So he suggests the following code which makes the whole code run in a threadpool thread :
static readonly Lazy<Task<int>> MySharedAsyncInteger = new Lazy<Task<int>>(() => Task.Run(
async () =>
{
await Task.Delay(TimeSpan.FromSeconds(2));
return _simpleValue++;;
}));
Question:
I don't understand what's the problem with the first code. The continuation would be executed in a threadpool thread (due to ConfigureAwait , we don't need the original context).
Also as soon that any control from any thread will reach the await
, the control will be back to the caller.
I don't see what extra risk the second code is trying to resolve.
I mean - what is the problem with "different thread types that may call Value
" in the first code?
An async method runs synchronously until it reaches its first await expression, at which point the method is suspended until the awaited task is complete. In the meantime, control returns to the caller of the method, as the example in the next section shows.
An async keyword is a method that performs asynchronous tasks such as fetching data from a database, reading a file, etc, they can be marked as “async”. Whereas await keyword making “await” to a statement means suspending the execution of the async method it is residing in until the asynchronous task completes.
Async and await in C# are the code markers, which marks code positions from where the control should resume after a task completes.
what is the problem with "different thread types that may call Value" in the first code?
There in nothing wrong with that code. But, imagine you had some CPU bound work along with the async
initialization call. Picture it like this for example:
static readonly Lazy<Task<int>> MySharedAsyncInteger = new Lazy<Task<int>>(
async () =>
{
int i = 0;
while (i < 5)
{
Thread.Sleep(500);
i++;
}
await Task.Delay(TimeSpan.FromSeconds(2));
return 0;
});
Now, you aren't "guarded" against these kind of operations. I'm assuming Stephan mentioned the UI thread because you shouldn't be doing any operation that's longer than 50ms on it. You don't want your UI thread to freeze, ever.
When you use Task.Run
to invoke the delegate, you're covering yourself from places where one might pass a long running delegate to your Lazy<T>
.
Stephan Toub talks about this in AsyncLazy:
Here we have a new
AsyncLazy<T>
that derives fromLazy<Task<T>>
and provides two constructors. Each of the constructors takes a function from the caller, just as doesLazy<T>
. The first constructor, in fact, takes the same Func thatLazy<T>
. Instead of passing thatFunc<T>
directly down to the base constructor, however, we instead pass down a newFunc<Task<T>>
which simply uses StartNew to run the user-providedFunc<T>
. The second constructor is a bit more fancy. Rather than taking aFunc<T>
, it takes aFunc<Task<T>>
. With this function, we have two good options for how to deal with it. The first is simply to pass the function straight down to the base constructor, e.g:
public AsyncLazy(Func<Task<T>> taskFactory) : base(taskFactory) { }
That option works, but it means that when a user accesses the Value property of this instance, the taskFactory delegate will be invoked synchronously. That could be perfectly reasonable if the
taskFactory
delegate does very little work before returning the task instance. If, however, thetaskFactory
delegate does any non-negligable work, a call to Value would block until the call totaskFactory
completes. To cover that case, the second approach is to run thetaskFactory
usingTask.Factory.StartNew
, i.e. to run the delegate itself asynchronously, just as with the first constructor, even though this delegate already returns aTask<T>
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With