摘要
本文阐述了在基于NET平台的应用程序开发中如何实现唯一应用程序运行实例对几种实现方式进行分析测试比较从而寻找一种合适的处理方式单击此处才查看本文的示例代码
概述
在开发一些应用系统的时候由于程序内在的一些特征系统的某些组成子程序只允许运行一个应用程序实例以保证业务和数据处理安全本文将从实际应用角度来分析其实现原理对三种实现方式进行测试比较从而确定一种合适的实现方法文章的例子使用C#语言进行描述
进程匹配
对于每一个应用程序运行实例都会包含该实例的一个或多个进程而且在程序运行过程中可能会动态的创建或销毁进程或者访问其他现有进程进行通信不难发现在程序最先初始化的那一刻只有一个进程运行而且应用程序进程生命周期最大进程名称集合是不变的因此在应用程序初始化的时候可以根据进程关键信息检查系统进程列表是否存在同当前初始化进程匹配的进程来确定是否已经运行进程实例
逻辑处理步骤如下
.初始化应用程序启动程序初始化进程
.访问系统进程列表根据初始化进程关键信息进行匹配查找
.没有找到匹配进程(这一步是不会发生的因为当前初始化进程也在列表中不过还要看获取进程列表的实现代码怎么写)继续初始化进程程序初始化完成运行
.找到第一个匹配进程判断找到的进程ID是否同初始化进程ID相同
.如果第一个匹配进程ID同初始化进程ID相同则为当前初始化进程继续查找
.没有找到第二个匹配进程表明当前运行的是首个实例继续初始化进程程序初始化完成运行
.找到第二个表明已有一个实例在运行停止当前程序初始化提示已有应用程序运行
.如果找到第一个匹配进程ID不同表明已有一个实例在运行停止当前程序初始化提示已有应用程序运行
可见上面的逻辑实现中用于进程匹配的信息是关键选择不当功能就无法实现在这个实例中笔者使用了应用程序完全文件名称作为关键信息
在代码中首先需要引用下面命名空间以调用WinAPI函数
using SystemRuntimeInteropServices;
把实现唯一运行实例功能的类名取为SingleInstance在类前面加static关键字为C# 新增的语言特征
public static class SingleInstance {}
使用GetRunningInstance静态方法获取应用程序进程实例如果没有匹配进程返回Null值
public static Process GetRunningInstance()
{
Process currentProcess = ProcessGetCurrentProcess(); //获取当前进程
//获取当前运行程序完全限定名
string currentFileName = currentProcessMainModuleFileName;
//获取进程名为ProcessName的Process数组
Process[] processes = ProcessGetProcessesByName(currentProcessProcessName);
//遍历有相同进程名称正在运行的进程
foreach (Process process in processes)
{
if (processMainModuleFileName == currentFileName)
{
if (processId != currentProcessId) //根据进程ID排除当前进程
return process;//返回已运行的进程实例
}
}
return null;
}
接下来调用两个WinAPI其功能将在包装方法中描述
[DllImport(Userdll)]
private static extern bool ShowWindowAsync(IntPtr hWnd int cmdShow);
[DllImport(Userdll)]
private static extern bool SetForegroundWindow(IntPtr hWnd);
定义类成员辅助变量
private const int WS_SHOWNORMAL = ;
以上的方法声明为私有对其进一步包装HandleRunningInstance静态方法为获取应用程序句柄设置应用程序为前台运行并返回bool值
public static bool HandleRunningInstance(Process instance)
{
//确保窗口没有被最小化或最大化
ShowWindowAsync(instanceMainWindowHandle WS_SHOWNORMAL);
//设置为foreground window
return SetForegroundWindow(instanceMainWindowHandle);
}
对上面的方法创建一个重载版本使调用代码更加简洁
public static bool HandleRunningInstance()
{
Process p = GetRunningInstance();
if (p != null)
{
HandleRunningInstance(p);
return true;
}
return false;
}
上面的方法实现获取已经运行的进程实例的句柄并获取其焦点显示到前台这个很有用在其他实现方式中也可以用到
在Main函数中调用下面代码实现单一应用程序实例
Process p = SingleInstanceGetRunningInstance();
if (p != null) //已经有应用程序副本执行
{
SingleInstanceHandleRunningInstance(p);
}
else //启动第一个应用程序
{
ApplicationRun(new MainForm());
}
简洁的调用为
if (SingleInstanceHandleRunningInstance()== false)
{
ApplicationRun(new MainForm());
}
可见在上面的实现过程中由于关键信息采用应用程序的完整文件名因此在文件名称或路径名称修改后以上实现就会失效
进程互斥
在这个实现方式中需要定义一个进程同步基元可以理解为临界资源该资源只允许一个进程使用根据这一点实现应用程序唯一运行实例就比较简单了
实现步骤如下
.应用程序初始化访问该同步基元
.可以访问说明该同步基元未被使用也就是说没有应用程序实例运行使用同步基元可以继续初始化成为第一个运行实例
.不可以访问说明该同步基元已被使用也就是说已有应用程序实例运行停止当前程序初始化提示已有应用程序运行
.应用程序实例退出释放同步基元占用
在代码中笔者使用SystemThreadingMutex类实现同步基元实现应用程序实例之间互斥功能Mutex默认名字取AssemblyGetEntryAssembly()FullName
在类成员中声明同步基元
private static Mutex mutex = null;
CreateMutex静态方法创建应用程序进程Mutex返回创建结果为true表示创建成功false失败
public static bool CreateMutex()
{
return CreateMutex(AssemblyGetEntryAssembly()FullName);
}
实现其重载方法让用户可以自定义Mutex名字
public static bool CreateMutex(string name)
{
bool result = false;
mutex = new Mutex(true name out result);
return result;
}
对应的释放Mutex资源方法为
public static void ReleaseMutex()
{
if (mutex != null)
{
mutexClose();
}
}
在Main函数中调用下面代码实现单一应用程序实例
if (SingleInstanceCreateMutex())
{
ApplicationRun(new MainForm());
SingleInstanceReleaseMutex();
}
else
{
MessageBoxShow(程序已经运行!);
}
可见在上面的实现过程中Mutex名字是同步基元的唯一标识如果刚好有不同的应用程序使用了相同名称的Mutex那不同的应用程序实例也会出现互斥现象
运行标志
使用应用程序运行标志简单来讲就是在程序初始化的时候设置一个标志表示程序已运行在程序运行结束的时候删除该标志
基本步骤如下
.应用程序初始化检查运行标志是否已经设置
.发现已经设置说明已有应用程序实例运行停止当前程序初始化提示已有应用程序运行 .发现没有设置说明没有应用程序实例运行继续当前程序初始化
.退出应用程序时删除该运行标志
对于标志存储载体可以使用注册表数据库或外部文件等这里的代码使用外部文件实现对存放标志的文件目录选择C:\Documents and Settings\All Users\Application Data也可以是C:\Program Files\Common Files
声明类成员标志文件名称变量
private static string runFlagFullname = null;
初始化程序运行标志如果设置成功返回true已经设置返回false设置失败将抛出异常
public static bool InitRunFlag()
{
if (FileExists(RunFlag))
{
return false;
}
using (FileStream fs = new FileStream(RunFlag FileModeCreate))
{
}
return true;
}
释放初始化程序运行标志如果释放失败将抛出异常
public static void DisposeRunFlag()
{
if (FileExists(RunFlag))
{
FileDelete(RunFlag);
}
}
获取或设置程序运行标志必须符合Windows文件命名规范
public static string RunFlag
{
get
{
if(runFlagFullname == null)
{
string assemblyFullName = AssemblyGetEntryAssembly()FullName;
string path = EnvironmentGetFolderPath(EnvironmentSpecialFolderCommonApplicationData);
runFlagFullname = PathCombine(path assemblyFullName);
}
return runFlagFullname;
}
set
{
runFlagFullname = value;
}
}
在Main函数中调用下面代码实现单一应用程序实例
if (SingleInstanceInitRunFlag())
{
ApplicationRun(new MainForm());
SingleInstanceDisposeRunFlag();
}
else
{
MessageBoxShow(程序已经运行!);
}
可见在上面的实现过程中需要访问文件IO因此有可能会出现异常对异常需要进行具体处理如果不同应用程序使用了相同的运行标志也会出现进程互斥实现中存在的问题由于运行标志存在外部载体中如果笔者把启动的应用程序进程实例直接在Windows管理器进程列表中结束或使其产生异常那设置的运行标志就不会销毁应用程序就没法再次运行
功能测试
这一节对上面的三个功能进行测试以分析之间的区别功能测试类别包括下面五类
.本地系统同一应用程序目录
.本地系统同一应用程序修改运行文件名称使两次运行名称不同
.本地系统两次运行程序目录不同不修改文件名称
.本地系统不同会话用户登录启动应用程序
.远程计算机程序访问启动应用程序(一个程序在远程另一个在本地)
根据代码实现细节不同对测试的结果可能会有所不同这里的测试结果以笔者上面几节中实现的代码为准为了测试简单化通过给应用程序传入测试参数决定使用哪种方式入口函数调用代码为
[STAThread]
static void Main(string[] args)
{
if (argsLength == ) //没有传送参数
{
Process p = SingleInstanceGetRunningInstance();
if (p != null) //已经有应用程序副本执行
SingleInstanceHandleRunningInstance(p);
else //启动第一个应用程序
ApplicationRun(new MainForm());
}
else //有多个参数
{
switch (args[]ToLower())
{
case api:
if (SingleInstanceHandleRunningInstance() == false)
ApplicationRun(new MainForm());
break;
case mutex:
if (argsLength >= ) //参数中传入互斥体名称
{
if ( SingleInstanceCreateMutex(args[]) )
{
ApplicationRun(new MainForm());
SingleInstanceReleaseMutex();
}
else
//调用SingleInstanceHandleRunningInstance()方法显示到前台
MessageBoxShow(程序已经运行!);
}
else
{
if (SingleInstanceCreateMutex())
{
ApplicationRun(new MainForm());
SingleInstanceReleaseMutex();
}
else
//调用SingleInstanceHandleRunningInstance()方法显示到前台
MessageBoxShow(程序已经运行!);
}
break;
case flag://使用该方式需要在程序退出时调用
if (argsLength >= ) //参数中传入运行标志文件名称
SingleInstanceRunFlag = args[];
try
{
if (SingleInstanceInitRunFlag())
{
ApplicationRun(new MainForm());
SingleInstanceDisposeRunFlag();
}
else
//调用SingleInstanceHandleRunningInstance()方法显示到前台
MessageBoxShow(程序已经运行!);
}
catch (Exception ex)
{
MessageBoxShow(exToString());
}
break;
default:
MessageBoxShow(应用程序参数设置失败);
break;
}
}
}
运行CMD命令行
第一种调用为 WindowsApplicationexe –api 或 WindowsApplicationexe
第二种调用为 WindowsApplicationexe –mutex 或WindowsApplicationexe –mutex {FAECfBDE}
第三种调用为 WindowsApplicationexe –flag 或WindowsApplicationexe –flag c:\zhzuo
测试结果
匹配/互斥/标志
同一目录
修改名称
不同目录
不同用户
远程访问
同一目录O/O/O
修改名称X/O/O
不同目录X/O/O
不同用户#/X/O
远程访问X/O/O
备注O 表示成功X – 表示失败# 程序第二个运行没有反应
针对远程访问的测试需要在系统管理工具的NET Framework Configuration中进行设置授权该局域网路径允许访问否则会抛出SystemSecuritySecurityException异常根据测试结果可见三种实现方式适用范围不同理想的实现是结合他们的优点进行多点判断
更多资源
关于NET平台应用的开发更多的技术文章可以访问 对于本文的建议或意见可在网站上留言