2013-11-21 52 views
4

考慮下面的簡單示例取消NetworkStream.ReadAsync(準備LinqPad推出,需要提升的帳戶):使用的TcpListener

void Main() 
{ 
    Go(); 
    Thread.Sleep(100000); 
} 
async void Go() 
{ 
    TcpListener listener = new TcpListener(IPAddress.Any, 6666); 
    try 
    { 
     cts.Token.Register(() => Console.WriteLine("Token was canceled")); 
     listener.Start(); 
     using(TcpClient client = await listener.AcceptTcpClientAsync() 
               .ConfigureAwait(false)) 
     using(var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5))) 
     { 
      var stream=client.GetStream(); 
      var buffer=new byte[64]; 
      try 
      { 
       var amtRead = await stream.ReadAsync(buffer, 
                0, 
                buffer.Length, 
                cts.Token); 
       Console.WriteLine("finished"); 
      } 
      catch(TaskCanceledException) 
      { 
       Console.WriteLine("boom"); 
      } 
     } 
    } 
    finally 
    { 
     listener.Stop(); 
    } 
} 

如果我telnet客戶端連接到localhost:6666和坐在那裏什麼都不做,持續5秒,爲什麼我看到「令牌被取消」但從未看到「繁榮」(或「完成」)?

這個NetworkStream不會取消取消嗎?

我可以用Task.Delay()Task.WhenAny的組合來解決這個問題,但我更願意按預期工作。

相反,取消下面的例子:

async void Go(CancellationToken ct) 
{ 
    using(var cts=new CancellationTokenSource(TimeSpan.FromSeconds(5))) 
    { 
     try 
     { 
      await Task.Delay(TimeSpan.FromSeconds(10),cts.Token) 
             .ConfigureAwait(false); 
     } 
     catch(TaskCanceledException) 
     { 
      Console.WriteLine("boom"); 
     } 
    } 
} 

打印 「咚」,如所預期。這是怎麼回事?

回答

13

不,NetworkStream不支持取消。

不幸的是,底層的Win32 API並不總是支持每次操作取消。傳統上,您可以取消全部特定句柄的I/O,但the method to cancel a single I/O operation相當新近。大多數.NET BCL都是針對XP API(或更早版本)編寫的,其中不包括CancelIoEx

Stream通過「僞造」取消支持(以及異步I/O),即使實現不支持它,也會使此問題變得複雜。對取消的「假」支持只是立即檢查令牌,然後開始無法取消的定期異步讀取。這就是你看到的NetworkStream

使用套接字(和大多數Win32類型),傳統的方法是關閉句柄,如果你想中止通信。這會導致所有當前操作(讀取和寫入)都失敗。從技術上講,這是違反BCL線程安全的文件,但它確實有效。

cts.Token.Register(() => client.Close()); 
... 
catch (ObjectDisposedException) 

如果,另一方面,要檢測一個半開的情況(其中你身邊是閱讀,但對方已經失去了連接),那麼最好的辦法就是定期發送數據。我更多地描述這個on my blog