许多应用程序,尤其是 Tablet PC 应用程序,大都是单实例应用程序。单实例应用程序是指,始终只有一个执行该应用程序的进程在运行的应用程序,无论是在特定桌面应用还是整个计算机中均是如此,取决于您对“单实例”的定义以及对应用程序的需求。对于 Sudoku,我想要计算机上的每个用户每次只能运行 Sudoku 的一个实例。如果使用 Microsoft Visual Studio 2005 和 .NET Framework 2.0 来实现 Sudoku,则单实例支持已经可用。但我使用的是 .NET Framework 1.1,所以还需其他工作才能实现单实例支持。有关单实例支持的详细信息,请参阅 .NET Matters column in MSDN Magazine's September, 2005 issue(英文)。
我的单实例支持内置于 SingleInstance 类中,位于 Utilities 文件夹的 SingleInstance.cs 文件中。MainInternal 按照下列方式使用 SingleInstance。
using(SingleInstance si = new SingleInstance())
{
if (si.IsFirst)
{
//创建主窗体并运行应用程序
...
//成功完成游戏
return true;
}
else
{
//不是第一个 Sudoku 实例... 显示另外一个
si.BringFirstToFront();
return false;
}
}
我首先创建一个新的类实例并查询其 IsFirst 属性,以确定该应用程序当前是否有其他实例正在运行。如果这是第一个实例,则应用程序继续正常运行,创建并显示应用程序的主窗体。如果这不是第一个实例,BringFirstToFront 方法会把 Sudoku 应用程序第一个实例的窗口转至前台,然后退出第二个实例。
单实例功能需要某种进程间通信 (IPC) 的方式,这是由于一个实例需要通知其他实例已经有实例存在,不应继续加载第二个实例。SingleInstance 类实际上依赖两个不同的 IPC 机制:进程间同步原语和内存映射文件。
创建单纯的单实例功能十分简单,只需几行代码即可完成。
static void Main()
{
string mutexName = "2ea7167d-99da-4820-a931-7570660c6a30";
bool grantedOwnership;
using(Mutex singleInstanceMutex =
new Mutex(true, mutexName, out grantedOwnership)
{
if (grantedOwnership)
{
... //此处为核心代码
}
}
}
当双线程或更多线程同时访问共享资源时,您需要有同步机制来确保在某一时间点只有一个线程可以访问该资源。对于我们而言,“资源”的逻辑含义要比其物理上的含义广阔的多。它是保证正在运行的应用程序只有一个实例的能力。所谓 mutex,如在 System.Threading.Mutex 类中所实现的,是在某一时间点,仅向一个线程授予独占访问权限的同步原语。如果一个线程获得 Mutex,则暂停其他请求相同 Mutex 的线程,直到第一个线程释放 Mutex 为止。或者,在某一线程等待资源时不将其暂停,而是在其尝试获得 Mutex 时被告知该 Mutex 已被其他线程占用。
此处,以 GUID 名称创建 Mutex(所有使用此类代码的不同应用程序将会选择其唯一的名称)。在您使用某一名称创建 Mutex 之后,就可以用来同步存在于不同进程之间的线程,而每一进程都可以根据其预设和已知的名称来引用 Mutex。Mutex 的一个构造函数不仅接受名称,还接受 out 布尔参数,该参数用于指示 Mutex 是否实际可得。这样就可以轻松创建单实例功能。该代码会创建 Mutex 并检查其是否可用。如果 Mutex 可用,则该应用程序的实例就是第一个实例,可以继续进行。如果 Mutex 不可用,那么它就是第二个实例,应该退出。
在内部,SingleInstance 围绕相同的原则构建,但是稍有不同。首先,SingleInstance 不使用硬编码 GUID,而是基于当前应用程序中输入程序集的身份生成 ID。
private static string ProgramID
{
get
{
Assembly asm = Assembly.GetEntryAssembly();
Guid asmId = Marshal.GetTypeLibGuidForAssembly(asm);
return asmId.ToString("N") + asm.GetName().Version.ToString(2);
}
}
ID 是程序集类型库 GUID 和程序集版本的组合。按照下列方式使用 ID 来构造 Mutex 名称。
string mutexName = "Local\" + ProgramID + "_Lock";
如同上一个简单的单实例示例,我的 SingleInstance 类构造函数对 Mutex 进行实例化,尽管是将其存储在成员变量而非本地变量中。
_singleInstanceLock = new Mutex(true, mutexName, out _isFirst);
构造函数的第三个参数是布尔变量,该变量用于存储此 Mutex 实例能否获得锁定,然后可以通过 IsFirst 属性访问该布尔值。
public bool IsFirst { get { return _isFirst; } }
SingleInstance 实现的 IDisposable.Dispose 会关闭 Mutex。
如果这正是所要求的功能,那么我的工作就基本上完成了。但是,在试图启动第二个实例时,许多单实例应用程序还会把现有实例转至前台。这一功能要求可在进程间传递附加信息。尤其是,除了检测是否已经存在实例外,第二个实例必须能够确定应将哪个进程转至前台。我注意到,一些应用程序是通过搜索桌面上具有特定名称的所有顶层窗口来完成这一任务,但这种方法不太可靠。我采用的是另外一种方式,内存映射文件。
对于我们的情况,可以将内存映射文件看作一种内存共享方式。这允许一个进程可以向内存中写入信息,供其他进程读取。特别是,应用程序的第一个实例可向内存映射文件中写入其进程 ID,然后第二个实例可以读取该进程 ID 并使用该 ID 将主实例转至前台。
private void WriteIDToMemoryMappedFile(uint id)
{
_memMappedFile = new HandleRef(this,
NativeMethods.CreateFileMapping(new IntPtr(-1), IntPtr.Zero,
PAGE_READWRITE, 0, IntPtr.Size, _memMapName));
if (_memMappedFile.Handle != IntPtr.Zero &&
_memMappedFile.Handle != new IntPtr(-1))
{
IntPtr mappedView = NativeMethods.MapViewOfFile(
_memMappedFile.Handle, FILE_MAP_WRITE, 0, 0, 0);
try
{
if (mappedView != IntPtr.Zero)
Marshal.WriteInt32(mappedView, (int)id);
}
finally
{
if (mappedView != IntPtr.Zero)
NativeMethods.UnmapViewOfFile(mappedView);
}
}
}
private uint ReadIDFromMemoryMappedFile()
{
IntPtr fileMapping = NativeMethods.OpenFileMapping(
FILE_MAP_READ, false, _memMapName);
if (fileMapping != IntPtr.Zero && fileMapping != new IntPtr(-1))
{
try
{
IntPtr mappedView = NativeMethods.MapViewOfFile(
fileMapping, FILE_MAP_READ, 0, 0, 0);
try
{
if (mappedView != IntPtr.Zero)
return (uint)Marshal.ReadInt32(mappedView);
}
finally
{
if (mappedView != IntPtr.Zero)
NativeMethods.UnmapViewOfFile(mappedView);
}
}
finally { NativeMethods.CloseHandle(fileMapping); }
}
return 0;
}
类似于进程间的互斥锁,也可以命名内存映射文件,以便多个进程通过该名称访问。可使用 kernel32.dll 提供的 CreateFileMapping 函数创建内存映射文件。然后使用同样由 kernel32.dll 提供的 MapViewOfFile 函数将内存映射文件映射到当前地址空间。MapViewOfFile 为内存提供起始地址,然后与来自 System.Runtime.InteropServices.Marshal 类的方法一起使用,读取内存中的数据以及向内存中写入数据。如果已经创建了内存映射文件,您可以使用来自 kernel32.dll 的 OpenFileMapping 函数打开该文件。
[DllImport("Kernel32", CharSet=CharSet.Auto, SetLastError=true)]
internal static extern IntPtr CreateFileMapping(IntPtr hFile,
IntPtr lpAttributes, int flProtect, int dwMaxSizeHi,
int dwMaxSizeLow, string lpName);
[DllImport("Kernel32", CharSet=CharSet.Auto, SetLastError=true)]
internal static extern IntPtr OpenFileMapping(int dwDesiredAccess,
[MarshalAs(UnmanagedType.Bool)] bool bInheritHandle,
string lpName);
[DllImport("Kernel32", CharSet=CharSet.Auto, SetLastError=true)]
internal static extern IntPtr MapViewOfFile(IntPtr hFileMapping,
int dwDesiredAccess, int dwFileOffsetHigh, int dwFileOffsetLow,
int dwNumberOfBytesToMap);
[DllImport("Kernel32", CharSet=CharSet.Auto, SetLastError=true)]
[return:MarshalAs(UnmanagedType.Bool)]
internal static extern bool UnmapViewOfFile(IntPtr pvBaseAddress);
[DllImport("kernel32.dll", SetLastError=true)]
[return:MarshalAs(UnmanagedType.Bool)]
internal static extern bool CloseHandle(IntPtr handle);
SingleInstance 构造函数创建 Mutex 并验证其是否为第一个实例。如果是第一个实例,SingleInstance 构造函数使用 WriteIDToMemoryMappedFile 方法来存储自己的进程 ID,以供第二个实例随后检索之用。当第二个实例开始运行并调用 BringFirstToFront 方法时,第二个实例首先使用 ReadIDFromMemoryMappedFile 来访问主实例的进程 ID。可将该进程 ID 与 user32.dll 提供的 ShowWindowAsync 和 SetForegroundWindow 函数结合使用,将主实例转至前台。
public void BringFirstToFront()
{
if (!_isFirst)
{
uint processID = ReadIDFromMemoryMappedFile();
if (processID != 0)
{
const int SW_SHOWNORMAL = 0x1;
const int SW_SHOW = 0x5;
IntPtr hwnd = new ProcessInfo(processID).MainWindowHandle;
if (hwnd != IntPtr.Zero)
{
int swMode = NativeMethods.IsIconic(new HandleRef(
this, hwnd)) ?SW_SHOWNORMAL :SW_SHOW;
NativeMethods.ShowWindowAsync(hwnd, swMode);
NativeMethods.SetForegroundWindow(new HandleRef(
this, hwnd));
}
}
}
}

RSS订阅