How to create and implement interfaces for operations that are only sometimes async

旧时模样 提交于 2019-12-12 17:44:31

问题


Let's say I have 100s of classes that implement a common interface with a method "calculate". Some of the classes will execute async (e.g. read a file), and other classes implementing the same interface will execute code that is sync (e.g. adding two numbers). What is a good way to code this, for maintenance and for performance?

The posts I read so far, always recommend to make async/await methods bubble up to the callers. So if you have one operation that is async, make the caller async, then its caller async, and so on. So this makes me think that the interface should be an async interface. However, this creates a problem when implementing the interface with code that is synchronous.

One idea I thought of is to expose in the interface 2 methods, one async and one sync, and one boolean property to tell the caller which method to call. This would look really ugly though.

The code I currently have is only one interface method that is async. Then for implementations that are synchronous, they wrap the code inside a Task object:

using System.IO;
using System.Threading.Tasks;

namespace TestApp
{
    interface IBlackBox
    {
        Task<string> PullText();
    }

    sealed class MyAsyncBlackBox : IBlackBox
    {
        public async Task<string> PullText()
        {
            using (var reader = File.OpenText("Words.txt"))
            {
                return await reader.ReadToEndAsync();
            }
        }
    }

    sealed class MyCachedBlackBox : IBlackBox
    {
        public Task<string> PullText()
        {
            return Task.Run(() => "hello world");
        }
    }
}

Is this the right approach to create and implement an interface that is only sometimes async? I have a lot of classes that implement short synchronous operations, and worry that this could add a lot of overhead. Is there some other way to do this that I am missing?


回答1:


This is a common situation with interfaces... If you have a contract that needs to specify a task for the Async Await Pattern and we have to implement that Task in the interface.

Assuming the caller is going to use await you can just drop the async and return a Task.

However, you need to be-careful with your exceptions. Its assumed that exceptions are placed on the task. So to keep this plumbing the caller will expect you have to handle them slightly differently.

Common usages

Standard async

public async Task<string> PullText()
{
   using (var reader = File.OpenText("Words.txt"))
   {
      return await reader.ReadToEndAsync();
   }
}

Returning a Task for CPU bound work (capturing the exception and placing it on the Task)

public Task<string> PullText()
{
   try
   {
      return Task.Run(() => DoCpuWork());
   }
   catch (Exception e)
   {
      return Task.FromException<string>(e);
   }
}

Slightly less efficient as we are plumbing an IAsyncStateMachine

public async Task<string> PullText()
{
    return await Task.Run(() => DoCpuWork());
}

Returning a completed Task with a simple results (capturing the exception and placing it on the Task)

public Task<string> PullText()
{
   try
   {
      // simplified example
      return Task.FromResult("someString");
   }
   catch (Exception e)
   {
      return Task.FromException<string>(e);
   }
}

There is also a 3rd approach, you can use the async keyword, and pragma out the warnings, this takes care of the error semantics for you. This feels a little dirty to me, just because it looks messy and the need to pragma out the warning, though i have now seen this used in bespoke production libraries

#pragma warning disable 1998
public async Task<string> PullText()()
#pragma warning restore 1998
{
    return Task.Run(() => "hello world");
}

and

#pragma warning disable 1998
public async Task<string> PullText()()
#pragma warning restore 1998
{
    return Task.FromResult("someString");
}

Note all the above deal with returning a Task<T> from the method. If one was just wanting to return the Task you can take advantage of Task.CompletedTask; with the same error semantics as above.




回答2:


Usually in these cases, You have something in front of the call that is handling the request and passing it off to the "worker" classes (e.g. TestApp). If this is the case, I don't see why having an "IAsyncable" interface where you could test if the class was async capable would not work.

if(thisObject is IAscyncAble) {
  ... call the ansync request.
}



回答3:


I ended up using the following code:

using System.IO;
using System.Threading.Tasks;

namespace TestApp
{
    interface IBlackBox // interface for both sync and async execution
    {
        Task<string> PullText();
    }

    sealed class MyAsyncBlackBox : IBlackBox
    {
        public async Task<string> PullText()
        {
            using (var reader = File.OpenText("Words.txt"))
            {
                return await reader.ReadToEndAsync();
            }
        }
    }

    sealed class MyCachedBlackBox : IBlackBox
    {
        public Task<string> PullText() // notice no 'async' keyword
        {
            return Task.FromResult("hello world");
        }
    }
}


来源:https://stackoverflow.com/questions/55446876/how-to-create-and-implement-interfaces-for-operations-that-are-only-sometimes-as

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!