C
C#•7mo ago
Abdesol

cancellation token to stop Console.ReadLine is only working on the first try

I have the following method I got from stackoverflow
async Task<string?> ReadLineAsync(CancellationToken cancellationToken = default)
{
var readTask = Task.Run(() => Console.ReadLine());
await Task.WhenAny(readTask, Task.Delay(-1, cancellationToken));
cancellationToken.ThrowIfCancellationRequested();
return await readTask;
}
async Task<string?> ReadLineAsync(CancellationToken cancellationToken = default)
{
var readTask = Task.Run(() => Console.ReadLine());
await Task.WhenAny(readTask, Task.Delay(-1, cancellationToken));
cancellationToken.ThrowIfCancellationRequested();
return await readTask;
}
I run it in a loop while passing it a cancellation token.. and the cancellation token is also in a loop, which means, I recreate it every time.. it works on the first try.. but after the recreation starts for the cancellation token, on await Task.WhenAny(readTask, Task.Delay(-1, cancellationToken)); cancellation token doesn't get to end first although it should have from the other concurrent task I manage to cancel the token. What could be a possible solution? Thank you!
53 Replies
Esa
Esa•7mo ago
Why are you passing in a negative value to a delay method?
Abdesol
AbdesolOP•7mo ago
to make the wait time indefinite until cancellation token cancels it
Esa
Esa•7mo ago
Cool, wasn't aware that was an option. 🙂
Abdesol
AbdesolOP•7mo ago
hehe, the problem is nnow either Task.WhenAny is doing something wrong or cancellationToken is not being hit in the delay
Esa
Esa•7mo ago
So, your readtask, the var readTask, is not passed a cancellation token. Is that intentional? Also you are setting the throw behavior after all your code has been awaited. Is that too intentional?
Abdesol
AbdesolOP•7mo ago
I am not sure as I just got the solution from the internet 😅
Esa
Esa•7mo ago
Okay. Let's start fresh. What do you want to achieve? In plain english pseudocode
Abdesol
AbdesolOP•7mo ago
but I think it is okay as long as the delay is the one getting done first while readline is awaiting, I wanna cancel the ReadLine whenever I want to
Esa
Esa•7mo ago
"I want an application that asynchronously reads a line from the user, that can be cancelled by a cancellation token at any point"
like this you mean? So for example if a user has a time limit to type in a line, the cancellation token should cancel it after that limit? I'm just trying to understand what you want before I suggest anything
Abdesol
AbdesolOP•7mo ago
yep, that's what I meant
SleepWellPupper
SleepWellPupper•7mo ago
You're trying to cancel a synchronous operation (Console.ReadLine) That's not possible in this way Cancellation is cooperative, you can't force a method to be cancelled You can write your own impl of that using stdin maybe Your readTask is blocking internally via Console.ReadLine, so even if you throw on cancellation requested, the task will still be around in the background, waiting for input
Abdesol
AbdesolOP•7mo ago
interesting I think you are right, I need to implement my own imp using stdin it is just weird how it works on first try and then it doesn't
SleepWellPupper
SleepWellPupper•7mo ago
Just use Console.In.ReadLineAsync(CancellationToken)
Esa
Esa•7mo ago
That one ignores the token I think
Abdesol
AbdesolOP•7mo ago
this is also not working.. I tried
SleepWellPupper
SleepWellPupper•7mo ago
Ah I see wack
public override ValueTask<string?> ReadLineAsync(CancellationToken cancellationToken)
{
return cancellationToken.IsCancellationRequested ?
ValueTask.FromCanceled<string?>(cancellationToken) :
new ValueTask<string?>(ReadLine());
}
public override ValueTask<string?> ReadLineAsync(CancellationToken cancellationToken)
{
return cancellationToken.IsCancellationRequested ?
ValueTask.FromCanceled<string?>(cancellationToken) :
new ValueTask<string?>(ReadLine());
}
it doesn't ignore it but it doesn't cancel the readline huh Guess you do have to write your own (code is source for TextReader.ReadLineAsync
Esa
Esa•7mo ago
var timeout = TimeSpan.FromSeconds(5);
var userLine = string.Empty;
var readTask = Task.Run(() => userLine = Console.ReadLine());
var delayTask = Task.Delay(timeout);
var finishedTask = await Task.WhenAny(readTask, delayTask);

Console.WriteLine(finishedTask == delayTask ? "Too slow!" : $"Your line: \"{userLine}\"");
var timeout = TimeSpan.FromSeconds(5);
var userLine = string.Empty;
var readTask = Task.Run(() => userLine = Console.ReadLine());
var delayTask = Task.Delay(timeout);
var finishedTask = await Task.WhenAny(readTask, delayTask);

Console.WriteLine(finishedTask == delayTask ? "Too slow!" : $"Your line: \"{userLine}\"");
This seems to work when I run it once, and is quite similar to what was originally posted But it doesn't support the cancellation, so I'll add that in, a sec
SleepWellPupper
SleepWellPupper•7mo ago
Question could be: Given two parallel Console.ReadLine calls, upon receiving input, which one gets the input? If both do, you can do the cts approach Also, instead of using delay, You could use a TaskCompletionSource, create it by passing the token param and WhenAny using that
Abdesol
AbdesolOP•7mo ago
ah okay.. let me try that
Esa
Esa•7mo ago
Yeah I got that working, but I'm not sure this is more readable than not using the TaskCompletionSource.
Abdesol
AbdesolOP•7mo ago
it is not a big problem in my project. That's why I am delaying it a bit to fix major issues first.. sorry for the late replies
Esa
Esa•7mo ago
Nah I agree now, TaskCompletionSource is clean. I'll wait for you to give this a go before I post mine @Abdesol 🙂
Abdesol
AbdesolOP•7mo ago
oh okay.. will check how I can do that
Esa
Esa•7mo ago
We tend to want to help you figure the goal instead of providing the entire solution. Reason being you don't really learn much if the solution is just handed to you
SleepWellPupper
SleepWellPupper•7mo ago
But now I think I'll try and implement this myself because it sounds like a cool little puzzle... We can compare notes later lol @TeBeCo I've been baited again
Abdesol
AbdesolOP•7mo ago
yeah... that is very much understable
MODiX
MODiX•7mo ago
No description
SleepWellPupper
SleepWellPupper•7mo ago
@Abdesol What is the desired bahaviour for parallel invocations of ReadLineAsync? Should they all receive the same input or each their own?
Abdesol
AbdesolOP•7mo ago
Sorry if I was unclear. There is no parallel invocations of ReadLineAsync.. it is just parallel with another task that awaits for something to finish.. if that task is done, we raise the cancellation token, since we don't need user input basically, what I am building is a terminal some scripts I run with the terminal want user input.. and the read line is for user input
SleepWellPupper
SleepWellPupper•7mo ago
Yeah, problem is, you can't cancel the ReadLine, so if we cancel operation a, operation b will later have it's ReadLine synchronized to the cancelled one so we have to input something for op a in order for op b to receive input
Abdesol
AbdesolOP•7mo ago
ah, I see..] what was the code sharing website I want to share it here my approach for the terminal
SleepWellPupper
SleepWellPupper•7mo ago
$paste
MODiX
MODiX•7mo ago
If your code is too long, you can post to https://paste.mod.gg/, save, and copy the link into chat for others to see your shared code!
Esa
Esa•7mo ago
If it is less than like 30 lines, you can just do it in here tbh Mine, which has comments and newlines, is 19 lines
Abdesol
AbdesolOP•7mo ago
BlazeBin - qlfcrxbptnpp
A tool for sharing your source code with the world!
Abdesol
AbdesolOP•7mo ago
oh.. it is not the fix I made.. I will have to do that
SleepWellPupper
SleepWellPupper•7mo ago
I have a working implementation that I could share It's strongly coupled to Console though, so no real reusability
Esa
Esa•7mo ago
So is mine, but honestly that's just an input source that can be changed Let's share I'm going to have to go for lunch now, but this is the approach I made:
// Setup the cancellation delay
CancellationTokenSource cancellationSource = new();
TimeSpan timeout = TimeSpan.FromSeconds(5);
// The token will be cancelled after the specified timeout
cancellationSource.CancelAfter(timeout);

