1

To explain my problem, I have prepared a simplified WPF application like this. There is a Window with a Button and this is the click event handler:

private async void Button_Click(object sender, RoutedEventArgs e)
{
    IWorker worker = worker = new Worker();
    await Task.Run(() => worker.DoSomething(10));
}

As you may have noticed I am using the "async void" construct which is not recommended but, if I understand correctly, it is allowed when it comes to an event handler. IWorker is a very simple interface and the Worker class is implementing it. Since DoSomething method is CPU bound and its operations can take several seconds, I decided to call it with Task.Run because I do not want to block the UI thread.

interface IWorker
{
    int DoSomething(int i);
}

public class Worker : IWorker
{
    public int DoSomething(int i)
    {
        // do long CPU-bound operation
        return 42;
    }
}

Till now the application works perfectly but I need to complicate the things a little. I have decided that there are two working modalities. The one I just implemented is called "normal", the other is "online". In "online" mode I want to use a different worker: RemoteWorker. It implements the IWorker interface as well but instead of performing CPU computations, it delegates a syncronous request to a server via socket.

public class RemoteWorker : IWorker
{
    public int DoSomething(int i)
    {
        // it serializes the request and sends it to a server via socket.
        // the server does the calculation and returns the value via socket.
        // this method is waiting syncronously the output and then returns it.
        return 42;
    }
}

So here the new click event handler:

private async void Button_Click(object sender, RoutedEventArgs e)
{
    IWorker worker;
    if (mode == "normal")
        worker = new Worker();
    else
        worker = new RemoteWorker();
    await Task.Run(() => worker.DoSomething(10));
}

The system works perfectly and I am satisfied because the same UI works whether I decide to perform the computation locally or remotely. But I am annoyed that RemoteWorker.DoSomething synchronously waits for the response on the socket. Perhaps the best solution would be to use async methods to read and write to the socket. In that case I should also change the method signature and IWorker interface like so:

interface IWorker
{
    Task<int> DoSomething(int i);
}

public class Worker : IWorker
{
    public Task<int> DoSomething(int i)
    {
        // do cpu long operation
        return Task.FromResult(42);
    }
}

public class RemoteWorker : IWorker
{
    public async Task<int> DoSomething(int i)
    {
        // it serializes the request and sends it to a server via socket.
        // the server does the calculation and returns the value via socket.
        // this method is waiting asyncronously the output and then returns it.
        return 42;
    }
}

and maybe the button event should be:

private async void Button_Click(object sender, RoutedEventArgs e)
{
    IWorker worker;
    if (mode == "normal")
        worker = new Worker();
    else
        worker = new RemoteWorker();
    await Task.Run(async () => await worker.DoSomething(10));
}

And at this point I come to my problem! I have two classes that implement the same interface but one has a 100% CPU method and the other has a 100% IO method. Is this the most elegant way to manage these requirements?

In theory Task.Run should only be used for 100% CPU methods while here I am also using it for 100% IO methods. I would like the code to remain as generic as possible so I prefer to avoid statements like "if type is Worker then do Task.Run" otherwise just await.

MaestroX
  • 11
  • 3
  • Would it help if you regarded both IO and CPU as resources without differentiating the underlying mechanics? – Andrew Morton Dec 03 '21 at 18:16
  • You're making a decision outside of the worker (i.e. using `Task.Run`) that really needs to be made inside the worker. I would recommend changing your `IWorker` to be task-based as you described, and call it directly from your button click. Within the specific implementation you can decide if it's appropriate to use `Task.Run`. – Jack A. Dec 03 '21 at 18:27
  • @AndrewMorton can you clarify? – MaestroX Dec 03 '21 at 18:34
  • @JackA. I thought to this option but I didn't it for 2 reasons: 1 it seems it is not a best practice. If you take a look here [link](https://blog.stephencleary.com/2013/11/taskrun-etiquette-examples-dont-use.html), the conclusion is "do not use Task.Run in the implementation of the method; instead, use Task.Run to call the method." 2 DoSomething method is called in other places (not only in the UI) and I do not want to spawn unecessary threads. – MaestroX Dec 03 '21 at 18:38
  • Stephen Cleary is definitely a good source for recommendations. One thing I'd like to point out: in his example he's working with a specific service that has well-known properties (i.e., it's known to be CPU-bound). In your case it appears that you don't necessarily know that. Do you really need the very generic `IWorker` interface? When you change implementations, will it happen while the application is running or will it require a code change? – Jack A. Dec 03 '21 at 18:55
  • @JackA. When I open the interface I choose since the beginning in which modality I would like to work and I do not change it during the normal usage. Anyway, as I said, this is a simplification of my real application: there are many buttons and many methods of such kind here and then. So I believe it is a better approach to pick the right worker in one single place and then refer to it as an instance of IWorker everywhere. – MaestroX Dec 03 '21 at 19:44

1 Answers1

0

The Task.Run works equally well with both synchronous and asynchronous delegates. Using it to invoke an asynchronous delegate from the event handler of a GUI application ensures that the UI will remain responsive, even if the asynchronous delegate has actually a synchronous implementation (like your Worker.DoSomething method, or some misbehaving APIs). The overhead of wrapping a truly asynchronous delegate in a Task.Run is negligible, unless the ThreadPool is saturated, in which case the delegate will have to wait for a thread to become available before the asynchronous delegate is invoked. This scenario is unlikely to happen, unless you are doing heavy usage of the ThreadPool, by initiating multiple concurrent Parallel.ForEach operations or something. So my suggestion is to adopt your second approach (the async-based API).

private async void Button_Click(object sender, RoutedEventArgs e)
{
    IWorker worker = CreateWorker(mode);
    await Task.Run(async () => await worker.DoSomethingAsync(10));
}
Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104