I've got a method, CheckForValue(), that uses named pipes to send a message to another process running locally, and then receive a message from that process, returning a bool indicating if it is equivalent with some value.
I'm concerned that I am not handling CancellationTokens and errors correctly. If there any errors (or timeouts), I just want this method to return false.
Again, I am not concerned with different methods of IPC, or using a single duplex named pipe, etc. My concern is the error handling and the usage of CancellationTokens here.
CheckForValue
public async Task<bool> CheckForValue()
{
int timeout = 300; //300ms should be plenty of time
try
{
using (var getValueCancellationTokenSource = new CancellationTokenSource())
{
var valueTask = GetValueUsingNamedPipe(getValueCancellationTokenSource);
using (var timeoutCancellationTokenSource = new CancellationTokenSource())
{
var completedTask = await Task.WhenAny(valueTask, Task.Delay(timeout, timeoutCancellationTokenSource.Token));
if (completedTask == valueTask)
{
if (timeoutCancellationTokenSource.Token.CanBeCanceled)
{
timeoutCancellationTokenSource.Cancel();
}
var result = valueTask.Result;
return (result == "WhatIWant");
}
if (getValueCancellationTokenSource.Token.CanBeCanceled)
{
getValueCancellationTokenSource.Cancel ();
}
return false;
}
}
}
catch (Exception)
{
return false;
}
}
GetValueUsingNamedPipe
public async Task<string> GetValueUsingNamedPipe(CancellationTokenSource ct)
{
var response = "";
try
{
Task sendMsg = SendMessage ("MyMessage", ct);
await sendMsg;
response = await Listen(ct);
}
catch (Exception)
{
return "";
}
return response;
}
SendMessage
public async Task SendMessage(string message, CancellationTokenSource ct)
{
try
{
using (var _pipeClientStream = new NamedPipeClientStream(".", "MyPipe", PipeDirection.Out, PipeOptions.Asynchronous))
{
await _pipeClientStream.ConnectAsync (1000, ct.Token);
var writer = new StreamWriter (_pipeClientStream) { AutoFlush = true };
await writer.WriteLineAsync (message);
await writer.WriteLineAsync (MessageFooter);
}
}
catch (Exception)
{
await Task.FromCanceled(ct.Token);
}
}
Listen
public async Task<string> Listen(CancellationTokenSource ct)
{
try
{
if (ct.Token.IsCancellationRequested)
{
ct.Token.ThrowIfCancellationRequested ();
}
using (var _pipeClientStream = new NamedPipeClientStream(".", "MyListenPipe", PipeDirection.In, PipeOptions.Asynchronous, TokenImpersonationLevel.Impersonation))
{
await _pipeClientStream.ConnectAsync (ct.Token);
if (!ct.IsCancellationRequested)
{
var sb = new StringBuilder ();
var reader = new StreamReader (_pipeClientStream);
do
{
string line = await reader.ReadLineAsync ();
if (line == MessageFooter || line == null)
{
break;
}
sb.AppendLine (line);
} while (true);
return sb.ToString ();
}
return "";
}
}
catch (Exception e)
{
return "";
}
}
1 Answer 1
According to my understanding your piece of software can finish in one of the following states:
- Succeeded
- Failed
- Timed out
You are not exposing the ability to cancel it on the top level. So, it is not cancelable.
Your explicit cancellation mechanism is not needed because:
- If timeout occurs at the top level then this fact will be available for all async methods, which received the
CancellationToken. The built-in BCL functions will check the token's validity so you don't have to worry about it. SendMessageandListenare called sequentially. So if former fails then latter won't be called.- After
connectAsyncyourIsCancellationRequestedis pointless. IfconnectAsyncsucceeded then this property won't be true. If it was not succeeded then you would not reach this line because yourListenfunction will be aborted.
On the other hand there is one place where it could make sense to check the cancellationToken. That's inside the do-while loop because the ReadLineAsync is not cancellable.
So let's put all this together with these in mind. I've converted your code into C# 8 if you don't mind.
Listen
public async Task<string> Listen(CancellationToken ct)
{
try
{
await using var _pipeClientStream = new NamedPipeClientStream(".", "MyListenPipe", PipeDirection.In, PipeOptions.Asynchronous, TokenImpersonationLevel.Impersonation);
await _pipeClientStream.ConnectAsync(ct);
var sb = new StringBuilder();
using var reader = new StreamReader(_pipeClientStream);
do
{
ct.ThrowIfCancellationRequested();
string line = await reader.ReadLineAsync();
if (line == MessageFooter || line == null)
{
break;
}
sb.AppendLine(line);
} while (true);
return sb.ToString();
}
catch
{
return "";
}
}
- The
NamedPipeClientStreamisIAsyncDisposablethat's why I usedawait using. - The
StreamReaderisIDisposablethat's why I usedusing. - I've checked the
CancellationTokenbefore eachReadLineAsyncbecause it is not cancellable.
SendMessage
public async Task SendMessage(string message, CancellationToken ct)
{
await using var _pipeClientStream = new NamedPipeClientStream(".", "MyPipe", PipeDirection.Out, PipeOptions.Asynchronous);
await _pipeClientStream.ConnectAsync(1000, ct);
await using var writer = new StreamWriter(_pipeClientStream) { AutoFlush = true };
await writer.WriteLineAsync(message.AsMemory(), ct);
await writer.WriteLineAsync(MessageFooter.AsMemory(), ct);
}
WriteLineAsynccan acceptCancellationTokenonly if it is called with aReadOnlyMemoryor with aStringBuilder. That's why I usedAsMemory.StreamWriterisIAsyncDisposablethat's why I usedawait using.
GetValueUsingNamedPipe
public async Task<string> GetValueUsingNamedPipe(CancellationToken ct)
{
await SendMessage("MyMessage", ct);
return await Listen(ct);
}
- The
sendMsgvariable is pointless because right after the assignment it is awaited then never used. - I've removed the try-catch block because in that way the
TaskCancelledExceptionwould be caught here. - So basically if you wish you can inline this method into the
CheckForValue
CheckForValue
public async Task<bool> CheckForValue()
{
const int timeout = 300;
try
{
using var timeoutTokenSource = new CancellationTokenSource(timeout);
var result = await GetValueUsingNamedPipe(timeoutTokenSource.Token);
return (result == "WhatIWant");
}
catch (TaskCanceledException) //The actual type will be TimedOutTaskCanceledException
{
return false;
}
catch (Exception) // GetValueUsingNamedPipe failed for some reason
{
return false;
}
}
- All the explicit cancellation calls are gone, because they are not needed. The cancellation fact will be propagated all the related parties and it will manifest in a
TaskCancelledException. - So it is much cleaner now. :D
I hope it helped you a bit.
You must log in to answer this question.
Explore related questions
See similar questions with these tags.
timeoutCancellationTokenSourcein your code? This wholeTask.WhenAnyandTask.Delayis unnecessary. You could specify a timeout at thectorof the CTS or by calling theCancelAfter\$\endgroup\$Task.Delay..technique: stackoverflow.com/questions/4238345/… \$\endgroup\$