4

I'm in a situation where we have some code that is run by user input (button click), that runs through a series of function calls and result in generating some data (which is a quite heavy operation, several minutes). We'd like to use Async for this so that it doesn't lock up the UI while we're doing this operation.

But at the same time we also have a requirement that the functions will also be available through an API which preferably should be synchronous.

Visualization/Example (pseudo-code):

public async void Button_Click() // from UI context
{
  await instanceOfClassA.FuncA();

  // some code
}

public async Task ClassA.FuncA()
{
  await instanceOfClassB.FuncB()

  // some code
}

public async Task ClassB.FuncB()
{
  await instanceOfClassC.SomeHeavyFunc()

  // some code
}

public async Task ClassC.SomeHeavyFunc()
{
  // some heavy calculations
}


// Also need to provide a public synchronous API function
public void SomeClass.SynchronousAPIFunc()
{
  // need to call differentInstanceOfClassB.FuncB()
}

Is there a way to make it so that the public API function does the waiting for the async operation internally?

EDIT: In this post, user Rachel provides two answers to the question. Both seem interesting, though I'm unsure which one would offer the least amount of risk/side effects.

EDIT2: I should note that we're using .NET v4.6.1.

Thanks in advance.

  • In SomeClass.SynchronousAPIFunc() you can call differentInstanceOfClassB.FuncB().Result(); which will result in method FuncB being called synchronously – auburg Jun 09 '20 at 13:30
  • This can be done, but it will be easy to introduce subtle deadlocks. It sounds like you have a bad requirement, if not an absurd one – Aluan Haddad Jun 09 '20 at 13:31
  • In this scenario it is usually better to transition to queue based or distributed workflow paradigm. If your API can run a function for several minutes, usually the calling framework will timeout or will interpret this as unresponsive. If the process genuinely takes that long, then change over to a pub-sub style architecture. – Chris Schaller Jun 09 '20 at 13:31
  • It's best to expose separate asynchronous / synchronous methods from your class – auburg Jun 09 '20 at 13:36
  • @AluanHaddad Using `.GetAwaiter().GetResult()` has the same risk of deadlock, so it's not any more "safe". All it does is unwrap the exception. – Gabriel Luci Jun 09 '20 at 15:07
  • Thanks for all of the replies. GetAwaiter().GetResult() was the solution we already had in place (which I failed to mention) in our initial experiment, but I was wondering if this was the "proper" way to go in regards to our predicament. It does brink some risk, which isn't ideal. But it seems to be the method that most people suggest. – OneRaccoonToRuleThemAll Jun 09 '20 at 18:21
  • Certainly that's probably the way to do it if *you* need to wait synchronously on something, but the problem here is making that decision for someone else and basically hiding that decision from them. They have no way to mitigate the risk of deadlock without that knowledge. Which is why I suggested the one option of just leaving it asynchronous and let the other person deal with it. – Gabriel Luci Jun 09 '20 at 18:34
  • Indeed. And I didn't mean to suggest otherwise, but now I think my comment is ambiguous – Aluan Haddad Jun 09 '20 at 23:45
  • You may find this interesting: [Should I expose asynchronous wrappers for synchronous methods?](https://devblogs.microsoft.com/pfxteam/should-i-expose-asynchronous-wrappers-for-synchronous-methods/) – Theodor Zoulias Jun 10 '20 at 09:19

3 Answers3

7

The problem with making "synchronous" versions of your methods that just call the asynchronous versions is that it can cause deadlocks, especially if the person calling this code is not aware that this is what is happening.

If you really want to make synchronous versions, then follow Microsoft's lead and write completely new methods that do not use any asynchronous code. For example, the implementation for File.ReadAllLines() doesn't use any of the same code as File.ReadAllLinesAsync().

If you don't want to do that, then just don't provide synchronous versions of your methods. Let the caller make the decision on how to deal with it. If they want to block synchronously on it, then they can mitigate the risk of deadlock.

Gabriel Luci
  • 38,328
  • 4
  • 55
  • 84
  • Thanks for the reply. I agree that this would be the tidiest approach. It might take quite a bit of restructuring though as these functions are very large and a lot of objects are involved. The chain of function calls goes through quite a lot of code. But it might be worth the refactoring, though it isn't always easy to convince others of the same. – OneRaccoonToRuleThemAll Jun 09 '20 at 18:17
  • I will flag this as the solution/answer, as it is the solution that has the least "code smell". – OneRaccoonToRuleThemAll Jun 09 '20 at 18:25
4

But at the same time we also have a requirement that the functions will also be available through an API which preferably should be synchronous.

If you have the need to expose both a synchronous and asynchronous API, I recommend the boolean argument hack. This looks like:

public Task<T> FuncBAsync() => FuncBAsync(sync: false);
public T FuncB() => FuncBAsync(sync: true).GetAwaiter().GetResult();
public async Task<T> FuncBAsync(bool sync)
{
  // Note: is `sync` is `true`, this method ***must*** return a completed task.
  ...
}

Is there a way to make it so that the public API function does the waiting for the async operation internally?

I do not recommend using direct blocking (e.g., GetAwaiter().GetResult()), as the straightforward implementation will lead to deadlocks.

EDIT: In this post, user Rachel provides two answers to the question.

I strongly recommend against using that solution. It uses a nested message loop with a custom SynchronizationContext, but doesn't do COM pumping. This can cause problems particularly if called from a UI thread. Even if the pumping isn't a problem, this solution can cause unexpected re-entrancy, which is a source of countless, extremely subtle, and difficult-to-find bugs.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • Thanks! This seems very interesting in conjunction with the anwer I flagged as the solution (providing both sync and async functions). Though I do have a question now that I read this and the article you linked. Most examples I see of async function chaining use async in the signatures in all the functions in the chain (and await in all bodies), while you only use it for the innermost call. Will execution still return to the caller(s) if the async keyword is omitted further up the chain? Is it enough to just return the task at that point? – OneRaccoonToRuleThemAll Jun 10 '20 at 09:24
  • Sorry for not being clear. You will need to use this pattern all the way through the chain. Each method that can be sync or async would need to use this pattern. – Stephen Cleary Jun 10 '20 at 10:43
  • Thanks for the reply! Sorry, I thin my wording was a bit poor. I was just wondering why the first line in your example doesn't say "public async Task FuncBAsync() => await FuncBAsync(sync: false);". It isn't immediately clear to me why we dont use the async and await keywords on that line. Also I'm going to check out your book on the matter. – OneRaccoonToRuleThemAll Jun 10 '20 at 20:10
  • 1
    Yes, I do tend to [elide async/await](https://blog.stephencleary.com/2016/12/eliding-async-await.html) when it's just a simple passthrough. – Stephen Cleary Jun 10 '20 at 21:32
2

You can utilize .GetAwaiter().GetResult()

as per your example, it would look like:

public void SomeClass.SynchronousAPIFunc()
{
  // need to call differentInstanceOfClassB.FuncB()
  ClassB.FuncB().GetAwaiter().GetResult();
}

Also, a good reference on when to not use the above can be found at Dont Block on Async Code