为啥在第一次输出后控制台冻结输出?

Posted

技术标签:

【中文标题】为啥在第一次输出后控制台冻结输出?【英文标题】:Why output on console freeze after first output?为什么在第一次输出后控制台冻结输出? 【发布时间】:2021-11-10 04:40:45 【问题描述】:

这里我有用 C# 编写的简单代码。

using System;
using System.Threading;
using System.Threading.Tasks;
using System.Reactive.Subjects;

namespace ReactiveProgramming

    class Program
    

        static void Main(string[] args)
        
            var generateSeq = new GenerateSequence();
            Console.WriteLine("Hello World!");

            generateSeq.Sequence.Subscribe(val =>
            
                Console.WriteLine(val);

                // it works if I remove below two lines ...
                Console.SetCursorPosition(0, Console.CursorTop - 1);   
                Console.Write("\r" + new string(' ', Console.WindowWidth) + "\r");
            );

            generateSeq.Run();
        
    

    class GenerateSequence
    
        public Subject<int> Sequence = new Subject<int>();

        public void Run(int runTimes = 10)
        
            ConsoleKeyInfo cki;

            Task.Run(() => runForTimes(10));

            do
            
                cki = Console.ReadKey();
             while (cki.Key != ConsoleKey.Escape);
        

        public void runForTimes(int runTimes = 10)
        
            for (var i = 0; i < 10; i++)
            
                Sequence.OnNext(i);
                Thread.Sleep(1000);
            
        
    

但不是在彼此之上打印序列,而是在第一次发出后冻结输出。

并且在 Linux 中也进行了测试...同样的输出。

如果我将这些行 Console.SetCursorPositionConsole.Write("\r" + new string(' ', Console.WindowWidth) + "\r") 从订阅远程...

但是如果我像这样更改我的 Main 函数:

    static void Main(string[] args)
    
        var generateSeq = new GenerateSequence();
        Console.WriteLine("Hello World!");

        generateSeq.Sequence.Subscribe(val =>
        
            Console.WriteLine(val);
            // Console.SetCursorPosition(0, Console.CursorTop - 1);
            // Console.Write("\r" + new string(' ', Console.WindowWidth) + "\r");
        );

        generateSeq.Run();
    

我已经评论了这两行...输出如下...

但不是像第二张图像那样按顺序输出,我想在同一位置打印输出。只需将新输出覆盖旧输出即可

注意:我在 Macbook Pro (Big Sur) 上运行它,它使用 .net core 3.1 或 .net 5.0 并使用 iTerm 作为控制台模拟器

【问题讨论】:

您可能想重新阅读minimal reproducible example...您是否认为这是线程问题还是只是SetCursorPosition?在这两种情况下都不需要所有花哨的代码...... 这与您的问题无关,但总的来说 Task.Delay() 比 Thread.Sleep() 更好。 await Task.Delay 会在您的代码中导致逻辑暂停,但保持线程空闲以进行其他活动,thread.sleep 会导致线程无法被任何人使用。 @AlexeiLevenkov 对于这个特定场景来说,这是最小的生产...... @AliReza 太棒了...效果很好...非常感谢您抽出宝贵的时间来研究它... 你为什么不使用Observable.Interval(TimeSpan.FromSeconds(1.0)).Take(10) 来生成你的序列? 【参考方案1】:

如果我写这个,我会选择这个实现:

    static async Task Main(string[] args)
    
        Console.WriteLine("Hello World!");

        IObservable<System.ConsoleKeyInfo> keys =
            Observable
                .Start(() => Console.ReadKey());

        await
            Observable
                .Interval(TimeSpan.FromSeconds(1.0))
                .Take(10)
                .TakeUntil(keys)
                .Do(x =>
                
                    Console.WriteLine(x);
                    Console.SetCursorPosition(0, Console.CursorTop - 1);
                ,
                () => Console.SetCursorPosition(0, Console.CursorTop + 1));

        Console.WriteLine("Bye World!");
    

应尽可能避免使用主题。

【讨论】:

