0

I have WinForms app where button click calls some async method of external library.

private async void button1_Click(object sender, EventArgs e)
{
    await CallLibraryAsync();
}

private static async Task CallLibraryAsync()
{
    var library = new Library();
    await library.DoSomethingAsync();
}

The library looks like this:

public class Library
{
    public async Task DoSomethingAsync()
    {
        Thread.Sleep(2000);
        await Task.Delay(1000).ConfigureAwait(false);

        // some other code
    }
}

Before any asynchronous code there is some calculation simulated by Thread.Sleep call. In that case this call will block UI thread for 2 seconds. I have no option to change the code in DoSomethingAsync.

If I want to solve blocking problem, I could call the library in Task.Run like this:

private static async Task CallLibraryAsync()
{
    var library = new Library();

    // added Task.Run
    await Task.Run(() => library.DoSomethingAsync());
}

It solves the problem, UI is not blocke anymore, but I've consumed one thread from ThreadPool. It is not good solution.

If I want to solve this problem without another thread, I can do something like this:

private static async Task CallLibraryAsync()
{
    var library = new Library();

    // added
    await YieldOnlyAsync().ConfigureAwait(false);

    await library.DoSomethingAsync();
}

// added
private static async Task YieldOnlyAsync()
{
    await Task.Yield();
}

This solution works. Task.Yield() causes that method YieldOnlyAsync() always runs asynchronously and ConfigureAwait(false) causes that next code (await library.DoSomethingAsync();) runs on some ThreadPool thread, not UI thread.

But it is quite complicated solution. Is there any simpler?

Edit:
If the library method looks like this

public class Library
{
    public async Task DoSomethingAsync()
    {
        await Task.Delay(1000).ConfigureAwait(false);
        Thread.Sleep(2000);
        await Task.Delay(1000);

        // some other code
    }
}

UI thread would not be blocked and I do not need to do anything. But that's the problem that it is some implementation detail I do not see directly because that could be in some nuget package. When I see that the UI freezes in some situations, I may find this problem (mean CPU-bound calculation before any await in async method) just after some investigation. There is no Wait() or Result, that would be easy to find, this is more problematic.

What I would like is to be prepared for that situation if possible in some simpler way. And that's why I do not want to use Task.Run whenewer I call some third-party library.

Lubos
  • 125
  • 2
  • 10
  • *Is there any simpler?* - if you can't change the library, replicate the code of `DoSomethingAsync` into your own codebase and comment out the Sleep? Or how about editing the compiled lib to remove the call? – Caius Jard Jan 26 '22 at 18:28
  • 5
    don't worry about using threads from thread pools, specially desktop apps, since your case is a one. they are there for the sole purpose, being used. and they will exit as soon as the work is being completed. – Peyman Jan 26 '22 at 18:47
  • @CaiusJard, this is only demo to simulate the problem. See _there is some calculation simulated by Thread.Sleep_. Think of that like you use some nuget package – Lubos Jan 26 '22 at 21:14
  • 1
    So there's a long, CPU bound calculation performed by the library, before it does that await.. I kinda don't understand the reluctance to Task.Run it; it has to be run somewhere, so where would you have it run? – Caius Jard Jan 26 '22 at 21:56
  • @CaiusJard I've edited my original question (added some more explanation to the end) trying to clarify more my concern. Please check that. – Lubos Jan 26 '22 at 22:06
  • 1
    I think you're saying that you don't necessarily know what some external library is doing and whether it is a CPU-intensive operation or not. But you will always know what it's doing, otherwise why would you use it? But also, you will find out in testing: if it locks the UI, then use `Task.Run`. – Gabriel Luci Jan 26 '22 at 22:09
  • I think you're looking for a universal solution where there is none. – Gabriel Luci Jan 26 '22 at 22:11
  • @GabrielLuci All this originates from discussion when I explained to someone how async/await works. So this is not real situation from production, but it can happen. Imagine that I use some library that gets picture as input and send it to some store of the library producer. Then I am able to see that image in their web app. Everything works fine, they use async methods, image is uploaded somewhere. No blocking. But then I update that nuget package where they add some image optimization before being uploaded. This is real use-case. – Lubos Jan 26 '22 at 22:24
  • @GabrielLuci _you don't necessarily know what some external library is doing and whether it is a CPU-intensive operation or not_ - In this example I know **what** the library is doing but I don't know **how** it is implemented. I do not check source code of all nuget packages I use ;) – Lubos Jan 26 '22 at 22:24
  • That's what testing is for :) If you upgrade the package and it starts locking the UI, then start using `Task.Run`. – Gabriel Luci Jan 26 '22 at 22:33
  • You could also `await Task.Yield().ConfigureAwait(false)` – Jeremy Lakeman Jan 27 '22 at 00:39
  • @JeremyLakeman That would make the following code run on a ThreadPool thread, which is exactly what `Task.Run` would do. There is no down side to using `Task.Run` here. – Gabriel Luci Jan 27 '22 at 15:37
  • 1
    @JeremyLakeman, no, `ConfigureAwait()` is method on `Task` class. `YieldAwaitable` (returned by `Task.Yield()`) doesn't have that method. – Lubos Jan 27 '22 at 16:41

