【问题标题】:Making interface implementations async使接口实现异步
【发布时间】:2013-01-05 16:24:54
【问题描述】:

我目前正在尝试使用一些异步方法来制作我的应用程序。 我所有的 IO 都是通过接口的显式实现完成的,我对如何使操作异步有点困惑。

正如我所见,我在实施中有两个选择:

interface IIO
{
    void DoOperation();
}

选项1: 做一个隐式实现异步并在隐式实现中等待结果。

class IOImplementation : IIO
{

     async void DoOperation()
    {
        await Task.Factory.StartNew(() =>
            {
                //WRITING A FILE OR SOME SUCH THINGAMAGIG
            });
    }

    #region IIO Members

    void IIO.DoOperation()
    {
        DoOperation();
    }

    #endregion
}

选项2: 异步执行显式实现并等待来自隐式实现的任务。

class IOAsyncImplementation : IIO
{
    private Task DoOperationAsync()
    {
        return new Task(() =>
            {
                //DO ALL THE HEAVY LIFTING!!!
            });
    }

    #region IIOAsync Members

    async void IIO.DoOperation()
    {
        await DoOperationAsync();
    }

    #endregion
}

这些实现中的一个是否比另一个更好,或者还有其他我没有想到的方法吗?

【问题讨论】:

    标签: c# asynchronous async-await


    【解决方案1】:

    这些选项都不正确。您正在尝试异步实现同步接口。不要那样做。问题是当DoOperation() 返回时,操作还没有完成。更糟糕的是,如果在操作过程中发生异常(这在 IO 操作中很常见),用户将没有机会处理该异常。

    你需要做的是修改接口,使其异步:

    interface IIO
    {
        Task DoOperationAsync(); // note: no async here
    }
    
    class IOImplementation : IIO
    {
        public async Task DoOperationAsync()
        {
            // perform the operation here
        }
    }
    

    这样,用户将看到操作是async,他们将能够await。这也几乎迫使您代码的用户切换到async,但这是不可避免的。

    另外,我假设在您的实现中使用 StartNew() 只是一个示例,您不需要它来实现异步 IO。 (而new Task() 更糟糕,那甚至都行不通,因为你没有Start()Task。)

    【讨论】:

    • 使用显式实现会如何?另外,你在哪里等待这个实现?
    • @Animal 显式实现看起来和往常一样(只需添加async):async Task IIO.DoOperationAsync()。你的意思是你在哪里await返回的Task?无论您在哪里拨打DoOperationAsync()
    • 基本上我认为我可以将我的问题归结为“我在哪里等待?”如果我不在异步方法中等待,我会收到编译警告。
    • 理想情况下,您不需要将 IO 代码包装在 Task.Run() 中,该 IO 代码本身应该是异步的,您可以直接使用 await。例如。 line = await streamReader.ReadLineAsync().
    • 那么让你的代码异步就没有意义了。见文章Should I expose asynchronous wrappers for synchronous methods?
    【解决方案2】:

    更好的解决方案是为异步操作引入另一个接口。新接口必须继承原接口。

    例子:

    interface IIO
    {
        void DoOperation();
    }
    
    interface IIOAsync : IIO
    {
        Task DoOperationAsync();
    }
    
    
    class ClsAsync : IIOAsync
    {
        public void DoOperation()
        {
            DoOperationAsync().GetAwaiter().GetResult();
        }
    
        public async Task DoOperationAsync()
        {
            //just an async code demo
            await Task.Delay(1000);
        }
    }
    
    
    class Program
    {
        static void Main(string[] args)
        {
            IIOAsync asAsync = new ClsAsync();
            IIO asSync = asAsync;
    
            Console.WriteLine(DateTime.Now.Second);
    
            asAsync.DoOperation();
            Console.WriteLine("After call to sync func using Async iface: {0}", 
                DateTime.Now.Second);
    
            asAsync.DoOperationAsync().GetAwaiter().GetResult();
            Console.WriteLine("After call to async func using Async iface: {0}", 
                DateTime.Now.Second);
    
            asSync.DoOperation();
            Console.WriteLine("After call to sync func using Sync iface: {0}", 
                DateTime.Now.Second);
    
            Console.ReadKey(true);
        }
    }
    

    附: 重新设计您的异步操作,使其返回 Task 而不是 void,除非您确实必须返回 void。

    【讨论】:

    • 为什么不用GetAwaiter().GetResult() 而不是Wait()?这样你就不需要解压 AggregateException 来获取内部异常。
    • 一种变体依赖于实现多个(可能是显式)接口的类:class Impl : IIO, IIOAsync。然而,IIO 和 IIOAsync 本身是不同的合约,可以避免将“旧合约”引入新代码。 var c = new Impl(); IIOAsync asAsync = c; IIO asSync = c.
    • 坏主意。如果您的操作是异步的,请将它们公开为异步,并让消费者根据需要处理它们。消费者可以决定像您在示例中所做的那样“转换为同步”,或者保持异步。事实上,我建议也始终包含取消令牌,并使用它,例如超时未及时完成的​​ IO 操作。 .NET 框架中的异步实现的 Mosf 包括它。
    • 您不应在代码中使用 GetAwaiter 和 GetResult()。它仅适用于编译器:TaskAwaiter Struct
    【解决方案3】:

    我根据 Svick 的回答创建了一个示例应用程序,发现在没有 async 关键字的情况下调用 IOImplementation.DoOperationAsync() 不会导致编译器/Visual Studio 警告。这是基于 Visual Studio 2019 和 .NET Core 3.1。

    下面的示例代码。

    public interface ISomething
    {
        Task DoSomethingAsync();
    }
    
    public class Something : ISomething
    {
        public async Task DoSomethingAsync()
        {
            await Task.Run(() => Thread.Sleep(2000));
            Console.WriteLine("Message from DoSomethingAsync");
    
            throw new Exception("Some exception");
        }
    }
    
    class Program
    {
        static void Main(string[] args)
        {
            ISomething something = new Something();
            
            Console.WriteLine("pre something.DoSomethingAsync() without await");
            something.DoSomethingAsync(); // No compiler warning for missing "await" and exception is "swallowed"
            Console.WriteLine("post something.DoSomethingAsync() without await");
    
            Thread.Sleep(3000);
    
            // Output:
            // pre something.DoSomethingAsync() without await
            // post something.DoSomethingAsync() without await
            // Message from DoSomethingAsync
        }
    }
    

    【讨论】:

    • 我相信如果您的 Main 方法是异步的,编译器必须警告缺少“等待”。顺便说一句,最好使用 Task.Delay 而不是 Thread.Sleep 来实现异步方法中的等待。
    【解决方案4】:

    可以使用抽象类代替接口(在 C# 7.3 中)。

    // Like interface
    abstract class IIO
    {
        public virtual async Task<string> DoOperation(string Name)
        {
            throw new NotImplementedException(); // throwing exception
            // return await Task.Run(() => { return ""; }); // or empty do
        }
    }
    
    // Implementation
    class IOImplementation : IIO
    {
        public override async Task<string> DoOperation(string Name)
        {
            return await await Task.Run(() =>
            {
                if(Name == "Spiderman")
                    return "ok";
                return "cancel";
            }); 
        }
    }
    

    【讨论】:

    • 这样做有什么好处?为什么IIO.DoOperation() 有一个虚拟实现而不是abstract 本身?
    • 在 C# 7.3 中,我不能使用没有主体的“抽象异步”方法。
    • 这样做是零用处。如果没有 abstract 成员,则创建类 abstract 是没有意义的。 DoOperation() 方法可以变成abstract,但是你得到的东西本质上只是一个接口。这让人回想起,为什么不直接使用界面呢?上面的代码毫无意义。 :(
    • “我不能使用没有主体的“抽象异步”方法”——没有理由想要使用abstract async。将async 添加到方法声明的唯一作用是允许您在方法中使用await。如果你的方法不使用await,那么你就不需要async。这就是接口方法首先起作用的原因。 真正重要的是返回类型。您可以在没有 async 关键字的情况下设置返回类型 Task&lt;string&gt;
    猜你喜欢
    • 2016-01-01
    • 2011-02-08
    • 1970-01-01
    • 2018-11-04
    • 2014-03-20
    • 2016-07-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多