SslStream.ReadAsync从不调用InnerStream.ReadAsync

时间:2015-05-21 11:20:10

标签: c# .net async-await

我不确定我是否遗漏了某些内容,或者SslStream中确实存在设计缺陷(可能还有其他可以封装内部流的流类)。

考虑这个伪代码:

MyStream
{
  Task<int> ReadAsync()
  { ... }
}

...

{
  MyStream my = new MyStream();
  SslStream ssl = new SslStream(my);
  ssl.ReadAsync(...);
}

虽然可以预期SslStream.ReadAsync最终将调用MyStream.ReadAsync,但它不会。相反,它会调用MyStream.BeginRead(如果已定义)。如果未定义MyStream.BeginRead,则行为将难以预测(它将取决于派生MyStream类等等)。

简而言之,为了使SslStream的async / await方法按预期工作,需要实现内部流类的BeginXXX / EndXXX(非异步/等待方法)。

BeginXXX / EndXXX模式比async / await模式更复杂(对我来说,这就是引入async / await的原因 - 使异步编程更容易)。但是仍然需要开发BeginXXX / EndXXX方法会失去async / await的目的。

此外,需要知道SslStream类的内部实现(因为如果实现不同,它可以直接调用InnerStream.ReadAsync)。我的意思是SslStream的公共签名并没有清楚地向我提供关于我是否应该在我的内部流类中实现ReadAsync或BeginRead的足够信息。

为此,我需要使用试错法或检查SslStream的源代码(及其Stream父作为SslStream从基本Stream类继承ReadAsync)。这似乎不是一种可靠而直接的编写代码的方法。

当前实现async / await方法如SslStream / Stream类中的ReadAsync是有原因的吗?

3 个答案:

答案 0 :(得分:5)

是的,Stream尤其有点混乱,因为它在async / await之前很久就存在了。例如,ReadAsync的默认实现实际上会对线程池线程执行阻塞读取。

我建议您将ReadAsync覆盖为常规TAP方法,并将BeginRead / EndRead覆盖为该TAP方法的APM包装。 MSDN docs具有最佳模式(正确处理callbackstate),但我更喜欢稍微调整一下,因此来自EndRead的任何异常都不包含在AggregateException

public static IAsyncResult ToBegin<T>(
    Task<T> task, AsyncCallback callback, object state)
{
  var tcs = new TaskCompletionSource<T>(state);
  task.ContinueWith(t =>
  {
    if (t.IsFaulted) tcs.TrySetException(t.Exception.InnerExceptions)
    else if (t.IsCanceled) tcs.TrySetCanceled();
    else tcs.TrySetResult(t.Result);

    if (callback != null) callback(tcs.Task);
  }, TaskScheduler.Default);
  return tcs.Task;
}

public static T ToEnd<T>(IAsyncResult result)
{
  // Original MSDN code uses Task<T>.Result
  return ((Task<T>)result).GetAwaiter().GetResult();
}

答案 1 :(得分:3)

Task实施IAsyncResult,因此您应该可以使用

return ReadAsync(...);

作为BeginRead的实施。也许它有点复杂,例如你需要将回调挂钩作为该任务的延续。但是你可以重用代码。

顺便说一下,你打破了Stream API合同,因为BeginRead做了与ReadAsync不同的事情。严格来说,这是你的错,而不是框架问题。

  

我的意思是SslStream的公共签名并没有清楚地告诉我是否应该在我的内部流类中实现ReadAsync或BeginRead。

没必要知道。从类继承时,您继承了此类具有的所有义务。您必须实现所有内容,因为这是您对该类用户的承诺。明显的例子:如果你是从IEnumerator派生的,那么你就不能实现MoveNext但是要忽略Current并希望呼叫者能够工作。

答案 2 :(得分:2)

ReadAsync只是更深入的异步API的辅助包装器。