4 Answers4

4

If I want to solve blocking problem, I could call the library in Task.Run like this:

It solves the problem, UI is not blocke anymore, but I've consumed one thread from ThreadPool. It is not good solution.

This is exactly what you want to do in a WinForms app. CPU-intensive code should be moved to a separate thread to free up the UI thread. There isn't any downside to consuming a new thread in WinForms.

Use Task.Run to move it to a different thread, and wait asynchronously from the UI thread for it to complete.

To quote the Asynchronous programming article from Microsoft:

If the work you have is CPU-bound and you care about responsiveness, use async and await, but spawn off the work on another thread with Task.Run.

Gabriel Luci
  • 38,328
  • 4
  • 55
  • 84
  • 1
    `Task.Run` will not *necessarily* create a new thread. – Leonardo Herrera Jan 26 '22 at 19:02
  • 2
    @LeonardoHerrera Yes and no. According to [the documentation](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.task.run), it will 'queue the specified work to run on the ThreadPool'. If there happens to be another ThreadPool thread available, it might use that, in which case, no it doesn't *create* a new thread. But if not, then it *will* create a new thread. Whether it *creates* a new thread isn't really relevant. The point is that it's not run on the UI thread. – Gabriel Luci Jan 26 '22 at 19:15
  • 1
    @LeonardoHerrera You are right, that's why I wrote _I've consumed one thread from ThreadPool_, not "created" :) If there is no one available, only then the new one is created (if possible based on ThreadPool configuration of course). – Lubos Jan 26 '22 at 21:16
  • @GabrielLuci You are absolutely right, there is no problem to consume one (or two, three) thread(s) from ThreadPool in desktop app. It would be problem in web app, but there is no UI thread ;) I've edited my original question (added some more explanation to the end) trying to clarify more my concern. Please check that. – Lubos Jan 26 '22 at 22:02
0

I have no option to change the code

People say that, but you might not actually be hamstrung thus..

Here's a simple app with the same problem you face:

enter image description here

It's definitely pretty sleepy:

enter image description here

So let's whack it into ILSpy with the Reflexil plugin loaded:

enter image description here

We can perhaps shorten that timeout a bit.. Right click, Edit..

enter image description here

Make it 1ms, Right click the assembly and Save As..

enter image description here

That's a bit quicker!

enter image description here

Have a play, NOP it out etc..

Caius Jard
  • 72,509
  • 5
  • 49
  • 80
  • 1
    The Thread.Sleep is not the actual problem, it's just an example for simulating the cpu-consuming task: '*there is some calculation simulated by Thread.Sleep*'. While I believe this solution should be avoided as much as possible, but it may give someone an idea for very very rare cases. – Reza Aghaei Jan 26 '22 at 20:07
0

You wrote:

If I want to solve blocking problem, I could call the library in Task.Run like this:

private static async Task CallLibraryAsync()
{
    var library = new Library();

    // added Task.Run
    await Task.Run(() => library.DoSomethingAsync());
}

It solves the problem, UI is not blocked anymore, but I've consumed one thread from ThreadPool. It is not good solution.

(emphasis added)

...and then you proceed with inventing a convoluted hack that does the same thing: offloads the invocation of the DoSomethingAsync method to the ThreadPool. So you either want to:

  1. Invoke the DoSomethingAsync method without using any thread at all, or
  2. Invoke the DoSomethingAsync method on a non-ThreadPool thread.

