0

Say I have an interface IBackgroundTask:

public interface IBackgroundTask
{
    Task<TaskResult> Execute();
}

The application that uses this reads messages from a queue, and then starts consumer logic to execute a background task for the message.

The interfaces dictates that it should be implemented as a Task, however sometimes the consumer does not actually need to execute any asynchronous work. In such cases the signature we have to implement would better be TaskResult Execute();. But we can't change this interface implementation, and it serves as the base to implement both synchronous and asynchronous work.

For the async work, this works well, we can properly use the async keyword and await the async calls (examples: calls to API, disk IO):

public async Task<TaskResult> Execute()
{
    await PossiblyLongExternalAPICall();
    return new TaskResult();
}

But for a consumer that only does synchronous work (examples: a query to the database), I'd have to implement it like so:

public Task<TaskResult> Execute()
{
    ExecuteSyncCode();
    return Task.FromResult(new TaskResult()); // is this the right thing to do?
}

Is this considered the right approach for situations like this? I have already read up on this topic and it seems like Task.FromResult for synchronous code isn't really a bad practice, but I'm wondering if there are better alternatives for this scenario.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
Fluppe
  • 479
  • 7
  • 22
  • 4
    It's fine -- it's a bit messy yes, but it's pretty cheap, and better than trying to have separate sync and async implementations. `awaiting` that already-complete `Task` is synchronous, and cheap, so there's very little cost there. Consider using `ValueTask` if you want to remove the overhead of the `Task` allocation – canton7 Mar 11 '22 at 11:33
  • 4
    I'm a little worried that your example of "synchronous work" is a database query, since that's usually one of the more obvious places to switch to async work. – Damien_The_Unbeliever Mar 11 '22 at 11:34
  • ^^ And it's especially _useful_ if you are planning on "going async" in the implementation later on, anyway. – Fildor Mar 11 '22 at 11:34
  • "does synchronous work (examples: a query to the database)" To me, this would be a prime example of asynchronous IO, unless it's an in-memory DB that lives in the same process as your app. – spender Mar 11 '22 at 12:09
  • 1
    `Task.FromResult` itself is not problematic, but if `ExecuteSyncCode` performs non-trivial work, this is still blocking the caller at a time it's not expected. I/O is *exactly* the sort of thing async is suitable for. Probably the least thing you can/should do if you cannot ditch the sync is make the method `async` and put an `await Task.Yield()` at the beginning so the call doesn't start blocking *immediately* and/or use `Task.Run`, but this does yield an async-over-sync implementation, which is not recommended. Note that if you have no result, `Task.CompletedTask` is simpler. – Jeroen Mostert Mar 11 '22 at 12:11
  • I think documentation is very important in cases like this. If I call `PossiblyLongExternalAPICall` I would expect it to take a long time, and can plan accordingly. If I call `PossiblyLongExternalAPICallAsync` I would expect it to behave like an async method and return a task quickly. – JonasH Mar 11 '22 at 12:15

3 Answers3

4

Is this considered the right approach for situations like this?

I would say No, for rather subtle reasons.

Task.FromResult allows you to return a task from a non-async method, sure. And it works fine for the successful case. But it doesn't work as expected for the exceptional case.

Methods with an asynchronous signature are expected to place all the method results on the returned task. They can complete synchronously - there's nothing wrong with that - but any exception is considered a "result" in this sense and should be placed on the returned task.

So, you'd want to catch any exceptions from ExecuteSyncCode (or even new TaskResult()) and return Task.FromException if there was one. And then to be properly pedantic, you may also want to catch OperationCanceledException explicitly and return Task.FromCanceled. By the time you're done handling all the corner cases, your code ends up looking something like this.

And by that time, your "simple" synchronous implementation is full of a bunch of boilerplate that muddles up the code and confuses future maintainers.

For this reason, I tend to prefer just marking the method as async and not using await. This will cause a compiler warning (CS1998 This async method lacks 'await' operators and will run synchronously.), which is generally useful but which you can #pragma ignore for this specific case since you want a synchronous implementation. Using async essentially makes the compiler do all that boilerplate for you, while keeping the implementation synchronous.

