5

I've just, for a rough draft, converted a whole lot of data access methods to async using the following pattern, and it looks too simple to be good enough for later iterations. How safe is it, what is missing, and how should I be doing it?

The service that provides long-running calls:

private class UserService
{
    public IdentityUser GetById(int id)
    {
        ...
    }
}

private UserService _userService = new UserService();

The original synchronous method:

public IdentityUser GetById(int id)
{
    return _userService.GetById(id);
}

My fantastic new async method:

public async Task<IdentityUser> GetByIdAsync(int id)
{
    await Task.Run(() => _userService.GetById(id));
}
ProfK
  • 49,207
  • 121
  • 399
  • 775
  • Long running queries have a long list of possible issues you need to deal with. Why not just install the SignalR nuget and get them all fixed for you - and websocket support as well! – Mattias Åslund Jan 20 '16 at 19:00
  • @MattiasÅslund: Different people have different definitions of "long-running". I have a feeling GetById() is not the sort of "long-running" that would merit use of SignalR. – StriplingWarrior Jan 20 '16 at 19:13
  • Is it something you consume as a WCF service or a Web API? If so, you only make things worse on the server-side. – noseratio Jan 20 '16 at 23:16
  • If you are thinking async wrappers for sync methods, please see [this](http://blogs.msdn.com/b/pfxteam/archive/2012/03/24/10287244.aspx) – snippetkid Jan 21 '16 at 08:51

3 Answers3

4

You should not make "fake async" methods like this:

public async Task<IdentityUser> GetByIdAsync(int id)
{
    await Task.Run(() => _userService.GetById(id));
}

The reason I call it "fake async" is because there is nothing intrinsically async about the operation. In this case, you should have only the synchronous method. If a caller wants to make it async by using Task.Run, he can do that.

When is something intrinsically async? When you make a request to a web service or a database, for example, between sending the request and receiving the response there is a waiting period - the request is an intrinsically async operation. To avoid blocking the calling thread, use async-await.

Timothy Shields
  • 75,459
  • 18
  • 120
  • 173
  • I have to implement the method exactly as `public async Task GetByIdAsync(int id)`, which will not compile if I don't make its inner operation a fake async, and `UserService` offers no async methods. – ProfK Jan 20 '16 at 19:40
  • 2
    @ProfK, then implement it as returning `Task.FromResult` and without `async` keyword (which is not a part of compiled method signature anyway). – noseratio Jan 20 '16 at 23:17
  • 1
    @Noseratio I kind if like `Task.FromResult`, and have used it many places I was forced to return a `Task` or even `await` one. I think I might play it safe and make all my 'fakes' use that. – ProfK Jan 21 '16 at 02:39
  • 1
    @ProfK, you can also cheat a bit: keep `async` and do `await Task.FromResult(0)` inside it to make the compiler happy, if you really like the [`async` exception propagation semantic](http://stackoverflow.com/a/21082631/1768303). This is still better than using `Task.Run` for faking asynchrony. – noseratio Jan 21 '16 at 02:43
1

Technically, that works, but it works by creating a new thread to perform a synchronous operation, which itself is wrapping and blocking on an inherently asynchronous operation. That means you're not getting some of the biggest benefits of going async in the first place.

The right way is to go asynchronous all the way. Whereas right now you probably have something like this:

private class UserService
{
    public IdentityUser GetById(int id)
    {
        return mContext.Users.Single(u => u.Id == id);
    }
}

... you should now create an async version:

private class UserService
{
    public async Task<IdentityUser> GetByIdAsync(int id)
    {
        return await mContext.Users.SingleAsync(u => u.Id == id);
    }
}

Usage:

public async Task<IdentityUser> GetByIdAsync(int id)
{
    return await _userService.GetByIdAsync(id);
}

Supposing, of course, that your underlying framework supports asynchronous methods like SingleAsync() for inherently asynchronous operations, this will allow the system to release the current thread while you wait for the database operation to complete. The thread can be reused elsewhere, and when the operation is done, you can use whatever thread happens to be available at that time.

It's probably also worth reading about and adopting these Best Practices. You'll probably want to use .ConfigureAwait(false) anywhere that you're not accessing contextual information like sessions and requests, for example.

This answer assumes, of course, that GetById is inherently asynchronous: you're retrieving it from a hard drive or network location or something. If it's calculating the user's ID using a long-running CPU operation, then Task.Run() is a good way to go, and you'll probably want to additionally specify that it's a long-running task in the arguments to Task.Run().

StriplingWarrior
  • 151,543
  • 27
  • 246
  • 315
  • I'm not calculating `id`, I'm looking for a user in a DB with a certain Id, and maybe the DB has millions of users and no index on `Id` – ProfK Jan 20 '16 at 19:47
  • @ProfK: Since your database back-end doesn't support asynchronous tasks, why are you trying to make your `GetByIdAsync` method `async` in the first place? If it's so that the caller can move on to another task while this one runs, then make it the caller's responsibility to call `Task.Run()`. (see http://blog.stephencleary.com/2013/11/taskrun-etiquette-examples-dont-use.html) Are you simply future-proofing your code, in hopes that some day this will be asynchronous? In that case, `return Task.FromResult( _userService.GetById(id));`. Otherwise, there's no reason to use `async` at all, so don't. – StriplingWarrior Jan 20 '16 at 23:27
0

Task.Run () should only be used for CPU-bound work. Don't quite remember why though. Try to make a GetByIdAsync () method instead, that ultimately calls an async resource.

Mogsdad
  • 44,709
  • 21
  • 151
  • 275
Mattias Åslund
  • 3,877
  • 2
  • 18
  • 17
  • I don't have an async resource, the resource being NHibernate, which as far as I have found out does not yet support async. – ProfK Jan 20 '16 at 19:42