线程安全日志记录不适用于调试器中的任务或线程

Threadsafe logging not working with Task or Thread in debugger

我尝试将来自 TaskThread 的文本消息记录到我表单上的文本框。为此,我使用 InvokeInvokeRequired 方法与主线程同步,因为我可以在互联网上的许多示例中找到。请参阅下面的 LogMessage_DelegateLogMessage_Threadsafe。当我关闭应用程序时,布尔标志 finished 设置为 true 并且 task/thread 应该停止工作。

一切正常,直到我在 Form1_FormClosing 事件处理程序 (finished = true;) 的第一行设置断点。然后我只看到控制台消息 "LogMessage InvokeRequired",但没有相应的 "LogMessage",应用挂起。

如果我注释掉 Work 中的 LogMessage_Threadsafe 调用(仅控制台消息),它会再次运行。正如预期的那样,应用正在关闭。

那么,有人可以向我解释一下这种行为吗?我找不到理由。

请注意,我在 Form1_FormClosing 事件处理程序中进行了标记,因此表单仍然有效。

namespace MultiThreadedTest
{
    public partial class Form1 : Form
    {
        //************************************************************
        // Fields

        Thread worker = null;
        Task task = null;
        bool finished = false;


        //************************************************************
        // Constructor

        public Form1()
        {
            InitializeComponent();

            worker = new Thread(Work);
            worker.Start();

            //task = Task.Factory.StartNew(Work);
        }


        //************************************************************
        // Helper methods

        public void LogMessage(string sMessage)
        {
            LogTextBox.Text += sMessage + Environment.NewLine;
        }

        /// <summary>
        /// Threadsafe wrapper for LogMessage
        /// </summary>
        delegate void LogMessage_Delegate(string sMessage);
        public void LogMessage_Threadsafe(string sMessage)
        {
            // InvokeRequired required compares the thread ID of the
            // calling thread to the thread ID of the creating thread.
            // If these threads are different, it returns true.
            if (this.InvokeRequired)
            {
                Console.WriteLine("LogMessage InvokeRequired");

                LogMessage_Delegate callback = new LogMessage_Delegate(LogMessage_Threadsafe);
                this.Invoke(callback, new object[] { sMessage });
            }
            else
            {
                Console.WriteLine("LogMessage");

                LogMessage(sMessage);
            }
        }


        //************************************************************
        // Commands

        void Work()
        {
            while (!finished)
            {
                Console.WriteLine("Tread/Task Waiting...");
                LogMessage_Threadsafe("Tread/Task Waiting...");
                Thread.Sleep(1000);  // Wait a little...
            }
            Console.WriteLine("Thread/Task Done");
        }


        //************************************************************
        // Events

        private void Form1_FormClosing(object sender, FormClosingEventArgs e)
        {
            finished = true;

            if (worker != null) worker.Join();

            if (task != null) Task.WaitAll(task);

            Console.WriteLine("App Done");
        }
    }
}

如果您在 UI 线程的断点处暂停,通过 Invoke 编组到 UI 线程的调用将不会执行,因为它们 运行在暂停的 UI 线程上。

但从您的评论来看,这似乎不是问题所在。所以我猜问题是,通过在那个断点处暂停,你已经允许后台线程进入它在 Invoke() 上阻塞的状态,然后你试图加入那个后台线程,这将阻塞直到 Invoke 完成,这永远不会发生。

作为一个单独的问题,如果您从多个线程访问 finished,则需要用 lock 块包围读取和写入以确保线程安全。

自 .NET 4 引入任务以来,您无需使用原始线程。 Invoke 也不是必需的,但自 .NET 4.5 引入 async/await 以来就已过时。 4.5 还通过 IProgress< T> inteface and Progress< T> implementation, as explained in Async in 4.5: Enabling Progress and Cancellation in Async APIs 引入了线程安全的进度报告和取消。

Progress<T> 在创建它的线程上调用它的委托,在本例中是 UI 线程。您可以将接口传递给任何后台方法(任务、线程方法等)并使用它来报告进度。

鉴于最早支持的 .NET 版本是 4.5.2,您可以假设这些 类 将始终可用。顺便说一句,在 4.5.2 中添加了对 TLS 1.2 的支持,因此任何坚持者都已经被迫升级,因为他们发现他们无法连接到 GMail 或其他需要 TLS 1.2 的服务。

通过使用这些 类,您的代码可以简化 很多。下面是一个带有后台计时器和线程安全报告的 quick&dirty 表单:

public partial class Form1 : Form
{
    System.Threading.Timer _timer;

    IProgress<string> _progress;

    public Form1()
    {
        InitializeComponent();
        _progress = new Progress<string>(msg => textBox1.Text += msg + "\r\n");
        _timer = new System.Threading.Timer(theCallback);
    }

    private async void theCallback(object state)
    {
        for (int i = 0; i < 5; i++)
        {
            await Task.Delay(100);
            _progress.Report($"Boo {i}");
        }
    }

    private void Form1_Load(object sender, EventArgs e)
    {            
            _timer.Change(0, 10000);
    }

    private void Form1_FormClosing(object sender, FormClosingEventArgs e)
    {
        _timer.Dispose();
        _timer = null;
        _progress = null;
    }
}

更新

至于为什么原来的代码会阻塞,是因为Thread.Join()是在Form.Closing中从UI线程调用的。如果后台线程试图调用 Invoke 来编组对 UI 线程的调用,它自己就会被阻塞,因为 UI 线程被阻塞了。

这可以通过调用 BeginInvoke 而不是 Invoke() 来避免。这是在 .NET 4 之前将回调处理到 UI 线程的典型方法。

阻塞行为可以在 Parallel Stacks 调试器 window (Debug / Windows / Parallel Stacks) 中看到。窗体死锁时出现两个栈,一个在Form.Closing方法中,一个在LogMessage_Threadsafe方法中