If you end up with several synchronous implementations of asynchronous interfaces, I'd recommend defining a couple static helper methods like this. That way, the ugly #pragmas are confined to a single helper class.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • 2
    "*but any exception is considered a "result" in this sense and should be placed on the returned task*" -- The [compiler team disagrees](https://sharplab.io/#v2:EYLgtghglgdgNAFxAJwK4wD4AEAMACLARgFYBuAWACgqsBmAgJgMIHYqBvKvbg+ogNgIAOAoICy0GAAoAlFx6dKPZXgBuEZHgR4AvHjFSYqADbGZFJTwC+87rd7NBWEVnFSi+AM4BCbzLzseDaUVkA==), interestingly. You could argue that boneheaded exceptions can avoid being wrapped in a Task I guess, whereas exogenous ones, etc, must be – canton7 Mar 11 '22 at 14:21
  • 1
    @canton7: Interesting; I did not know that! I believe your interpretation is most likely correct: that specifically for boneheaded exceptions, it doesn't matter where they're thrown. – Stephen Cleary Mar 11 '22 at 14:28
  • Any drawback with making the method async and sticking an `await Task.Yield()` at the top? This both removes the warning and defers the initial blocking, which sounds like a good idea in my book. But my book is mostly theoretical, yours isn't. – Jeroen Mostert Mar 11 '22 at 14:32
  • 1
    @JeroenMostert: `await Task.Yield();` forces asynchronous behavior, so the method is no longer synchronous, so it is slightly less performant. I prefer the `#pragma` for a couple reasons. One is, I dislike writing code that does nothing (especially if it results in actual CPU code) just to silence a compiler warning. If I have considered the warning and decided it is spurious, then a `#pragma` communicates *exactly that*, whereas code added to avoid the warning is much less clear. Another reason is, `Task.Yield` is a pretty good code smell indicator and I prefer to keep it that way. IMO. :) – Stephen Cleary Mar 11 '22 at 14:37
  • Well, putting sync code in an async method is a smell in and of itself, so by that point we're just arguing about the specifics of the aroma. :P The performance could matter if you have a "small" synchronous method, but if it's a big one, wouldn't the block-immediately-upon-call be a problem for many callers? It's *legal* for async methods to simply complete synchronously, but it sure is something most callers don't *expect*. Of course we in the async-over-sync woods already, so one way or another there could be trouble, but still. – Jeroen Mostert Mar 11 '22 at 14:41
  • 1
    If you're going to `await` anything there, it might as well be a `Task.CompletedTask` -- that avoids the mandatory dispatch back to the `SynchronizationContext` / thread pool. Note that if your method is blocking and the calling thread has a `SynchronizationContext`, then `await Task.Yield()` will still block the calling thread, just at an ever-so-slightly later point. – canton7 Mar 11 '22 at 14:41
  • 1
    Yes, I'm assuming that we *want* a synchronous implementation of an asynchronous method. Starting from that assumption, my preferences are based on (IMO) future maintainability of the code. When I read `#pragma warning disable`, I know that a previous dev is disabling a warning. When I read `await Task.Yield` or `await Task.CompletedTask`, my first thought is that a previous dev didn't understand `async`/`await`. – Stephen Cleary Mar 11 '22 at 14:49
  • @canton7: yes -- the reason I mention it is because I've seen scenarios with code piling up a bunch of tasks to await them all. If any of those is one of these long-running-sync-not-really-async tasks, things block immediately in the construction phase when we hit that task. If instead we block *slightly* later, the truly asynchronous tasks at least all get to do their thing before we slog through the sync work. Of course, none of this truly "fixes" the core problems, it's just mitigating a bad situation. ...in any case, this starts reading more like material for a new question. :P – Jeroen Mostert Mar 11 '22 at 14:52
  • How come the asynchronous methods get to do their thing? If they have multiple awaits, they're pretty much guaranteed to have one or more continuations which are scheduled after that sync work – canton7 Mar 11 '22 at 14:55
  • 1
    @JeroenMostert using `await Task.Yield()` per your suggestion is just an unreliable and idiomatic way of offload synchronous work to the `ThreadPool`. It only works in the absence of a `SynchronizationContext`. If you want to offload, it's better to do it reliably and explicitly with the `Task.Run`. And then you will still have to explain to Stephen Toub why you violated the guideline of [not exposing asynchronous wrappers for synchronous methods](https://devblogs.microsoft.com/pfxteam/should-i-expose-asynchronous-wrappers-for-synchronous-methods/). :-) – Theodor Zoulias Mar 11 '22 at 16:15
1

Yes, you are doing the right thing. Using the Task.FromResult method is the most economical way to create a completed Task<TResult>.

