2

Can anyone explain to me the difference between these 2 async methods?

Method A

public async Task<List<Thumbnail>> GetAllThumbnailsAsync()
{
    return await Task.Factory.StartNew(() =>             
    {
        var imageUris = GetAllDirectoriesWithImageAsync(CommandBaseUri).Result;

        return imageUris.Select(GetThumbnail).OrderByDescending(t => t.ImageDateTime).ToList();
    });
}

Method B

public async Task<List<Thumbnail>> GetAllThumbnailsAsync()
{
    var imageUris = await GetAllDirectoriesWithImageAsync(CommandBaseUri);

    return imageUris.Select(GetThumbnail).OrderByDescending(t => t.ImageDateTime).ToList();           
}

To my understanding both methods should return to the caller and does not block the UI thread, but in this case only Method A works as expected while Method B blocks my UI.

I believe there must be some fundamental concept that I may have misunderstood in the usage of async/await.

Can anyone enlighten me?

IvanJazz
  • 763
  • 1
  • 7
  • 19
  • Can you show how are you calling those methods? – Darin Dimitrov May 08 '17 at 07:00
  • You might want to check out [this question](http://stackoverflow.com/questions/14682513/task-factory-startnew-vs-async-methods) – Tamás Szabó May 08 '17 at 07:01
  • `MethodB` will block for as long as `GetAllDirectoriesWithImageAsync` blocks. Without seeing it, we can't tell you whether that is likely to be a long time or not. – Damien_The_Unbeliever May 08 '17 at 07:02
  • @Damien_The_Unbeliever: It won't be blocking the UI thread during that time. It's the second part of the async method that's the problem here, IMO. – Jon Skeet May 08 '17 at 07:05
  • Hard to believe you could have *that* many image files. At least tell us how many. – Hans Passant May 08 '17 at 07:07
  • @JonSkeet - if that method in fact spends a great deal of its time acting synchronously before ever starting a task or in other ways "going async" we may be paying for all of the I/O costs inside that method before it returns. I don't see how your crystal ball can tell you its cheap/not the issue. – Damien_The_Unbeliever May 08 '17 at 07:07
  • @Damien_The_Unbeliever: Ah yes, I see what you mean - I'd consider the method to be broken in that case. I read your comment as meaning "as long as it takes to get all the directories". I'd suggest it's at least *more likely* that `GetAllDirectoriesWithImageAsync` is okay, but that it's fetching the thumbnails that's broken. (I've edited my answer to mention that though.) – Jon Skeet May 08 '17 at 07:11
  • @HansPassant Only around 60 images. – IvanJazz May 08 '17 at 08:55
  • @Damien_The_Unbeliever, my `GetAllDirectoriesWithImageAsync` is not complex, in fact it's first call is an awaitable method. – IvanJazz May 08 '17 at 09:02
  • StartNew is dangerous exactly in such cases: https://blog.stephencleary.com/2013/08/startnew-is-dangerous.html – VMAtm Jun 01 '17 at 01:54
  • @VMAtm Thanks for the link, I really didn't know those complications existed, I just sort of followed the bandwagon since `Task.Factory` came into existence. – IvanJazz Jun 01 '17 at 03:53

1 Answers1

8

Method A is basically executing everything in a separate task, which will probably end up in a new thread. When you await the resulting task, you won't block anything.

Method B starts by calling GetAllDirectoriesWithImageAsync, and awaits the result. That means while the asynchronous operation is processing, you won't be blocking the UI thread - but by default, when you await the task, that means the continuation will run in the UI thread. So the imageUris.Select(...).OrderByDescending(...).ToList() will run in the UI thread, and I suspect that's the part that's causing problems in the UI.

Now you could call .ConfigureAwait(false) at the end of the first line of GetAllThumbnailsAsync, to indicate that you don't need to execute the second part on the UI thread - but that wouldn't guarantee that you wouldn't execute the second part on the UI thread. Instead, it seems to me that you really want an asynchonous way of getting thumbnails. Then you could do something like:

public async Task<List<Thumbnail>> GetAllThumbnailsAsync()
{
    var imageUris = await GetAllDirectoriesWithImageAsync(CommandBaseUri)
        .ConfigureAwait(false);

    var thumbnailTasks = imageUris.Select(GetThumbnailAsync).ToList();
    var thumbnails = await Task.WhenAll(thumbnailTasks).ConfigureAwait(false);

    // I'm assuming there will be sufficiently few thumbnails here
    // that sorting them on the UI thread wouldn't be a problem, in
    // the unlikely event that we're *really* still on the UI thread by
    // this point...
    return thumbnails.OrderByDescending(t => t.ImageDateTime).ToList();           
}

Note that I'm assuming that all async methods are written reasonably, e.g. that GetAllDirectoriesWithImageAsync doesn't perform a significant amount of synchronous work before returning a task.

Jon Skeet
  • 1,421,763
  • 867
  • 9,128
  • 9,194
  • It was the second part of the method that was causing the problems. I confirmed by wrapping only the second part in `Task.Factory.StartNew`. It takes around 2-3 seconds which is weird, as the `GetThumbnail` is just a object mapping method. However, I'm still confused as to which part is ran on the UI thread though, isn't the second part of the code of Method B supposed to be like a callback for the first 'await'? – IvanJazz May 08 '17 at 08:55
  • @IvanJazz: Yes, and that will be scheduled on the UI thread by default, if you called the whole method from the UI thread. (Not sure what you mean by "just a object mapping method" - if it's talking to the database, you may be making N queries. We don't really have enough information to say at the moment.) – Jon Skeet May 08 '17 at 08:56