真的很好奇为什么要尽可能避免使用主题。也许你可以帮助我的问题here。谢谢! Observables 的行为应该一致。当您使用主题时,您可能会创建一个可能会死亡的可观察对象,因为您的主题在某处得到了OnCompletedOnError。它们是功能世界中的一种状态形式,应该避免。 ***.com/questions/14396449/… 感谢您的见解! OT,Rx .NET 的低采用率是怎么回事?毕竟,它是当时最初的 Rx 实现。今天:RxJava repo - 45.3K 星,rxjs repo - 25.6K 星,Rx .NET repo - 5.1K 星。【参考方案2】:

SetCursorPosition 在不从另一个线程调用时工作得非常好。您可以使用异步方法来解决问题,而不是使用Task.Run

  class Program
    
        static void Main(string[] args)
        
            var generateSeq = new GenerateSequence();
            Console.WriteLine("Hello World!");

            generateSeq.Sequence.Subscribe(val =>
            
                Console.WriteLine(val);
                // move cursor back to previous line
                Console.SetCursorPosition(0 ,Console.CursorTop - 1);
            );
            
            // start background operation
            generateSeq.Run();
        
    
    
    class GenerateSequence
    
        public readonly Subject<int> Sequence = new();

        public void Run(int runTimes = 10)
        
            ConsoleKeyInfo cki;

            // create a cancelation token, because if the user presses
            // Escape key we don't need to run our background task 
            // anymore and the task should be stopped.
            var tokenSource = new CancellationTokenSource();
            var token = tokenSource.Token;

            // we can not use await keyword here because we need to 
            // listen to ReadKey functions in case the user wants to 
            // stop the execution. without the await, task will run in 
            // the background asynchronously
            var task = RunForTimes(runTimes,token);

            // wait for the Escape key to cancel the execution or stop it 
            // if it's already running
            do
            
                cki = Console.ReadKey();
             while (cki.Key != ConsoleKey.Escape && !task.IsCompleted);

            // cancel the background task if it's not compeleted.
            if (!task.IsCompleted)
                tokenSource.Cancel();    
            
            // Revert CursorPosition to the original state
            Console.SetCursorPosition(0, Console.CursorTop + 1); 

            Console.WriteLine("Execution ends");
        

        // we use an async task instead of a void to run our background 
        // job Asynchronously.
        // the main difference is, we should not use a separate thread 
        // because we need to be on the main thread to safely access the Console (to read or write)
        private async Task RunForTimes(int runTimes, CancellationToken token)
        
            for (var i = 0; i < runTimes; i++)
            
                Sequence.OnNext(i);
                await Task.Delay(1000, token);
             // exit the operation if it is requested
                if (token.IsCancellationRequested) return;
            
        
    

【讨论】:

是的,它可以在 Windows 终端上运行......我也可以确认......我现在也在 Windows VM 上对其进行了测试......但是相同的代码在 Mac 上不起作用......而且但它又不是Console.SetCursorPosition ...如果我在没有线程的情况下使用Console. SetCursorPosition,它也可以在Mac终端上运行......它的线程不工作...... 您是否尝试过 Linux 机器/VMWSL。我没有尝试过 WSL,但是是的,它不能在 Linux 机器上运行...... Task.Run(() =&gt; runForTimes(10));await RunForTimes(10); 的更改违背了整个目的。那时开始一个新任务的目的是,我可以并行运行该任务......当它等待RunForTimes 函数执行时,如果用户点击escape 键,那将退出应用程序。如果我从Task.Run(() =&gt; runForTimes(10)); 更改为await runForTimes(10),基本上do ... while 将永远不会被执行,直到RunForTimes 完成它的执行。 哦,直到现在我才看到你的 cmets xD。是的,你是对的,我希望最新的答案更新可以解决你的问题。 @microchip78 那太好了...效果很好...非常感谢您抽出宝贵的时间来研究它...对不起,我没有在我的问题或我之前的评论

以上是关于为啥在第一次输出后控制台冻结输出?的主要内容,如果未能解决你的问题,请参考以下文章

新人求问,sublime控制台输入后为啥没有输出

UIPickerView 的 ViewController 在选择后冻结

一道C语言题,从键盘输入23,为啥输出结果是32?

为啥在模拟器中启动应用程序后 xcode 控制台输出缓慢?

使用 Pyinstaller 冻结 Python 程序时摆脱控制台输出

js定义了一个数组,在控制台输出,第一次打开控制台时显示的是数组,刷新一下怎么就变成object了