The first is impossible. You can't invoke a method without using a thread. Code runs on CPUs, not on thin air. The second can be done in many ways, with the easiest being to use the Task.Factory.StartNew method, in combination with the LongRunning flag:

await Task.Factory.StartNew(() => library.DoSomethingAsync(), default,
    TaskCreationOptions.LongRunning, TaskScheduler.Default).Unwrap();

This way you will invoke the DoSomethingAsync on a newly created thread, which will be destroyed immediately after the invocation of the method has completed. To be clear, the thread will be destroyed when the invocation completes, not when the asynchronous operation completes. Based on the DoSomethingAsync implementation that you have included in the question (the first one), the invocation will complete immediately after creating the Task.Delay(1000) task, and initiating the await of this task. There will be nothing for the thread to do after this point, so it will be recycled.

Side notes:

  1. The CallLibraryAsync method violates the guideline for not exposing asynchronous wrappers for synchronous methods. Since the DoSomethingAsync method is implemented as partially synchronous and partially asynchronous, the guideline still applies IMHO.
  2. If you like the idea of controlling imperatively the current context, instead of controlling it with wrappers like the Task.Run method, you could check out this question: Why was SwitchTo removed from Async CTP / Release? There are (not very many) people who like it as well, and there are libraries available that make it possible (SwitchTo - Microsoft.VisualStudio.Threading).
Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
  • I agree, that both solutions (`Task.Run` vs. hack with `YieldOnlyAsync()`) consume thread from Thread Pool (so there is no difference), that was not good argument. `Task.Run` is often used incorrectly as async wrapper over sync method and I am afraid that when developers see this solution, they will use it incorrectly in other code. Here I would use `Task.Run` to run **async** method which looks strange. But it's true that it is better solution in this particular case. I don't agree with some other notes you wrote, I will react in separate comments :) – Lubos Jan 27 '22 at 17:47
  • Exposing async wrappers for sync methods is something different. It is when I have somewhere method `Calculate()` and I would create `CalculateAsync() { await Task.Run(Calculate); }`. That would violate that guideline. In my example `DoSomethingAsync()` is method in some third-party library. – Lubos Jan 27 '22 at 17:55
  • 1
    @Lubos using the `Task.Run` with async delegate is indeed somewhat strange the first time you see it, because it is not immediately obvious what purpose it serves. It serves exactly the purpose illustrated in your question: dealing with methods that have an asynchronous signature but a partially or fully synchronous implementation. Such methods ideally shouldn't exist, but in reality they do exist, and the `Task.Run` was [specifically designed](https://devblogs.microsoft.com/pfxteam/task-run-vs-task-factory-startnew/) to deal with them. – Theodor Zoulias Jan 27 '22 at 19:14
  • @Lubos as for the *"not expose asynchronous wrappers for synchronous methods"* guideline, it's a guideline, not a rule. If you judge that it's better to violate it because the situation demands it, and you have good arguments to justify the violation, feel free to do it. Stephen Toub is not going to come after you with a chainsaw and a sledgehammer. Unless he happens to become a user of you API, in which case you can expect him to become a bit displeased. – Theodor Zoulias Jan 27 '22 at 19:31
  • 1
    Agree with first, and the second (with Stephen Toub) made me laugh :) – Lubos Jan 27 '22 at 21:10
-1

When you use async/await for I/O or CPU-bound operations, your UI thread will not blocked. In your example, you use Thread.Sleep(2000);command for simulating your CPU-bound operations but this will block your thread-pool thread not UI thread. You can use Task.Delay(2000); for simulating your I/O operations without blocking thread-pool thread.

  • Not exactly. When you call async method, the code inside the method is synchronous up to the first await. It means that it runs on the same thread as the method is called. And in my example it's the UI thread. You can try to run this code an you can see that the UI freezes for 2 seconds. – Lubos Jan 26 '22 at 21:26
  • _You can use Task.Delay(2000); for simulating your I/O operations without blocking thread-pool thread._ You are right and that's what I used in my example. `Thread.Sleep` simulates CPU-bound operation and `Task.Delay` simulates IO-bound operation. The problem is with CPU-bound oparation, because in the example it blocks the UI. – Lubos Jan 26 '22 at 21:29