纯 .NET 单例应用程序解决方案
本文演示了如何强制执行单一应用程序实例,并使用纯 .NET 代码执行进程间通信 (IPC)。
引言
应用程序有时需要确保一次只能运行一个实例。这有时会带来另一个问题,如果应用程序接受命令行参数,这些参数应该传递给应用程序的已运行实例。
我的一款应用程序有此需求,我意识到如果该解决方案以 DLL 的形式提供,那么任何将来需要相同功能的应用程序都可以使用它,这将更有用。
本文介绍如何使用纯托管代码,该代码创建了一个用于实现这两个目标的底层架构
- 检测应用程序是否已有实例正在运行。
- 将数据发送到应用程序的已运行实例。
注意:文章中有一些地方假设您对 .NET Remoting、序列化、异常处理、委托、线程同步、Dispose 模式等基本概念有一定的了解。不用说,这些主题不会在此介绍,因为它们不是本文的主题。但是,我会在适当的时候覆盖上述声明。
这是我在 CodeProject 上的第一篇文章,希望大家觉得有用。阅读愉快…… :)
背景
一如既往,在编码之前,我做了一些研究。当然,上述问题还有其他解决方案,但我发现的大多数建议解决方案都实现了我选择避免的不同事物,要么是因为解决方案使用了过时的技术(例如 DDE 等),要么是因为解决方案使用了一些包装类来调用非托管的本机 API 函数,而这些函数必须手动编写(也就是说,它们不是由 .NET Framework 实现的)。我实际上发现了一个,或者可能是两个纯托管解决方案,但它们使用了 TcpChannel
,这在性能速度和资源消耗方面都相当浪费。
我会提及其他可能的解决方案策略,以防您想进一步拓展知识,但只有我选择实现的解决方案才会得到全面解释。
描述问题
每个加载到内存的应用程序都有其边界。这些边界定义了应用程序可以访问的内容规则,称为应用程序域。由于存在这些边界,应用程序不能简单地“交谈”到另一个应用程序。没有直接的方法可以做到这一点。如果一个应用程序不能正常地“交谈”到另一个应用程序,它既不能轻易地检测到前一个实例,也不能向其发送数据(例如命令行参数)。
解决方案
应用程序是否已在运行?
为了检测应用程序的前一个实例是否已被启动,我使用了一个命名互斥体 (Named Mutex) 对象。互斥体是一种提供线程同步机制的对象,对吗?是的,但不仅仅如此。互斥体对象还可以用于跨应用程序边界同步代码。能够识别现有互斥体的就是它的唯一名称。当创建一个具有特定名称的互斥体时,没有其他互斥体可以创建相同的名称,因此称为“命名互斥体”。但是,可以打开一个现有的互斥体,并使用它来同步代码。
主要构想
每个应用程序实例都尝试使用特定名称创建互斥体并拥有它。如果互斥体已成功拥有,则表示这是应用程序的第一个实例。所有后续尝试创建和拥有互斥体的操作都将失败,这意味着这不是应用程序的第一个实例。
核心思想 - 付诸代码
我们需要“某人”以一种简单的方式为我们提供有关应用程序实例的信息,而无需我们每次都编写任何与互斥体相关的代码。因此,让我们创建一个类来实现上述想法(稍后我们将扩展该类的功能)
注意:System.Threading.Mutex
类继承自 System.Threading.WaitHandle
,因此具有 Close()
方法。完成后调用此方法对于释放资源以及释放互斥体非常重要。这就是为什么以下类实现 IDisposable
接口。
public class SingleInstanceTracker : IDisposable
{
private bool disposed;
private Mutex singleInstanceMutex;
private bool isFirstInstance;
public SingleInstanceTracker(string name)
{
if (string.IsNullOrEmpty(name))
throw new ArgumentNullException("name",
"name cannot be null or empty.");
try
{
singleInstanceMutex = new Mutex(true, name, out isFirstInstance);
}
catch (Exception ex)
{
throw new SingleInstancingException("Failed to instantiate a new
SingleInstanceTracker object.
See InnerException for more
details.", ex);
}
}
~SingleInstanceTracker()
{
Dispose(false);
}
protected virtual void Dispose(bool disposing)
{
if (!disposed)
{
if (disposing)
{
if (singleInstanceMutex != null)
{
singleInstanceMutex.Close();
singleInstanceMutex = null;
}
}
disposed = true;
}
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
public bool IsFirstInstance
{
get
{
return isFirstInstance;
}
}
}
让我们概述一下构造函数的代码……首先,我们尝试创建一个 Mutex
对象。Mutex
构造函数的参数如下:
bool initiallyOwned
- 此值决定调用代码是否会在创建互斥体时拥有它。string name
- 这是标识互斥体的唯一名称。out bool createNew
- 当互斥体构造函数返回时,此参数包含一个值,指示互斥体是已创建还是已打开。
现在,我们知道了如何检查是否存在前一个实例。我们只需读取最后一个参数的值……如果其值为 true
,则表示互斥体已创建,这是应用程序的第一个实例。如果其值为 false
,则表示互斥体已存在,因此已被打开(而非创建),这不是应用程序的第一个实例。太棒了!第一个问题解决了!
注意:我曾见过使用 Mutex
进行检测的不同策略,涉及使用 WaitOne()
方法,但我选择使用构造函数,因为它只需要一行检测代码。
实例间传递数据
剩下的就是让我们的类能够与第一个应用程序实例“交谈”并向其发送数据。跨应用程序域的应用程序之间的对话过程称为进程间通信(又称 IPC)。IPC 可以通过多种方式实现,如果您有兴趣,可以在此处开始:Microsoft 的 IPC 页面。
经过在网络上的漫长探索,我决定编写一个包装器类来使用命名管道处理通信。但是,我有一个绝妙的主意!如果我在 Object Browser 的搜索框中输入“IPC”会怎么样?我这样做了……结果是!有一个专门用于 IPC 的命名空间,称为 System.Runtime.Remoting.Channels.Ipc
。整个命名空间在 .NET Framework 2.0 中是新的,它包含三个类:IpcServerChannel
、IpcClientChannel
和 IpcChannel
(它是两者的组合)。这些类是使用命名管道实现的。要使用该命名空间,必须将对 System.Runtime.Remoting.dll 的引用添加到项目中。
注意:System.Runtime.Remoting.Channels.Ipc
仅在同一计算机上运行的进程之间通信时有用。但这对我们目前的情况来说并不是一个缺点。相反,这是一个巨大的优势,因为与使用 TcpChannel
/HttpChannel
不同,IpcChannel
不依赖于网络连接,这意味着开销会少得多,通信速度也会大大提高。这正是我在文章开头说我选择避免 TcpChannel
解决方案的原因。
那么,现在我们知道了要使用的工具,让我们继续解决方案……
主要构想
首先,关于 Remoting 的几点说明……Remoting 允许一个应用程序中的对象被另一个应用程序使用。为了使 Remoting 生效,有一些规则:
- 应远程实例化其对象的类必须继承自
MarshalByRefObject
类,以允许跨应用程序域访问它。 - 每个 Remoting 会话都必须有一个服务器,它将提供对象,以及一个客户端,它将使用这些对象。
- 为了让客户端能够访问远程对象,服务器必须注册它并使其可见。
- 通过 Remoting 传递的数据必须是可序列化的,因为 Remoting 依赖于对象的序列化。
我们需要能够通过任何新实例在第一个应用程序实例上执行方法。这意味着我们需要在服务器(第一个应用程序实例)上实例化某个对象,并在客户端(应用程序的任何新实例)上使用它。这个对象反过来将执行第一个应用程序实例中的方法。它将是我们的代理对象。
现在我们有了驻留在第一个实例中的代理对象,我们需要以某种方式告诉它调用第一个应用程序实例中的方法,无论是主窗体还是其他任何内容,具体取决于应用程序的设计。因此,为了使一个类能够接收来自新实例的消息,它必须实现一个包含处理消息的方法的接口。然后,当实例化代理对象时,它会收到一个实现该接口的对象。
就是这样!现在,每当客户端(新的应用程序实例)需要调用服务器(第一个应用程序实例)上的方法时,它将像使用自己的对象一样使用代理对象并调用方法。
核心思想 - 付诸代码
让我们从接口开始
public interface ISingleInstanceEnforcer
{
void OnMessageReceived(MessageEventArgs e);
void OnNewInstanceCreated(EventArgs e);
}
正如您所见,OnMessageReceived()
方法接收了一个 MessageEventArgs
参数。我不会讨论这个参数,因为您可以在提供的源代码中找到它。但是,我会说 MessageEventArgs
类必须标记为 Serializable
。客户端发送消息,因此消息对象在客户端的实例上创建,并应发送到服务器,这意味着它将被序列化。还记得规则吗?通过 Remoting 传递的数据必须是可序列化的。
接下来是我们的代理
internal class SingleInstanceProxy : MarshalByRefObject
{
#region Member Variables
private ISingleInstanceEnforcer enforcer;
#endregion
#region Construction / Destruction
public SingleInstanceProxy(ISingleInstanceEnforcer enforcer)
{
if (enforcer == null)
throw new ArgumentNullException("enforcer",
"enforcer cannot be null.");
this.enforcer = enforcer;
}
#endregion
#region Overriden MarshalByRefObject Members
public override object InitializeLifetimeService()
{
return null;
}
#region Properties
public ISingleInstanceEnforcer Enforcer
{
get
{
return enforcer;
}
}
#endregion
}
这个类基本上是自解释的,除了两件事:首先,请注意该类继承了 MarshalByRefObject
。再次,如果我们回顾一下规则……这个类应该在服务器上实例化,但从客户端访问,所以它必须继承自 MarshalByRefObject
。其次,该类定义为 internal
修饰符。这不是必需的,但由于这个类除了 SingleInstanceTracker
类之外不会被任何人使用,所以我决定它不应该是外部可见的。
-- 更新 (17/11/07): --
另一个重要的事情是重写的 InitializeLifetimeService()
方法。每个 MarshalByRefObject
对象都有一个生命周期(即,它何时应被 GC 收集的规范)。每次引用对象时,此生命周期都会重置。但是,如果对象在一定时间内未使用,它就会被垃圾回收。
那么,我们为什么要在乎它呢?当第一个应用程序实例首次实例化代理对象时,它是由我们手动实例化的,使用的是唯一的构造函数,它接收一个 ISingleInstanceEnforcer
对象。如果我们一定时间内不使用该对象,GC 就会收集它。下一次我们尝试远程访问代理对象时,框架会尝试使用无参数构造函数(不存在)来实例化一个新的代理对象。解决方案是让对象永远存在。这通过重写 InitializeLifetimeService()
方法并返回 null
来实现。
非常感谢 **Thomas Schoeniger** 和 **byates** 报告了该错误并发布了解决方案。 :)
-- 更新结束 --
我们已经确定,如果这是第一个实例,应用程序应该创建一个服务器,并“发布”一个 SingleInstanceProxy
对象,以便后续的每个实例都创建一个客户端并能够访问该对象。
这是 SingleInstanceTracker
的新构造函数代码,后跟解释
public SingleInstanceTracker(string name)
: this(name, null) { }
public SingleInstanceTracker(string name,
SingleInstanceEnforcerRetriever enforcerRetriever)
{
if (string.IsNullOrEmpty(name))
throw new ArgumentNullException("name",
"name cannot be null or empty.");
try
{
singleInstanceMutex = new Mutex(true, name, out isFirstInstance);
// Do not attempt to construct the IPC channel
// if there is no need for messages
if (enforcerRetriever != null)
{
string proxyObjectName = "SingleInstanceProxy";
string proxyUri = "ipc://" + name + "/" + proxyObjectName;
// If no previous instance was found, create a server channel which
// will provide the proxy to the first created instance
if (isFirstInstance)
{
// Create an IPC server channel to listen for SingleInstanceProxy
// object requests
ipcChannel = new IpcServerChannel(name);
// Register the channel and get it ready for use
ChannelServices.RegisterChannel(ipcChannel, false);
// Register the service which gets the SingleInstanceProxy object,
// so it can be accessible by IPC client channels
RemotingConfiguration.RegisterWellKnownServiceType(
typeof(SingleInstanceProxy),
proxyObjectName,
WellKnownObjectMode.Singleton);
// Attempt to retrieve the enforcer from the delegated method
ISingleInstanceEnforcer enforcer = enforcerRetriever();
// Validate that an enforcer object was returned
if (enforcer == null)
throw new InvalidOperationException("The method delegated by the
enforcerRetriever argument returned null.
The method must return an
ISingleInstanceEnforcer object.");
// Create the first proxy object
proxy = new SingleInstanceProxy(enforcer);
// Publish the first proxy object so IPC clients
// requesting a proxy would receive a reference to it
RemotingServices.Marshal(proxy, proxyObjectName);
}
else
{
// Create an IPC client channel to request
// the existing SingleInstanceProxy object.
ipcChannel = new IpcClientChannel();
// Register the channel and get it ready for use
ChannelServices.RegisterChannel(ipcChannel, false);
// Retreive a reference to the proxy object which
// will be later used to send messages
proxy = (SingleInstanceProxy)
Activator.GetObject(typeof(SingleInstanceProxy),
proxyUri);
// Notify the first instance of the application
// that a new instance was created
proxy.Enforcer.OnNewInstanceCreated(new EventArgs());
}
}
}
catch (Exception ex)
{
throw new SingleInstancingException("Failed to instantiate a
new SingleInstanceTracker object.
See InnerException for more details.", ex);
}
}
为了使通道可用,必须对其进行注册。这通过在 if (isFirstInstance)
条件块的两个分支中调用 ChannelServices.RegisterChannel()
方法来完成,无论是注册服务器通道还是客户端通道。
为了使类可远程使用,也必须对其进行注册。这里,在使用 RemotingConfiguration.RegisterWellKnownServiceType()
方法时,我们有两种选择……指定 WellKnownObjectMode.SingleCall
或 WellKnownObjectMode.Singleton
。后者意味着每个客户端使用已注册类的请求都将导致引用同一个对象。这正是这里所需要的……代理对象一次在服务器上实例化,并在每个客户端上有一个引用。因此,我们有了服务器和客户端注册代码,以及代理的注册代码。我们需要创建一个代理对象,但要做到这一点,我们首先必须检索一个 ISingleInstanceEnforcer
对象。我选择让构造函数接收一个返回此类对象的委托。在构造 SingleInstanceProxy
的第一个(也是唯一一个)实例后,我们必须在通道上发布它,以便请求代理的客户端最终收到它的引用,我们通过调用 RemotingServices.Marshal()
方法来实现这一点。在客户端,要检索对代理对象的引用,我们使用 Activator.GetObject()
方法,并指定对象的 URI。
完成!现在,要将消息发送到第一个实例,只需调用 proxy.Enforcer
的某个方法即可。构造函数通过调用 proxy.Enforcer.OnNewInstanceCreated()
立即通知第一个实例有另一个实例已被创建。
最后,这是发送消息到第一个实例的代码
public void SendMessageToFirstInstance(object message)
{
if (disposed)
throw new ObjectDisposedException("The SingleInstanceTracker object
has already been disposed.");
if (ipcChannel == null)
throw new InvalidOperationException("The object was constructed with
the SingleInstanceTracker(string name)
constructor overload, or with
the SingleInstanceTracker(string name,
SingleInstanceEnforcerRetriever
enforcerRetriever) constructor overload,
with enforcerRetriever set to null,
thus you cannot send messages to
the first instance.");
try
{
proxy.Enforcer.OnMessageReceived(new MessageEventArgs(message));
}
catch (Exception ex)
{
throw new SingleInstancingException("Failed to send message to the first
instance of the application.
The first instance might have
terminated.", ex);
}
}
Using the Code
选择应处理消息的类,并使其实现 ISingleInstanceEnforcer
接口。在演示项目中,我使用主窗体作为执行器,并这样实现它
public partial class MainForm : Form, ISingleInstanceEnforcer
{
private delegate void OnMessageReceivedInvoker(MessageEventArgs e);
public MainForm()
{
InitializeComponent();
}
#region ISingleInstanceEnforcer Members
void ISingleInstanceEnforcer.OnMessageReceived(MessageEventArgs e)
{
OnMessageReceivedInvoker invoker = delegate(MessageEventArgs eventArgs)
{
string msg = eventArgs.Message as string;
if (string.IsNullOrEmpty(msg))
MessageBox.Show("A non-textual message has been received.",
"Message From New Instance",
MessageBoxButtons.OK,
MessageBoxIcon.Information);
else
MessageBox.Show(msg,
"Message From New Instance",
MessageBoxButtons.OK,
MessageBoxIcon.Information);
};
if (InvokeRequired)
Invoke(invoker, e);
else
invoker(e);
}
void ISingleInstanceEnforcer.OnNewInstanceCreated(EventArgs e)
{
MessageBox.Show("New instance of the program has been created.",
"Notification From New Instance",
MessageBoxButtons.OK, MessageBoxIcon.Information);
}
protected virtual void OnMessageReceived(MessageEventArgs e)
{
((ISingleInstanceEnforcer)this).OnMessageReceived(e);
}
protected virtual void OnNewInstanceCreated(EventArgs e)
{
((ISingleInstanceEnforcer)this).OnNewInstanceCreated(e);
}
#endregion
}
我显式实现了该接口,以便能够创建接口中包含的相同方法,但将它们声明为 protected virtual
,以模仿 .NET Framework 中事件处理的设计。完成 ISingleInstanceEnforcer
接口的实现后,就可以使用 SingleInstanceTracker
类了。而还有什么地方比在主窗体实例化之前更好呢?所以,让我们看看修改后的 Main()
方法……
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
SingleInstanceTracker tracker = null;
try
{
// Attempt to create a tracker
tracker = new SingleInstanceTracker("SingleInstanceSample",
new SingleInstanceEnforcerRetriever
(GetSingleInstanceEnforcer));
// If this is the first instance of the application, run the main form
if (tracker.IsFirstInstance)
Application.Run((MainForm)tracker.Enforcer);
else // This is not the first instance of the application, so do
// nothing but send a message to the first instance
tracker.SendMessage("Hello first instance! this is the new
instance!\nI will now terminate if
you don't mind...");
}
catch (SingleInstancingException ex)
{
MessageBox.Show("Could not create a SingleInstanceTracker object:\n" +
ex.Message + "\nApplication will now terminate.");
return;
}
finally
{
if (tracker != null)
tracker.Dispose();
}
}
private static ISingleInstanceEnforcer GetSingleInstanceEnforcer()
{
return new MainForm();
}
正如您所见,现在只需要几行代码就可以确定应用程序是否已在运行,并将一个对象传递给它的第一个实例。首先,创建一个 SingleInstanceTracker
对象,然后对其进行“询问”以确定它是否是第一个实例,并在需要时发送消息。请注意,一旦决定这是应用程序的第一个实例,主窗体将从 Enforcer
属性中提取,而不是被实例化。原因是 SingleInstanceTracker
的构造函数已经调用了 GetSingleInstanceEnforcer()
方法,该方法实例化了窗体。如果创建了一个新窗体而不是从 Enforcer
属性中提取,它将不会收到消息,因为它不是与代理对象关联的那个。
结语
代码不提供对先前实例的实时检查,而是实例化时检查的快照。但是,这不成问题,因为通常只在应用程序启动时进行一次检查。此代码还可以修改为使第一个实例能够向新实例发送消息,使 ISingleInstanceEnforcer.OnNewInstanceCreated()
接收一个定义了 Cancel
属性的 EventArgs
对象,或者其他任何内容,但当前实现已满足我的需求。
欢迎提出任何建议、问题或建设性批评……感谢您的阅读,祝您编码愉快!
历史
- 22/7/07 - 更新了源代码(不小心提供了一个旧的)。
- 17/11/07 - 修复了一个导致代理对象在几分钟后被回收的 bug(源代码也已更新)。