如果要覆盖异步行为,最好的方法是覆盖BeginReadEndRead - ReadAsync的默认实现会在这些任务之上构建任务(其他BeginRead将调用底层流的BeginRead

这只是因为后来添加了ReadAsync - 事实上,在某些平台上,它们尚未可用。但最后,期望ReadAsync将作为重载工作,与期望它会自动使用你已经超载的Read非常相似 - 它们并不适合。

但不要担心 - 这并不会让使用基于Task的API变得更难! Task本身实现IAsyncResult。因此,在覆盖BeginRead方法时,只需让它返回ReadAsync :)

简化示例实现:

class MyStream : Stream
{
    public override async Task<int> ReadAsync
      (byte[] buffer, int offset, int count, CancellationToken cancellationToken)
    {
        const string str = "Hi there!\r\n";

        await Task.Delay(1000);

        return Encoding.UTF8.GetBytes(str, 0, str.Length, buffer, offset);
    }

    public override IAsyncResult BeginRead
      (byte[] buffer, int offset, int count, AsyncCallback callback, object state)
    {
        return ReadAsync(buffer, offset, count).ContinueWith(t => callback(t));
    }

    public override int EndRead(IAsyncResult asyncResult)
    {
        // Stolen from Stephen. Nicer than rethrowing the inner exception manually :)
        return ((Task<int>)asyncResult).GetAwaiter().GetResult();
    }
}

显然,将它变成基类非常容易 - 允许您只覆盖子项中的ReadAsync方法。如果这不是一个选项,您可以管理扩展方法中的功能(使用简化代码看起来并不重要,但您可能希望在实际代码中进行一些检查和错误处理;更重要的是,您真的想要处理state - 对于我的测试用例,实现它是不必要的,但这只是违反API的一件事。)

正如@usr指出的那样,您错过的一点是 负责确保您的流的行为一致。这就像你只覆盖==运算符而不是EqualsGetHashCode一样 - 由此产生的不一致是你的错,因为你应该确保一致性。另一方面,如果您只覆盖BeginRead,它会正常工作,因为ReadAsync的默认实现会调用BeginRead - 代码保持倒退兼容。但出于完全相同的原因,它不能以相反的方式工作。

修改

好的,我已经写了一些即使传递state也应该可以正常工作的东西 (现已针对TaskTask<T>进行了更新):

static class Extensions
{
    struct Unit { }

    public static IAsyncResult Apmize<T>(this Task<T> @this, AsyncCallback callback, object state)
    {
        return @this.ApmizeInternal<T>(callback, state);
    }

    public static IAsyncResult Apmize(this Task @this, AsyncCallback callback, object state)
    {
        return @this.ApmizeInternal<Unit>(callback, state);
    }

    private static IAsyncResult ApmizeInternal<T>(this Task @this, AsyncCallback callback, object state)
    {
        if (@this.AsyncState == state)
            return @this.ContinueWith(t => callback(t));

        var tcs = new TaskCompletionSource<T>(state);

        @this.ContinueWith
            (
                t =>
                {
                    if (t.IsFaulted) tcs.TrySetException(t.Exception.InnerExceptions);
                    else if (t.IsCanceled) tcs.TrySetCanceled();
                    else
                    {
                        if (t is Task<T>) 
                        {
                            tcs.TrySetResult(((Task<T>)t).Result);
                        }
                        else
                        {
                            tcs.TrySetResult(default(T));
                        }
                    }

                    if (callback != null) callback(tcs.Task);
                },
                CancellationToken.None, TaskContinuationOptions.None, TaskScheduler.Default
            );

        return tcs.Task;
    }
}

用法如下:

public override IAsyncResult BeginRead
  (byte[] buffer, int offset, int count, AsyncCallback callback, object state)
{
    return ReadAsync(buffer, offset, count).Apmize(callback, state);
}

public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state)
{
    return WriteAsync(buffer, offset, count).Apmize(callback, state);
}

它有点长,但它只是一种可重用的扩展方法。并且它正确处理状态:)