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 issues with multithreaded c# app only when run on Linux #49301

Closed
Tracked by #64487
AliciaRaven opened this issue Mar 6, 2021 · 8 comments
Closed
Tracked by #64487

Console issues with multithreaded c# app only when run on Linux #49301

AliciaRaven opened this issue Mar 6, 2021 · 8 comments

Comments

@AliciaRaven
Copy link

Description

Standard design for a c# console application is for the main thread to start worker threads, in my case listening for network connections, and then enter a while loop blocking on Console.Readline waiting for any console input. The documentation supports this model as console should support multithreading and when running on windows it does as expected. When running on linux with dotnet 3.1 runtimes it does not. When the main thread blocks on Readline as expected, any calls to Write to console also block and freeze that thread until a console command is issues releasing the readline block then those frozen writes to console are triggered. I am sure this is a bug as the code works as expected in windows as documented. It is only in linux environment that this console blocking behaviour is displayed. I have also noticed in my server code, under linux the startup gets some data from Console.Readline the first call that is not typed in the screen. It is displayed as squares indicating it is byte data that can not be interpreted as a string.

I have included a small sample code that shows the first thread blocking behaviour but does not show the garbage that console readline picks up, this would require more testing and isolation of the cause with my full server solution.

Summary is that when Console.Readline blocks main thread, other worker threads should be able to write to console and not block themselves causing application to freeze. Again important part of this is that it works on windows but not linux.

Configuration

Regression?

Unknown

Other information

Example Code:

namespace LinuxTest
{
    public class Program
    {
        public static Manager_Console ManagerConsole;

        public static bool TaskRunning;

        public static void Main(string[] args)
        {
            ManagerConsole = new Manager_Console();

            ManagerConsole.AddLine(LogLevel.Info, "Start Up", "Starting Console Test Application");

            ManagerConsole.AddLine(LogLevel.Info, "Start Up", "Starting Worker Thread");

            Thread t = new Thread(NewThreadMethod);
            t.Start();

            string commandText;

            // Main Thread Now Blocks Waiting For Console Input
            while (ManagerConsole.ExitRequest == false)
            {
                // Read Command
                commandText = Console.ReadLine();

                if (string.IsNullOrEmpty(commandText))
                {
                    // Print Error If No Command
                    ManagerConsole.AddLine(LogLevel.Warning, "Console", "No Command Typed");
                }
                else
                {
                    // Route Command To Correct Service
                    ManagerConsole.AddLine(LogLevel.Warning, "Console", "Command Typed: " + commandText);

                    if (commandText.ToLower() == "q")
                        ManagerConsole.ExitRequest = true;
                }
            }

            ManagerConsole.AddLine(LogLevel.Info, "Main Thread", "Main Thread Exiting, Hit Any Key To Exit");
            Console.ReadLine();
        }

        private static void NewThreadMethod(object obj)
        {
            TaskRunning = true;

            int Sheep = 0;

            while (ManagerConsole.ExitRequest == false)
            {
                Thread.Sleep(1000);

                ManagerConsole.AddLine(LogLevel.Info, "Worker", "From Worker Thread. Counting Sheep " + Sheep.ToString());

                Sheep++;
            }

            ManagerConsole.AddLine(LogLevel.Info, "Worker", "Worker Thread Exiting");
        }
    }
public enum LogLevel
    {
        Debug = 0,
        Info = 1,
        Important = 2,
        Warning = 3,
        Error = 4,
        Success = 5
    }

    public class Manager_Console
    {
        private readonly object Locker;

        public bool ExitRequest;

        public Manager_Console()
        {
            Locker = new object();

            ExitRequest = false;
        }

        // Console Printing
        public void AddLine(LogLevel enLevel, string strHeader, string strLine)
        {
            lock (Locker)
            {
                // First Move Cursor Back To Write Over Previous Command Prompt
                Console.SetCursorPosition(0, Console.CursorTop);

                DateTime DTNow = DateTime.Now;

                // Write Date PreFix (Dim)
                Console.ForegroundColor = ConsoleColor.DarkCyan;
                Console.Write(DTNow.ToString("yy.MM.dd"));

                // Time (Default Silver)
                Console.ResetColor();
                Console.Write(DTNow.ToString(" HH:mm:ss") + " [");

                // Write Header (Colour)
                Console.ForegroundColor = ConsoleColor.Yellow;
                Console.Write(strHeader);

                // Write Body
                Console.ResetColor();
                Console.Write("]: ");

                // Message
                switch (enLevel)
                {
                    case LogLevel.Warning:
                        Console.ForegroundColor = ConsoleColor.DarkYellow;
                        break;
                    case LogLevel.Important:
                        Console.ForegroundColor = ConsoleColor.Cyan;
                        break;
                    case LogLevel.Error:
                        Console.ForegroundColor = ConsoleColor.Red;
                        break;
                    case LogLevel.Debug:
                        Console.ForegroundColor = ConsoleColor.DarkMagenta;
                        break;
                    case LogLevel.Success:
                        Console.ForegroundColor = ConsoleColor.Green;
                        break;
                    default:
                        Console.ResetColor();
                        break;
                }

                // Write Main Body Of Message
                Console.Write(strLine + Environment.NewLine);

                // End With Command Prompt
                Console.ForegroundColor = ConsoleColor.White;
                Console.Write("Command: ");
            }
        }
    }
public delegate void Delegate_ConsoleCommand(string[] cmd);

