Taking something that can't be done, and then doing it.

by Jiří {x2} Činčura

Gotcha cancelling read on the NetworkStream

Published 15 Jan 2017 in .NET, .NET Core, Multithreading/Parallelism/Asynchronous/Concurrency, and Network

You know the feeling when you write some awesome code and then you find the underlying code or library does not do what you expect it to do? Well, that’s exactly what happened to me with the NetworkStream.

My idea, in a nutshell, was pretty simple. Open a Socket, wrap it into NetworkStream and wait for data. Based on other state of the code around, occasionally cancel waiting for the data and move on, still using the same socket.

Because I was rewriting the code from around .NET 2.0 era to something up to date, I was excited I would be able to use await and more importantly ReadAsync with CancellationToken. That would make it so smooth and nice.

But it didn’t. Something was not behaving correctly as I was testing it. Being in this business for a long time, I knew my code is obviously correct and there’s a bug someplace else. Oh wait. No. The other way around.

I started digging into corefx sources and found this commit. As you can see it’s using the “old” BeginXxx/EndXxx (aka APM) methods and wrapping it into tasks (aka TAP). That’s fine. But the CancellationToken there is not used inside the call. Looks weird. But there’s a simple explanantion, as I was reminded by Stephen Toub. The NetworkStream uses the underlying Socket and the cancellation is not supported there (yet).

Here’s a small code that shows the behavior.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
class Program
{
	static readonly IPEndPoint Endpoint = new IPEndPoint(IPAddress.Loopback, 6666);

	static void Main(string[] args)
	{
		using (var cts = new CancellationTokenSource())
		{
			var server = ServerAsync();
			var client = ClientAsync(cts.Token);
			cts.CancelAfter(2000);
			client.Wait();
		}
	}

	static async Task ServerAsync()
	{
		TcpListener server = new TcpListener(Endpoint);
		server.Start();
		using (var client = await server.AcceptTcpClientAsync().ConfigureAwait(false))
		{
			using (var stream = client.GetStream())
			{
				await Task.Delay(-1).ConfigureAwait(false);
			}
		}
	}

	static async Task ClientAsync(CancellationToken cancellationToken)
	{
		using (var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp))
		{
			socket.Connect(Endpoint);
			using (var stream = new NetworkStream(socket, true))
			{
				Console.WriteLine("Reading");
				try
				{
					await stream.ReadAsync(new byte[4], 0, 4, cancellationToken).ConfigureAwait(false);
				}
				catch (TaskCanceledException)
				{
					Console.WriteLine("Canceled");
					return;
				}
				Console.WriteLine("Done");
			}
		}
	}
}

The ClientAsync method never reaches the Cancelled (or Done).

All right. Time to find another solution. Because that’s what developers do, right?