Windows Mobile .NET 本地线程同步






4.07/5 (8投票s)
本文介绍 Windows Mobile 提供的本地同步对象以及如何使用它们。
引言
在 MSDN 的智能设备开发论坛中,我看到过几次关于需要哪些本地同步对象来解决问题的指导请求。由于不止一次遇到这些问题,我写了这篇关于 Windows Mobile 设备上本地事件的介绍。本文档专门为托管代码开发人员编写,但本地代码开发人员在解读本文代码示例时应该不会遇到困难。
什么是同步对象?我为什么需要它们?
同步对象用于协调多个线程正在执行的操作。最简单的形式下,同步对象可以用来确保多个线程不会同时修改同一个对象,但它们也可以用于促进程序之间的通信。.NET Compact Framework 包含许多可用于线程级别同步的托管类,这些类位于 Threading
命名空间中。
类 | 框架版本 |
---|---|
Interlocked |
2.0 |
ManualResetEvent |
1.0 |
显示器 |
1.0 |
WaitHandle |
1.0 |
EventWaitHandle |
1.0 |
这些类在同一进程的上下文中工作,用于协调该进程内线程的操作。我在这里讨论的大部分操作系统提供的功能都可以跨越进程边界。我不会在这里讨论托管同步类,只讨论本地同步函数。在撰写本文时,操作系统提供的同步对象不受 .NET Framework 的直接支持,因此我们需要使用 P/Invoke 来访问它们。我将介绍以下对象:
对象 | 描述 |
---|---|
事件 |
用于向线程发送信号以继续执行 |
信号量 |
限制可以访问资源的线程数量 |
互斥体 |
用于确保一次只有一个线程可以访问资源 |
我已经将本地同步对象的本地函数调用封装到了单独的类中。抽象类 SyncBase
包含了这里定义的三个同步对象共有的功能。这三个同步类中的每一个都派生自这个抽象类。请注意,这些类实现了 IDisposable
。这些类会持有 Windows 句柄(这是系统资源),并且这些句柄不应被持有超过必要的时间。
虽然您可以随意使用此代码,但我强烈建议您研究 MSDN 中有关同步函数的文档。我对可用函数的用法并非详尽无遗,而是针对我认为最常用的功能。因此,同步函数还有其他我在这里未提及的功能。
事件
事件是最简单的同步对象。它们就像交通信号灯;等待事件的代码会暂停,直到事件变为已发出信号的状态。事件可以选择性地命名。命名事件可以在多个进程之间共享。由于 .NET Framework 对系统事件没有本地支持,我们需要使用 P/Invoke 来与之交互。CreateEvent
函数用于创建事件,它定义在 CoreDLL 中。事件可以是手动重置事件或自动重置事件。对于手动重置事件,一旦事件被设置为已发出信号的状态,它将一直保持该状态,直到使用 ResetEvent
函数将其显式更改为非信号状态。当事件处于已发出信号状态时,所有尝试等待该事件的线程都将继续运行而不会被阻塞。对于自动重置事件,一旦事件被设置为已发出信号的状态,它将一直保持该状态,直到一个线程等待它。一旦线程等待了它,它将被允许继续,事件将自动重置为非信号状态。为了暂时将事件设置为已发出信号的状态,有一个名为 PulseEvent
的函数。PulseEvent
的行为取决于它是被用于手动重置事件还是自动重置事件。当针对手动重置事件调用时,所有当前可以被解除阻塞的线程都会被解除阻塞,并且事件会变回非信号状态。对于自动重置事件,一个线程会从阻塞中释放出来,然后事件会返回到非信号状态。
CreateEvent
的调用签名在 C++ 头文件中定义如下:
HANDLE CreateEvent( LPSECURITY_ATTRIBUTES lpEventAttributes, BOOL bManualReset, BOOL InitialState, LPTSTR lpName );
参数 | 描述 |
---|---|
LPSECURITY_ATTRIBUTES |
这应该始终为 null 。 |
bManualReset |
控制是创建手动重置事件还是自动重置事件。 |
InitialState |
指定事件在创建时是否处于已发出信号的状态。 |
lpName |
要创建的事件的名称。如果没有指定名称,则创建一个未命名事件,该事件仅在其创建的进程中有意义。 |
调用它需要使用 P/Invoke
[DllImport("CoreDLL", SetLastError=true)]
public static extern IntPtr CreateEvent(
IntPtr AlwaysNull0,
[In, MarshalAs(UnmanagedType.Bool)] bool ManualReset,
[In, MarshalAs(UnmanagedType.Bool)] bool bInitialState,
[In, MarshalAs(UnmanagedType.BStr)] string Name
);
如果您尝试为 SetEvent
、PulseEvent
和 ResetEvent
进行适当的 P/Invoke 语句,它们都会失败。它们失败是因为这些函数都不存在于任何 Windows Mobile DLL 中。如果您查看名为“kfuncs.h”的 C++ 头文件,您会发现这些函数被定义为对 EventModify
的内联调用。对 EventModify
的调用中的第一个参数是我们要使用的事件的句柄,第二个参数是一个表示要对事件执行的操作的数值。在我的代码中,我为 EventModify
进行了 P/Invoke 语句,并为 SetEvent
、ResetEvent
和 PulseEvent
声明了相应的调用。
您几乎拥有开始使用事件所需的所有信息。最后一个缺失的组件是当您不再使用事件句柄时必须做什么。当您不再需要事件时,通过调用 CloseHandle
来释放它。现在,让我们来看第一个代码示例。我已经声明了我们将用于访问上述函数的 P/Invoke 语句,在一个名为 NativeSync 的项目中。如果您打开 CoreDLL.cs,您会找到 CreateEvent
、EventModify
和 CloseHandle
的 P/Invoke。我还创建了一个名为 SystemEvent
的类来封装与事件相关的函数。该类实现了 IDisposable
,以便在不再使用事件时可以释放句柄。
第一个代码示例位于一个名为“SoundOnEvent”的项目中。该程序有两个线程。除了 GUI 线程之外,还有一个线程,其入口点在 SoundLoop
方法中,定义如下。SoundLoop
会等待一个事件,并在事件发出信号时播放声音。承载此程序的窗体只有一个按钮。该按钮的事件处理程序会调用另一个事件对象的 SetEvent
。请注意,按钮的事件处理程序使用的 Event
对象实例与 SoundLoop
方法中的实例不同。由于两个实例都是使用相同的名称创建的,操作系统将赋予这两个对象实例相同的内核对象。
SystemEvent _soundEvent;
private void Form1_Load(object sender, EventArgs e)
{
_soundEvent = new SystemEvent("PlaySound", false, false);
Thread t = new Thread(new ThreadStart(SoundLoop));
t.Start();
}
void SoundLoop()
{
using (SystemEvent evt = new SystemEvent("PlaySound", false, false))
{
while (_continuePlaying)
{
evt.Wait();
if (_continuePlaying)
{
SndPlaySync(SoundPath, 0);
}
}
}
}
private void cmdCreateEvent_Click(object sender, EventArgs e)
{
_soundEvent.SetEvent();
}
while
循环看起来非常像一个经常被不推荐使用的反模式,称为忙等待或对变量进行自旋。虽然代码看起来像忙等待,但实际上不是;在忙等待中,代码块会被不必要地重复执行,不执行任何实际工作,同时消耗 CPU 周期。在上面的代码中,线程会完全暂停,直到需要执行工作,并且不会浪费 CPU 周期。
虽然一个事件的句柄不能直接传递给另一个进程(句柄只在其创建的进程中有意义),但如果另一个进程尝试创建一个同名的事件,它将收到一个指向已存在的对象的自己的句柄。如果另一个程序调用了这个相同事件的 SetEvent
,那么第一个程序就会对其做出反应(无需修改任何代码!)。
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using NativeSync;
namespace SoundOnEventTrigger
{
public partial class Form1 : Form
{
SystemEvent _soundEvent = new SystemEvent("PlaySound", false, false);
public Form1()
{
InitializeComponent();
}
private void cmdCreateEvent_Click(object sender, EventArgs e)
{
_soundEvent.SetEvent();
}
private void miExit_Click(object sender, EventArgs e)
{
this.Close();
}
private void Form1_Closing(object sender, CancelEventArgs e)
{
_soundEvent.Dispose();
}
}
}
等待多个对象
当您有一个对象集合并希望等待其中任何一个发出信号时,可以使用 WaitForMultipleObjects
方法。在桌面版本的 Windows 上,此函数可用于等待所有事件发出信号,或等待其中任何一个发出信号。对于 Windows CE 操作系统,此函数只能用于等待任何一个事件发生。该函数的本地原型如下:
DWORD WINAPI WaitForMultipleObjects(
__in DWORD nCount,
__in const HANDLE *lpHandles,
__in BOOL bWaitAll,
__in DWORD dwMilliseconds
);
第一个参数是要传递的同步对象的数量,第二个参数是对象句柄的数组。第三个参数必须始终为 false
(在桌面操作系统上,如果第三个参数为 true
,则该方法将等待所有对象发出信号)。最后一个参数是要等待其中一个对象发出信号的毫秒数,或者 -1 表示不超时。在我的代码中,我已经简化了调用签名,使其仅接受超时值和要等待的同步对象列表。该方法在 SyncBase
类上实现,因为系统事件不是程序可以等待的唯一对象类型(我们稍后将介绍其他类型的对象)。
演示 WaitForMultipleObjects
的示例代码与第一个示例类似。对于 WaitOnMultipleObjects
函数的封装,我让该方法返回一个引用,该引用指向导致线程被解除阻塞的已发出信号的对象。示例程序将使用哪个事件被发出信号的信息来决定播放哪个声音。用户可以通过界面上的三个按钮来发出其中三个系统事件对象的信号。还有一个第四个事件,这个程序的用户界面无法访问。第四个事件的名称与第一个示例程序中使用的事件名称相同,因此可以使用 SoundOnEventTrigger
来触发第四个事件。
void SoundLoop()
{
using (SystemEvent evt =
new SystemEvent("PlaySound", false, false))
{
int target;
while (_continuePlaying)
{
SystemEvent signaledEvent = SyncBase.WaitForMultipleObjects(
_event1,
_event2,
_event3,
_playSoundEvent
) as SystemEvent;
if (_continuePlaying)
{
target = 4;
if (signaledEvent == _event1)
target = 1;
if (signaledEvent == _event2)
target = 2;
if (signaledEvent == _event3)
target = 3;
SndPlaySync(String.Format(SoundPath, target), 0);
}
}
}
}
一旦您了解了如何使用事件对象,您就掌握了使用互斥锁和信号量的基础;它们都扩展了事件的概念。
互斥体
互斥锁用于确保只有一个线程可以访问资源。互斥锁处于已发出信号的状态,直到一个线程等待它。当一个线程等待互斥锁,并且没有其他线程正在等待该互斥锁时,它就会获得互斥锁的所有权,其等待函数返回,并且互斥锁进入未发出信号的状态,以确保没有其他等待同一信号量的线程可以继续。当后续线程尝试等待互斥对象时,它们会被阻塞。当一个线程完成使用互斥锁后,它可以调用 ReleaseMutex
,下一个被阻塞的线程将被解除阻塞并获得互斥锁的所有权。演示互斥体使用情况的程序有四个线程,它们什么也不做,只是计数。每个线程的计数显示在不同的文本框中。
每个线程执行的源代码如下。我在代码中插入了对 Sleep
的调用,使其运行时间更长,以便您更容易地观察线程执行时的用户界面。
void StartCounting()
{
int index = Interlocked.Increment(ref lastIndex);
using (NativeSync.Mutex m = new NativeSync.Mutex(MutexName,false))
{
mutex.Wait();
for (int i = 0; (i < 50) && (_continueRunning); ++i)
{
SetTextCount(index, i);
Thread.Sleep(100);
}
mutex.ReleaseMutex();
}
}
当代码运行时,一次只有一个计数器在执行。一旦它完成工作,下一个计数器就会开始,因为互斥锁一次只允许一个线程进入计数循环。
信号量
信号量类似于互斥锁。它也限制可以访问资源的线程数量。与互斥锁不同,您可以使用信号量来允许一个以上的线程访问资源。信号量有一个计数属性。计数可以从 0 开始, up to a maximum amount that is decided when the semaphore is created。当计数大于零时,信号量处于已发出信号的状态。当一个线程等待信号量时,如果信号量处于已发出信号的状态(这意味着计数大于零),那么计数就会减一。当线程完成使用资源时,它应该调用 ReleaseSemaphore
来增加计数。
为了演示信号量的使用,我采用了与使用互斥锁的相同程序,并将其修改为等待信号量。此代码示例中使用的信号量允许最多两个线程访问它。因此,当运行此程序时,您将看到两个计数器同时工作。
下一步
Windows Mobile 操作系统提供了比本文所述更多的同步功能。我只想在这篇文章中关注事件及其衍生物。同步函数还支持进程间通信、监控进程终止以及许多其他功能。(请参阅 MSDN 的文档。)