    /// <summary>Class Representing A Registered Console Command</summary>
    public class ConsoleCommand
    {
        public string Service_Name { get; }

        public string Command_Help { get; }
        public string Command_Trigger { get; }

        public Delegate_ConsoleCommand Command_Delegate { get; }

        public ConsoleCommand(string serviceName, string trigger, string helpText, Delegate_ConsoleCommand commandDelegate)
        {
            Service_Name = serviceName;

            Command_Trigger = trigger;
            Command_Help = helpText;
            Command_Delegate = commandDelegate;
        }
    }
}
`
@antonfirsov antonfirsov transferred this issue from dotnet/core Mar 8, 2021
@dotnet-issue-labeler dotnet-issue-labeler bot added area-System.Console untriaged New issue has not been triaged by the area owner labels Mar 8, 2021
@Clockwork-Muse
Copy link
Contributor

Not your actual problem, but can cause issues nonetheless;

  1. ExitRequest needs to be volatile (or you need to switch to CancellationTokenSource/CancellationToken), or your other thread will see stale values (may not exit when expected).
  2. Instead of Thread you probably want to be using Task.Run.

@tmds
Copy link
Member

tmds commented Mar 16, 2021

The behavior you see is because fetching the cursor position (Console.CursorTop getter) and Console.Read* take the same lock:

// Synchronize with all other stdin readers. We need to do this in case multiple threads are
// trying to read/write concurrently, and to minimize the chances of resulting conflicts.
// 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)

@AliciaRaven
Copy link
Author

@tmds as this behaviour is different to windows, will this be considered a bug or be a caveat that I need to work around?

@Clockwork-Muse I would rather manage my threads directly as they are long running network listeners, I do not want to pass them to a thread pool in a fire and forget manor.

Thanks to both of you for your comments :)

@tmds
Copy link
Member

tmds commented Mar 16, 2021

will this be considered a bug or be a caveat that I need to work around?

It's a documented (at least in code) limitation of the implementation.
I don't think it is easy to improve, so it depends on how much users run into the issue if it worth to investigate.

In your case, instead of Console.SetCursorPosition(0, Console.CursorTop); try writing a \r.

@AliciaRaven
Copy link
Author

Thanks @tmds the use of \r works on linux. I will leave this issue open as i still feel it is a bug and should be on record.

@danmoseley
Copy link
Member

I wonder whether it would help if Console had an event for input as an alternative to Read()/ReadLine(). Similar to Process.OutputDataReceived

@jeffhandley jeffhandley added this to the 7.0.0 milestone Jul 13, 2021
@jeffhandley jeffhandley removed the untriaged New issue has not been triaged by the area owner label Jul 13, 2021
@jeffhandley jeffhandley modified the milestones: 7.0.0, Future Jul 9, 2022
RenodeBot pushed a commit to renode/renode-infrastructure that referenced this issue Jan 17, 2023
…-log-entries` config on dotnet

It required user input to release lock before collapsed repeated log was printed.

The behavior is because fetching the cursor position (Console.CursorTop getter) and Console.Read* take the same lock: dotnet/runtime#49301 (comment).

Renode running with `--console` switch interleaves log output and monitor input in a single shell. The proposed solution is a workaround that ignores  `collapse-repeated-log-entries`  config on dotnet and prints all lines.
RenodeBot pushed a commit to antmicro/renode that referenced this issue Jan 17, 2023
…-log-entries` config on dotnet

It required user input to release lock before collapsed repeated log was printed.

The behavior is because fetching the cursor position (Console.CursorTop getter) and Console.Read* take the same lock: dotnet/runtime#49301 (comment).

Renode running with `--console` switch interleaves log output and monitor input in a single shell. The proposed solution is a workaround that ignores  `collapse-repeated-log-entries`  config on dotnet and prints all lines.
@gmkado
Copy link

gmkado commented Sep 12, 2023

This behavior difference was surprising to me. My temporary workaround:

private static async Task<ConsoleKeyInfo> ReadKeyInternalAsync(bool intercept)
    {
        // the deadlock issue is only on linux
        if(RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
        {
            return Console.ReadKey(intercept);
        }

        // asynchronously wait for a key press to become available
        while (!Console.KeyAvailable)
        {
            await Task.Delay(50).ConfigureAwait(false);
        }

        return Console.ReadKey(intercept);
    }

@adamsitnik
Copy link
Member

Based on what @tmds wrote in #49301 (comment) I don't see us fixing it in the near future.

@adamsitnik adamsitnik closed this as not planned Won't fix, can't repro, duplicate, stale Nov 14, 2023
@github-actions github-actions bot locked and limited conversation to collaborators Dec 14, 2023
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

7 participants