Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Console.CursorTop blocks on Linux when ReadKey is called from another thread #34239

Closed
VoightKampff opened this issue Mar 28, 2020 · 3 comments
Closed

Comments

@VoightKampff
Copy link

This happens on 3.1.101. It seems to be dependent on the order of calls, because if CursorTop is accessed at least once from printThread in the following code before ReadKey is called from the main thread, this issue does not occur. This can be tested by passing 1 as the argument to the repro code. I could not reproduce this on Windows (although I'm using a different machine for it).

using System.Collections.Generic;
using System.Threading;

namespace console_repro
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine($"Main thread {Thread.CurrentThread.ManagedThreadId}");

            var workThread = new Thread(() =>
            {
                int count = 0;
                Console.WriteLine($"Work thread {Thread.CurrentThread.ManagedThreadId}");
                while (true)
                {
                    Console.WriteLine($"Work thread message {count}");
                    Thread.Sleep(1000);
                    count++;
                }
            });
            workThread.Start();

            
            var printThread = new Thread(() =>
            {
                int count = 0;
                TimeSpan lastPrint = default;
                Console.WriteLine($"Print thread {Thread.CurrentThread.ManagedThreadId}");
                if (!(args.Length >= 1 && args[0] == "1"))
                {
                    Thread.Sleep(100);
                }
                while(true)
                {
                    var beforeCt = DateTime.Now;
                    var cursorTop = Console.CursorTop;
                    var afterCt = DateTime.Now;
                    Console.WriteLine($"Print thread message {count}. CursorTop block time: {afterCt - beforeCt}. Previous WriteLine time: {lastPrint}.");
                    var afterWl = DateTime.Now;
                    lastPrint = afterWl - afterCt;
                    Thread.Sleep(100);
                    count++;
                }
                
            });

            printThread.Start();
            if (args.Length >= 1 && args[0] == "1")
            {
                Thread.Sleep(100);
            }
            while(true)
            {
                var key = Console.ReadKey(true);
                Console.WriteLine($"Pressed {key.Key}");
            }
        }
    }
}

This produces the following output on Linux:

Main thread 1
Work thread 5
Work thread message 0
Print thread 6
Work thread message 1
Work thread message 2
Work thread message 3
Pressed A
Print thread message 0. CursorTop block time: 00:00:02.9728804. Previous WriteLine time: 00:00:00.
Work thread message 4
Pressed A
Print thread message 1. CursorTop block time: 00:00:00.9241746. Previous WriteLine time: 00:00:00.0004039.
Pressed A
Print thread message 2. CursorTop block time: 00:00:00.7504215. Previous WriteLine time: 00:00:00.0006605.
Work thread message 5
Pressed A
Print thread message 3. CursorTop block time: 00:00:00.8206150. Previous WriteLine time: 00:00:00.0005050.
Work thread message 6
Work thread message 7
Work thread message 8
Pressed A
Print thread message 4. CursorTop block time: 00:00:02.3339954. Previous WriteLine time: 00:00:00.0003828.
Work thread message 9

Machine info:

System:    Host: es131 Kernel: 5.3.0-42-generic x86_64 bits: 64 Desktop: Gnome 3.34.3 Distro: Ubuntu 19.10 (Eoan Ermine) 
CPU:       Quad Core: Intel Celeron N3150 type: MCP speed: 480 MHz min/max: 480/2080 MHz 
Graphics:  Device-1: Intel Atom/Celeron/Pentium Processor x5-E8000/J3xxx/N3xxx Integrated Graphics driver: i915 v: kernel 
           Display: x11 server: X.Org 1.20.5 driver: i915 resolution: 1920x1080~60Hz 
           OpenGL: renderer: Mesa DRI Intel HD Graphics 400 (Braswell) v: 4.5 Mesa 19.2.8 
Drives:    Local Storage: total: 238.47 GiB used: 35.39 GiB (14.8%) 
Info:      Processes: 239 Uptime: 3d 7h 20m Memory: 7.70 GiB used: 2.92 GiB (38.0%) Shell: bash inxi: 3.0.36 
@Dotnet-GitSync-Bot Dotnet-GitSync-Bot added area-System.Console untriaged New issue has not been triaged by the area owner labels Mar 28, 2020
@tmds
Copy link
Member

tmds commented Mar 28, 2020

This is a 'known' limitation:

// This does mean that Console.get_CursorLeft/Top can't be used concurrently with Console.Read*, etc.;
// attempting to do so will block one of them until the other completes, but in doing so we prevent
// one thread's get_CursorLeft/Top from providing input to the other's Console.Read*.
lock (StdInReader)

The thread performing the Console.Read is reading standard in.
The thread getting cursor position also needs standard in to read the current position.
This lock ensures there is only one reader for standard in.

It seems to be dependent on the order of calls, because if CursorTop is accessed at least once from printThread in the following code before ReadKey is called from the main thread, this issue does not occur

It doesn't occur when Console has a cached value for cursor position. The cached value can get cleared for several reasons (like the user resizing the window). So you can't use this as a workaround to avoid the blocking.

@danmoseley
Copy link
Member

Is this related somehow?
#34197

@stephentoub
Copy link
Member

Is this related somehow?

No. This is effectively by design, as @tmds outlined. The only reasonable way of getting the current cursor position on Unix is by writing an escape sequence to stdout requesting the position, and reading from stdin the response sent by the terminal. We have to lock around that, or else concurrent reads might end up reading the terminal's response, which would a) be gibberish and b) break the request/response protocol. That does mean if another thread is blocked on reading the console, a call to get the current position will block until the first read completes, unless we have a position cached.

@jeffhandley jeffhandley removed the untriaged New issue has not been triaged by the area owner label Sep 17, 2020
@ghost ghost locked as resolved and limited conversation to collaborators Dec 10, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

6 participants