在多线程程序设计中,线程间的通信是一个复杂但至关重要的问题。为了确保数据的一致性和线程的安全性,需要采用一些同步机制。本文将探讨两种常用的线程间通信技术:消息队列和Windows消息队列。
消息队列是一种线程安全的通信机制,允许一个线程发送消息,而另一个线程接收并处理这些消息。这种机制的核心在于,消息的发送和接收都是线程安全的,从而避免了直接访问共享数据时可能产生的竞态条件。
发送者的任务是将命令消息传递给其他线程。这是一个相对简单的操作,只需将消息添加到一个线程安全的队列中,并使用信号量来高效地通知其他线程消息的可用性。信号量可以避免线程在没有消息时空转,从而减少CPU的使用率。
接收者的任务是在循环中查找消息。在循环内部,等待信号量,然后根据消息类型执行相应的操作。消息队列会持续存储消息,直到有线程唤醒并接收它们。这种机制允许消息的积压,这对于处理速度较慢的线程来说非常重要。
如果需要双向通信,需要创建额外的队列和信号量,并使用它们进行反向通信。在这种情况下,每个线程既是发送者也是接收者。
需要注意的是,在Windows Forms应用程序中,接收者不能是主应用程序线程。这是因为Windows Forms已经在主线程上运行了一个应用程序循环,无法直接访问它。
另一种与窗口化线程通信的方法是通过传递窗口消息。这种方法与消息队列类似,但需要子类化一个本地窗口。虽然在接收参数方面有一定的限制,因为只接受两个整数值,但可以使用Control.Invoke()来实现类似的功能。基本上,处理自定义窗口消息,并依赖Windows来维护一个同步的消息队列。
可以通过NativeWindow的WndProc()回调和PostMessage()方法来访问它,后者需要使用P/Invoke。
在多线程应用程序中,实现无窗口接收器相对简单。首先,定义了相关的消息队列成员变量:
ConcurrentQueue _messages = new ConcurrentQueue();
SemaphoreSlim _messagesAvailable = new SemaphoreSlim(0);
_messages队列用于存储待处理的消息,而_messagesAvailable信号量用于在有消息等待处理时发出信号。
还定义了各种消息的常量:
const int MSG_STOP = 0;
const int MSG_RESET = 1;
const int MSG_INCREMENT = 2;
const int MSG_DECREMENT = 3;
接下来,看看如何从一个发送线程传递消息:
_messages.Enqueue(new Message(MSG_INCREMENT, null));
_messagesAvailable.Release(1);
这里有两个步骤。首先,将消息添加到队列中。Message的第一个参数是消息ID,第二个参数是消息的参数数组。在演示中没有使用第二个参数。第二步是在信号量上调用Release(),以发出消息可用的信号。
现在让看看接收线程是如何工作的:
var thread = new Thread(() => {
var done = false;
while (!done) {
_messagesAvailable.Wait();
Message msg;
if (_messages.TryDequeue(out msg)) {
switch (msg.Key) {
case MSG_STOP:
done = true;
break;
case MSG_RESET:
_counter = 0;
break;
case MSG_INCREMENT:
++_counter;
break;
case MSG_DECREMENT:
--_counter;
break;
}
PostMessage(_uiReceiver.Handle, WM_USER, _counter, 0);
}
}
});
注意,可以使用Control.Invoke()来安全地更新显示。这只是将内容传回主线程的一种快速而简单的方法。然而,使用的是通过PostMessage()访问的窗口化消息传递技术。
关键在于循环等待信号量,然后尝试出队消息,并通过消息ID来确定要执行的操作。
如果需要在Windows Forms应用程序的主UI线程上接收消息,就需要使用这种方法。子类化一个NativeWindow,然后使用PostMessage()与之通信,让Windows本身处理同步。
这是需要的P/Invoke声明和常量:
[DllImport("user32.dll")]
static extern bool PostMessage(IntPtr hWnd, uint Msg, int wParam, int lParam);
const int WM_USER = 0x0400;
WM_USER常量是用于自定义窗口消息的,以避免与系统消息ID发生冲突。可以使用WM_USER, WM_USER+1, WM_USER+2, 一直到WM_USER+0x7FFF。同时,PostMessage()允许异步地向窗口发送消息。
这是NativeWindow的子类:
private class _NativeReceiver : NativeWindow {
Main _main;
public _NativeReceiver(Main main) {
_main = main;
}
protected override void WndProc(ref System.Windows.Forms.Message m) {
switch (m.Msg) {
case WM_USER:
_main.CounterLabel.Text = "Count: " + m.WParam.ToInt32();
break;
}
base.WndProc(ref m);
}
}
所做的就是等待WM_USER消息,然后将计数器标签设置为消息参数WParam的值。所有这些都发生在UI线程上。注意,WParam和LParam在这里是IntPtr类型,但在PostMessage()中它们是int类型。这没问题,只需将IntPtr转换为int即可。
最后,设置接收器的步骤是在主窗体的构造函数中进行的:
_uiReceiver = new _NativeReceiver(this);
var p = new CreateParams();
_uiReceiver.CreateHandle(p);
PostMessage()的调用相当简单,但缺点是它们只接受两个整数参数。幸运的是,不需要更多,但如果确实需要,就得变得聪明一些。无论如何,这是PostMessage()的调用:
PostMessage(_uiReceiver.Handle, WM_USER, _counter, 0);
可能已经注意到它之前被无窗口接收器调用过。可以看到正在使用第一个整数参数,仅此而已。这就是向_NativeReceiver.WndProc()发送消息的方式。
演示应用程序有4个按钮:Start/Stop, Reset, Increment (+), 和 Decrement (-)。消息的处理不会发生,直到点击Start。Reset重置计数器。Increment (+) 和 Decrement (-) 分别增加或减少计数器1。可以通过停止处理来排队消息,然后使用Start开始处理,此时所有待处理的消息都会被处理。每个操作都需要双向线程间通信来执行。