In case you anticipate that implementations of the IBackgroundTask.Execute method will return completed tasks most of the time, you could consider changing the interface so that the return type is ValueTask<TaskResult> instead of Task<TaskResult>. Creating a completed ValueTask<TResult> imposes no additional allocations, beyond what memory needs to be allocated for the TResult itself. On the contrary a completed Task<TResult> requires ~70 additional bytes per instance. This optimization makes sense if you also anticipate that the Execute method will be invoked in hot paths. Otherwise, if it's invoked once in a while, any performance gains will be absolutely tiny and negligible. The ValueTask<TResult> type is generally more cumbersome to use than the Task<TResult>, so proceed with caution.


Note: this answer assumes that your decision to provide a synchronous implementation to a method with asynchronous contract, is based on good reasons. I am not an advocate of breaking the asynchronous contract in general. If you don't have good reasons to break it, then please don't.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
  • Task allocation optimization when the core implementation is synchronous is definitely not the thing to focus on. That really comes into play only when the tasks are many and the time needed for execution is short. It is also supremely improbable that something called `IBackgroundTask` would be designed to return completed tasks most of the time, as there's not much "background" going on then. – Jeroen Mostert Mar 11 '22 at 12:21
  • @JeroenMostert yes, the context implied by the name `IBackgroundTask` suggests that using `ValueTask` is unlikely to be an appropriate API design decision in this case. My answer though is intended to be context-agnostic. I gave a general advice, regarding any kind of interface that might include an asynchronous method. – Theodor Zoulias Mar 11 '22 at 12:46
  • An example of a built-in interface that includes a `ValueTask`-returning method (two methods actually) is the [`IAsyncEnumerator`](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.iasyncenumerator-1). – Theodor Zoulias Mar 11 '22 at 12:51
  • Of course -- `IAsyncEnumerator` deals with piecemeal, probably very short-lived I/O, like enumerating records, with results that are very likely to not be needed more than once, where it makes eminent sense to use `ValueTask`. I took issue with your answer because it emphatically starts with "yes, you are doing the right thing", where it's at least probably the OP is in fact *not* doing the right thing. :P – Jeroen Mostert Mar 11 '22 at 12:53
  • @JeroenMostert if you have to implement an interface that includes an asynchronous method, and yourself don't have an underlying asynchronous API to work with, you have basically two options: 1. Block the current thread and return a completed task. 2. Block a `ThreadPool` thread and return an incomplete task to the current thread (`Task.Run`). Neither is a happy option. AFAIK the option selected by Microsoft itself when presented with this dilemma is always the option 1. Check the `Console.Out.WriteLineAsync` for example. – Theodor Zoulias Mar 11 '22 at 13:03
  • 1
    If you truly, positively, have no async option and must wrap a sync method, then yes. But no answer of this kind is complete without mentioning that the first thing that should definitely be done is look for the async option instead -- "a consumer that only does synchronous work (examples: a query to the database)" suggests that hasn't been done in this case, as a great deal of work has gone into optimizing async DB access. When you *are* wrapping a sync method, `ValueTask` vs. `Task` is not really something important, which is something one could easily take away from this answer. – Jeroen Mostert Mar 11 '22 at 13:07
0

Specific answer - use async code for db work

But for a consumer that only does synchronous work (examples: a query to the database),

Use async code for database work. It's one of the primary use cases for async/await. It's very unlikely that there are not async versions of the calls you're making.

General answer - avoid Task.FromResult

With the notable exception for tests when mocking, I would not recommend using Task.FromResult because of subtle possible bugs:

  • pretend async methods are executed synchronously; they don't 'let go' of the executing thread which subsequently can lead to:
    • thread starvation: your website will not be able to process as many request as if it would be if you're using real async,
    • Code Task.WhenAll etc. will not behave as expected because the caller makes incorrect assumptions (interface says it's async) ,
  • exception handling (please see Stephen Cleary's answer);
tymtam
  • 31,798
  • 8
  • 86
  • 126
  • "*Use async code for database work.*" -- Yes, except when you shouldn't! [Horrible performance using SqlCommand Async methods with large data](https://stackoverflow.com/questions/42415969/horrible-performance-using-sqlcommand-async-methods-with-large-data) / [Reading large data (binary, text) asynchronously is extremely slow](https://github.com/dotnet/SqlClient/issues/593) / [SQL Reader Async 10 times slower than sync](https://github.com/dotnet/runtime/issues/25329). – Theodor Zoulias Mar 18 '22 at 03:08
  • 1
    @TheodorZoulias Fair, absolute statements tend to break at the edges. But :). 1. a) Solved with async code / 2. I acknowledge / 3. Issue is closed as fixed. – tymtam Mar 18 '22 at 03:57