线程同步 - UI 线程和工作线程





4.00/5 (6投票s)
本文介绍工作线程如何接管 UI 并更新由 UI 线程创建的 UI。
引言
您在进行长时间的数据库操作、调用长时间运行的服务或进行网络连接(如连接 CD 驱动器等)时,是否曾遇到过基于 UI 的应用程序的问题?我猜您的答案会是“是”,而您可能希望在上述操作在后台运行时,您的 UI 应用程序能够继续响应您的操作,并在后台操作完成后立即在 UI 上显示结果。我希望如此,您不希望吗?
背景
简单的答案是,创建一个多线程应用程序,并让不同的线程管理不同的操作。人们在谈论软件编程时喜欢使用“多线程”这个词,但编写多线程程序并非易事。它可能导致整个应用程序崩溃,并需要数周时间来查找和修复问题。
任何应用程序(为避免争论,我现在说的是 Win UI 应用程序)默认都在一个线程上运行,因为线程是负责在操作系统分配的进程区域中执行程序的逻辑实体。由于此线程负责创建、绘制和管理可视化控件,因此通常将此默认线程称为 UI 线程,而负责在后台模式下执行操作/方法的线程通常称为工作线程。
根据多线程的经验法则,负责创建可视化控件的线程也必须在其整个生命周期内管理它们,工作线程绝不应该尝试管理它们。
到目前为止,我说的都不是什么新鲜事,我们都知道 :-)。在本文中,我将解释工作线程如何直接与可视化控件交互。
工作线程接管 UI
最近,我正在处理一项需求,其中 UI 需要处理用户输入的数据,将它们(以Request
对象的形式)放入Request
队列,同时 UI 还需要显示从队列收到的响应和错误(业务逻辑在队列的另一侧)。但问题是——我不能使用同一个 UI 线程同时轮询 3 个不同的队列(请求队列、响应队列和错误队列),因为一个线程在等待队列中的新消息时会被阻塞,而无法用于其他活动。所以唯一的选择是编写一个多线程程序,其中包含 3 个线程——每个队列一个。因此,我选择让 UI 线程接收用户输入并将其放入请求队列,并创建了两个工作线程——一个将不断轮询响应队列,另一个将轮询错误队列。
- UI 线程 - 接收用户输入并将其放入请求队列
- 工作线程 #1 - 查找响应队列中的新消息并在 UI 上显示(假设在一个列表框中)
- 工作线程 #2 - 查找错误队列中的新消息并在 UI 上显示(假设在一个列表框中)
有两种方法可以解决这个问题
- 让 UI 线程依次轮询两个工作线程,并显示从响应和错误队列接收到的数据。但在这种情况下,UI 线程将无限期地等待两个线程完成,导致 UI 无响应。
- 当工作线程在队列中找到新消息时,它将直接在 UI 控件上显示数据,而无需依赖 UI 线程来执行此活动。
我知道 .NET 中有很多方法可以做到这一点,也有很多关于线程同步以及工作线程如何加入主线程的文章。但如果我有一个解决方案可以实现方法 #2,那就再好不过了。
将用户输入数据发送到请求队列的代码应该很简单,对吧?代码看起来会是这样的
public static void Send()
{
System.Messaging.Message message = new System.Messaging.Message();
message.Formatter = new BinaryMessageFormatter();
RequestMessage reqMessage = new RequestMessage();
try
{
//Can set collected data from the UI
reqMessage.MsgText = "Message# " + (++_Counter).ToString();
reqMessage.Status = ProcessingStatus.SentFromClient;
message.Body = reqMessage;
RequestQ.Send(message, MessageQueueTransactionType.Single);
}
catch (Exception ex)
{
Logger.HandleError(reqMessage, ex);
}
}
在我看来,在工作线程上运行代码最简单的方法是使用异步委托调用。要异步调用委托,请调用BeginInvoke
,它将在 .NET 线程池中的一个线程上运行该方法。在这种情况下,UI 线程将立即返回,而无需等待工作线程完成。
不要为调用不接受任何输入参数的方法声明自己的委托,您可以使用System.Windows.Forms
命名空间中的MethodInvoker
委托。如果您的方法在工作线程上运行,并且需要任何输入参数,那么您应该声明自己的委托。
MethodInvoker
委托
private void Form_Load(object sender, EventArgs e)
{
MethodInvoker mi = new MethodInvoker(LongRunningMethod);
mi.BeginInvoke(null, null);
}
在我的例子中,我需要传递一个参数。所以我创建了自己的委托,它可以接受输入参数。
delegate void MethodAsyncCallDelegate(IConsole console);
private void Form_Load(object sender, EventArgs e)
{
//ReceiveResponse() method of MessageProcessor watches the Response Q
new MethodAsyncCallDelegate(MessageProcessor.ReceiveResponse)
.BeginInvoke((IConsole)this, null, null);
//ReceiveError() method of MessageProcessor watches the Error Q
new MethodAsyncCallDelegate(MessageProcessor.ReceiveError)
.BeginInvoke((IConsole)this, null, null);
}
为了更好地理解代码,让我们看看我的ReceiveResponse
/ReceiveError
代码。您的代码将基于您的需求。
//This runs on a worker thread
internal static void ReceiveResponse(IConsole console)
{
_Console = console;
MessageQueue _myQueue = new MessageQueue
(AppConstants.RESPONSE_QUEUE_PATH, QueueAccessMode.Receive);
_myQueue.Formatter = new BinaryMessageFormatter();
//Infinite Loop
while (true)
{
//Waits here till it finds a message in the Queue
System.Messaging.Message msg = _myQueue.Receive();
ResponseMessage responseMessage = (ResponseMessage)msg.Body;
//*** DISPLAY THE RESPONSE MESSAGE ON THE UI
}
}
//*** 在 UI 上显示响应消息 - 这里我需要显示从队列收到的响应数据,在 UI 上。现在我将告诉您一件有趣的事情(如果您已经知道了,请忽略文章的其余部分?),它将允许工作线程与 UI 线程进行交互。
Control
类的Invoke
方法允许任何工作线程发起调用。Invoke
方法接受一个委托和一个可选的参数列表,并在 UI 线程上调用该委托,无论哪个工作线程发起Invoke
调用。但请注意,此机制仅在 UI 线程当前未被阻塞并且处理在 UI 线程准备好处理时立即开始时才有效。这不会阻塞 UI 线程,并在用户继续在 UI 上输入数据时在后台运行。Invoke
方法安排线程切换,并在 UI 线程上调用由委托封装的方法。在这种情况下,工作线程将被阻塞,直到由委托封装的方法完成。为了避免阻塞工作线程,Invoke
还有一个异步版本,它立即返回并在 UI 线程上执行方法。像任何其他异步委托方法一样,Control
类也有一个相应的Begin
方法 - BeginInvoke
,它允许调用在 UI 线程上异步运行。
为了满足我的需求,我做了以下几点
- 在 UI 上创建了一个方法 -
DisplayResponse
,供上述ReceiveResponse
模式直接调用。 - 在
DisplayResponse
内部,调用 UIControl
的方法来显示响应数据。
所以,//*** 在 UI 上显示响应消息被替换为
_Console.DisplayResponse(responseMessage.MsgText, responseMessage.MessageID);
其中_Console
是对 UI 的引用,它作为输入参数传递给ReceiveResponse
方法。_Console
的DisplayResponse
方法大致如下
delegate void PopulateDelegate(string msgText, string msgID);
void IConsole.DisplayResponse(string msgText, string msgID)
{
if (InvokeRequired)
{
string[] args = new string[] { msgText, msgID };
this.BeginInvoke(new PopulateDelegate(PopulateResponse), args);
}
else
PopulateResponse(msgText, msgID);
}
private void PopulateResponse(string msgText, string msgID)
{
lstResponse.Items.Add("Received response for - " +
msgText + " and Message ID: " + msgID);
}
在DisplayResponse
方法中,通过调用 Form 控件的BeginInvoke
方法,在 UI 线程上执行控件处理方法(PopulateResponse
)。到目前为止,代码是在工作线程上执行的。检查 Form 控件的InvokeRequired
属性,以确保调用线程是工作线程。如果不是,则直接调用PopulateResponse
方法,而不是异步委托方法。如果我们非常确定DisplayResponse
将始终从工作线程调用,则不需要此InvokeRequired
属性检查。
结论
我相信,对于您(如果您还不知道的话),这是一种新的方法,您可以使用它比 UI 线程等待工作线程发出信号或完成来同步 UI 和工作线程,效率更高。
这个特定的解决方案或方法在工作线程需要通知 UI 线程后台工作的进度,并且 UI 线程需要以进度条的形式显示后台工作的进度而不中断用户工作的情况下会非常有用。
我请求所有读者对本文提供宝贵的评论。
历史
- 2010 年 5 月 15 日:首次发布