// Create a task that fails when timeout is reached
TaskCompletionSource<bool> completionSource = new TaskCompletionSource<bool>();
cancellationSource.Token.Register(obj =>
(obj as TaskCompletionSource<bool>)?.SetCanceled(), completionSource);
Task<bool> timeoutTask = completionSource.Task;

// Await the user input or the timeout task to complete
string? userLine = string.Empty;
Task<string?> readTask = Task.Run(() => userLine = Console.ReadLine());
Task finishedTask = await Task.WhenAny(readTask, timeoutTask);

// print the result
Console.WriteLine(finishedTask == timeoutTask ? "Too slow!" : $"Your line: \"{userLine}\"");
// Setup the cancellation delay
CancellationTokenSource cancellationSource = new();
TimeSpan timeout = TimeSpan.FromSeconds(5);
// The token will be cancelled after the specified timeout
cancellationSource.CancelAfter(timeout);

// Create a task that fails when timeout is reached
TaskCompletionSource<bool> completionSource = new TaskCompletionSource<bool>();
cancellationSource.Token.Register(obj =>
(obj as TaskCompletionSource<bool>)?.SetCanceled(), completionSource);
Task<bool> timeoutTask = completionSource.Task;

// Await the user input or the timeout task to complete
string? userLine = string.Empty;
Task<string?> readTask = Task.Run(() => userLine = Console.ReadLine());
Task finishedTask = await Task.WhenAny(readTask, timeoutTask);

