控制台应用程序(C#)在交互过程中卡住了

6

我正在编写一个控制台应用程序,该应用程序通过调度程序执行一些工作,并将输出写入控制台。一切都很好,但是当我点击控制台时,它停止工作并等待我的右键单击。之后它才继续工作。

我认为它只是不会将文本写入控制台并执行所需的操作,但事实并非如此,它需要等待我的交互。我可以将此代码重写为WinForms或WPF,但我认为可以用另一种方式解决。这是我的代码:

    static void Main(string[] args)
    {
        Console.WriteLine("Started...");
        var timer = new System.Timers.Timer(1000);
        timer.Elapsed += timer_Elapsed;
        timer.Start();

        Console.ReadLine();
    }

    static void timer_Elapsed(object sender, ElapsedEventArgs e)
    {
        Console.WriteLine("Writing to file " + DateTime.Now.ToString());
        System.IO.File.AppendAllLines(@"C:\Temp\log.txt", 
          new[] { DateTime.Now.ToString()});
    }

点击控制台后,停止将时间附加到文件log.txt中。有什么想法如何解决这个问题吗?谢谢。

5
好的,我会尽力为您翻译。以下是需要翻译的内容:Check out this question: https://superuser.com/questions/459609/what-does-it-do-exactly-if-i-click-in-the-window-of-cmd请查看这个问题:https://superuser.com/questions/459609/what-does-it-do-exactly-if-i-click-in-the-window-of-cmd - Kfir Guy
好的,这不是 .net 特定的问题,谢谢。 - Tarun Khamar
2个回答

6
这是标准的控制台行为,它等待你的用户输入并锁定执行线程。要了解其原因,请查看Console.Write的实现。它只是写入到控制台输出(默认情况下是同步的TextWriter)(来源)。所以,“魔术”就在这里。
你可以通过委派专用线程来解决这个问题。例如,采用以下方式:
    static void timer_Elapsed(object sender, ElapsedEventArgs e)
    {
        Task.Factory.StartNew(() =>
        {
            Console.WriteLine("Writing to file " + DateTime.Now.ToString());
        });
        System.IO.File.AppendAllLines(@"C:\Temp\log.txt", 
          new[] { DateTime.Now.ToString()});
    }

因此,您的应用程序将继续写入文件,但只有在右键单击时才会将文本写入控制台。 这种实现的问题是它可能会创建许多线程,从而增加ThreadPool。

更好的实现可以通过某些特殊的TaskScheduler来完成,例如使用SequentialScheduler

    static TaskFactory factory = new TaskFactory(new SequentialScheduler());

    static void timer_Elapsed(object sender, ElapsedEventArgs e)
    {
        factory.StartNew(() =>
        {
            Console.WriteLine("Writing to file " + DateTime.Now.ToString());
        });
        System.IO.File.AppendAllLines(@"C:\Temp\log.txt", 
         new[] { DateTime.Now.ToString()});
    }

它不会增加线程池。其他实现方式也可以使用,但主要思想是将控制台写入委托给另一个可能被用户阻塞的线程,工作线程将解除阻塞并继续工作。


3

实施建议

如果您想避免将大量工作放在线程池上,仅仅为了将一些内容写入控制台,则队列是您的朋友。这也确保了消息的正确顺序,并为您提供了一些额外的控制(例如处理不重要的条目)。

创建一个控制台日志记录器线程,该线程从并发队列中读取要写入控制台的条目。请注意,如果控制台无限期地被阻塞,则队列最终会增长,直到内存用尽 - 如果您确实在此期间将数百万个条目加入队列中,则会发生这种情况。

示例:

    static ConcurrentQueue<string> consoleQueue = new ConcurrentQueue<string>();
    static ManualResetEventSlim itemEnqueuedEvent = new ManualResetEventSlim();

    static void WriteToConsoleLoop(object state)
    {
        var token = (CancellationToken)state;
        while (!token.IsCancellationRequested)
        {
            string entry;
            while (consoleQueue.TryDequeue(out entry))
            {
                Console.WriteLine(entry);
            }
            try
            {
                itemEnqueuedEvent.Wait(token);
                itemEnqueuedEvent.Reset();
            }
            catch (OperationCanceledException)
            {
                break;
            }
        }
    }

    static void WriteToConsole(string entry)
    {
        consoleQueue.Enqueue(entry);
        itemEnqueuedEvent.Set();
    }

    static void Main(string[] args)
    {
        var cts = new CancellationTokenSource();
        // Background or foreground, depends on how vital it is
        // to print everything in the queue before shutting down.
        var thread = new Thread(WriteToConsoleLoop) { IsBackground = true };
        thread.Start(cts.Token);

        WriteToConsole("Started...");

        // Do your stuff...

        cts.Cancel();
    }

4
可以使用BlockingCollection<string>来大大简化代码,而不是使用ConcurrentQueue<string>(BlockingCollection使用ConcurrentQueue作为其默认支持存储),你的整个while循环简化为foreach(var line in consoleQueue.GetConsumingEnumerable(cts.Token)) { Console.WriteLine(line); },并把try/catch移至循环外部。此时,WriteToConsole只需调用static void WriteToConsole(string entry) { consoleQueue.Add(entry); }即可。 - Scott Chamberlain
注意:如果您想等待所有消息写入完成,请调用consoleQueue.CompleteAdding()而不是cts.Cancel(),然后执行thread.Join()以等待线程完成队列中未处理的项目写出。 - Scott Chamberlain

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接