线程安全日志记录不适用于调试器中的任务或线程
Threadsafe logging not working with Task or Thread in debugger
我尝试将来自 Task
或 Thread
的文本消息记录到我表单上的文本框。为此,我使用 Invoke
和 InvokeRequired
方法与主线程同步,因为我可以在互联网上的许多示例中找到。请参阅下面的 LogMessage_Delegate
和 LogMessage_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
方法中
我尝试将来自 Task
或 Thread
的文本消息记录到我表单上的文本框。为此,我使用 Invoke
和 InvokeRequired
方法与主线程同步,因为我可以在互联网上的许多示例中找到。请参阅下面的 LogMessage_Delegate
和 LogMessage_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
方法中