// print the result
Console.WriteLine(finishedTask == timeoutTask ? "Too slow!" : $"Your line: \"{userLine}\"");
SleepWellPupper
SleepWellPupper•7mo ago
static class AsyncConsole
{
private static readonly Object _inFlightLock = new();
private static Task<String?>? _inFlightReadLine;
private static Task<String?> ReadLineTask
{
get
{
var inFlightReadLine = _inFlightReadLine;

if(inFlightReadLine != null)
return inFlightReadLine;

lock(_inFlightLock)
{
if(_inFlightReadLine != null)
return _inFlightReadLine;

inFlightReadLine = Task.Run(ReadLine);
_inFlightReadLine = inFlightReadLine;

return inFlightReadLine;
}
}
}
private static String? ReadLine()
{
var result = Console.ReadLine();
_inFlightReadLine = null;
return result;
}
public static async Task<String?> ReadLineAsync(CancellationToken cancellationToken)
{
var tcs = new TaskCompletionSource();
_ = cancellationToken.Register(tcs.SetResult);
var readLineTask = ReadLineTask;
_ = await Task.WhenAny(tcs.Task, readLineTask);
cancellationToken.ThrowIfCancellationRequested();
return readLineTask.Result;
}
}
static class AsyncConsole
{
private static readonly Object _inFlightLock = new();
private static Task<String?>? _inFlightReadLine;
private static Task<String?> ReadLineTask
{
get
{
var inFlightReadLine = _inFlightReadLine;

if(inFlightReadLine != null)
return inFlightReadLine;

lock(_inFlightLock)
{
if(_inFlightReadLine != null)
return _inFlightReadLine;

inFlightReadLine = Task.Run(ReadLine);
_inFlightReadLine = inFlightReadLine;

return inFlightReadLine;
}
}
}
private static String? ReadLine()
{
var result = Console.ReadLine();
_inFlightReadLine = null;
return result;
}
public static async Task<String?> ReadLineAsync(CancellationToken cancellationToken)
{
var tcs = new TaskCompletionSource();
_ = cancellationToken.Register(tcs.SetResult);
var readLineTask = ReadLineTask;
_ = await Task.WhenAny(tcs.Task, readLineTask);
cancellationToken.ThrowIfCancellationRequested();
return readLineTask.Result;
}
}
Unknown User
Unknown User•7mo ago
Message Not Public
Sign In & Join Server To View
SleepWellPupper
SleepWellPupper•7mo ago
Consumer needs that
Unknown User
Unknown User•7mo ago
Message Not Public
Sign In & Join Server To View
SleepWellPupper
SleepWellPupper•7mo ago
https://paste.gg/p/anonymous/69d1b968e05e4bc9ae2073bdc11f8f7c Yes, the consumer will have to
try
{
_ = await ReadLineAsync(ct);
} catch (OperationCancelledException)
{
//compensate
}
try
{
_ = await ReadLineAsync(ct);
} catch (OperationCancelledException)
{
//compensate
}
Abdesol
AbdesolOP•7mo ago
let me try this still.. this approach works only once for me like it is a terminal so it is in a loop
using var cts = new CancellationTokenSource();
using var cts = new CancellationTokenSource();
I use cts like this so on first enter, it goes and throws error.. on second, it doesn't throw error until I click enter again
SleepWellPupper
SleepWellPupper•7mo ago
Can you share how you're invoking the method?
Abdesol
AbdesolOP•7mo ago
maybe that thing that you said, the readline from the previous one is still active in this code I just changed
var input = await ReadLineAsync(cts.Token);
var input = await ReadLineAsync(cts.Token);
to
var input = await AsyncConsole.ReadLineAsync(cts.Token);
var input = await AsyncConsole.ReadLineAsync(cts.Token);
SleepWellPupper
SleepWellPupper•7mo ago
Yes, that is what I'm doing, we keep a single ReadLine-task alive for all ReadLineAsync invocations, and when it completes it enables another ReadLine to occur
Abdesol
AbdesolOP•7mo ago
yeah it looks like that did you try it in a loop where cts have to be desposed and defined multiple times? I feel like the recreation of cts is the problem here
SleepWellPupper
SleepWellPupper•7mo ago
Add a try/catch block for OperationCancelledException around your usage of ReadLineAsync When you cancel the operation, that thing is gonna throw Instead of your catch-all And what do you mean by "it only works once"? Does the method only return input once for all invocations? this
static async Task RunSequential()
{
var cts = new CancellationTokenSource(5000);
while(true)
{
try
{
Console.Write(">> ");
var input = await AsyncConsole.ReadLineAsync(cts.Token);
Console.WriteLine($"<< {input}");
} catch(OperationCanceledException)
{
Console.WriteLine("Cancelled");
break;
}
}
}
static async Task RunSequential()
{
var cts = new CancellationTokenSource(5000);
while(true)
{
try
{
Console.Write(">> ");
var input = await AsyncConsole.ReadLineAsync(cts.Token);
Console.WriteLine($"<< {input}");
} catch(OperationCanceledException)
{
Console.WriteLine("Cancelled");
break;
}
}
}
works just fine for me
Abdesol
AbdesolOP•7mo ago
like, it does it once and it doesn't work again shall I put the full code on paste bin and will you try it? It is just a one page program.cs code this also works for me.. but the cts is cancelled in another task actually this is also not the main task by it only works oce I mean, upon cts getting redefined, it stops working
Abdesol
AbdesolOP•7mo ago
this is the problem for me
Abdesol
AbdesolOP•7mo ago
after first try, when cts gets redefined, the issue is this
SleepWellPupper
SleepWellPupper•7mo ago
sure you can paste your code, I'll take a look at
Want results from more Discord servers?
Add your server