多线程应用中的线程间通信技术

多线程程序设计中,线程间的通信是一个复杂但至关重要的问题。为了确保数据的一致性和线程的安全性,需要采用一些同步机制。本文将探讨两种常用的线程间通信技术:消息队列和Windows消息队列。

消息队列

消息队列是一种线程安全的通信机制,允许一个线程发送消息,而另一个线程接收并处理这些消息。这种机制的核心在于,消息的发送和接收都是线程安全的,从而避免了直接访问共享数据时可能产生的竞态条件。

发送者的任务是将命令消息传递给其他线程。这是一个相对简单的操作,只需将消息添加到一个线程安全的队列中,并使用信号量来高效地通知其他线程消息的可用性。信号量可以避免线程在没有消息时空转,从而减少CPU的使用率。

接收者的任务是在循环中查找消息。在循环内部,等待信号量,然后根据消息类型执行相应的操作。消息队列会持续存储消息,直到有线程唤醒并接收它们。这种机制允许消息的积压,这对于处理速度较慢的线程来说非常重要。

如果需要双向通信,需要创建额外的队列和信号量,并使用它们进行反向通信。在这种情况下,每个线程既是发送者也是接收者。

需要注意的是,在Windows Forms应用程序中,接收者不能是主应用程序线程。这是因为Windows Forms已经在主线程上运行了一个应用程序循环,无法直接访问它。

Windows消息队列

另一种与窗口化线程通信的方法是通过传递窗口消息。这种方法与消息队列类似,但需要子类化一个本地窗口。虽然在接收参数方面有一定的限制,因为只接受两个整数值,但可以使用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开始处理,此时所有待处理的消息都会被处理。每个操作都需要双向线程间通信来执行。

沪ICP备2024098111号-1
上海秋旦网络科技中心:上海市奉贤区金大公路8218号1幢 联系电话:17898875485