文章目录
- 一. 技能目标
- 二. 技能知识点介绍
- ① Mutex(互斥量)
- ② EventWaitHandle(事件等待句柄)
- 三. 在WPF应用程序中启动程序的时候检查应用是否已经启动,如果已经启动就将主窗口显示出来
一. 技能目标
在开发应用程序的过程中,我们会遇到这样的情况,当我们启动一个应用的时候,如果这个应用已经启动了,我们就展示已经启动的应用就可以,如果没有启动,就正常启动这个应用.怎么实现这个功能呢? 答案是使用进程间通信,使用语言来表述就是在启动新进程的时候去检测某个进程间之间共享的一个变量,然后如果发现这个变量已经存在,就通知那个已经启动了的进程显示主窗口,然后关闭当前正在启动的新进程(新应用).
二. 技能知识点介绍
① Mutex(互斥量)
什么是互斥量? 互斥量用于确保应用程序只有一个实例在运行,思路就是在应用程序的开始启动部分,去创建一个互斥量,创建互斥量的方式如下,互斥量的创建,需要一个常量字符串
MutexId
,来唯一标识这个信号量
private const string _mutexId = "MutexTest-2024-04-08-Fioman";
private static Mutex? _mutex;
_mutex = new Mutex(true, _mutextId, out bool CreatedNew);
-
参数解释
-
true
调用线程是否拥有新创建的互斥量的所有权,true表示拥有该互斥量的所有权.拥有Mutex
互斥量的所有权是什么意思呢?
拥有Mutex
互斥量的所有权意味着该线程可以决定何时释放互斥量.,而其他的线程必须等待所有权释放后才能获取它.说的直白点就是,设置为true就是立马要获取这个资源,如果这个资源已经被占用了,就获取不到了.如果设置为false
,延迟同步操作,这个时候创建_mutex
的时候是没有要求获取所有权的,就是还不要求去操作这个资源,但是什么时候请求操作这个资源呢,需要手动的调用WaitOne()
来请求所有权.
① true的使用场景
单实例应用程序,确保一个应用程序在运行,这个时候你需要立即检查并阻止多个实例的运行.
namespace TestSimple
{public class MutexSimple{private const string mutexId = "MutexSimpleId";public static void SingleInstanceCheck(){using (Mutex mutex = new Mutex(true, mutexId, out bool IsCreateNew)){if (!IsCreateNew){Console.WriteLine("应用程序已经在运行");Console.ReadLine();Environment.Exit(0);}// 注意这有一个细节就是这里不能放到using外面去,因为如果放到using外面去的话,到这里锁资源已经释放了// 下次再运行程序的时候永远获取到的都是新锁,所以这里要放里面Console.WriteLine("应用程序启动成功!");Console.ReadLine();}}}
}
② false的使用场景
在应用程序的初始化阶段不需要同步,但是在后续的操作中需要控制对个某个共享资源的访问.比如多线程日志写入.
private const string mutexIdForLog = "MutexIdForLog";private static Mutex mutex = new Mutex(false, mutexIdForLog);public static void Log(string message){Console.WriteLine("日志写入前请求日志文件资源");mutex.WaitOne(); // 显示请求所有权,就是当其他的线程或者是进程释放了互斥体之后,才会获取到它的所有权try{// 执行写入操作File.AppendAllText("Log.txt", message + Environment.NewLine);Console.WriteLine("日志写入结束,释放资源");}finally{mutex.ReleaseMutex(); // 释放互斥锁}}
这个例子中,互斥体初始设置不拥有所有权(false),日志操作可能不会立即发生,且在应用程序的不同阶段需要多次访问.通过在Log
中显示调用WaitOne()
来请求所有权,可以灵活地控制何时进行同步写入操作.
true
表示立即同步,false
表示延迟同步,各有各自的使用场景.
② EventWaitHandle(事件等待句柄)
EventWaitHandle
是一个非常灵活的同步机制,可以用于两个线程间通信,也可以用于两个进程间通信.它的工作原理类似于一个交通信号灯,可以阻止(无信号,非终止等待状态)或者允许(有信号状态,终止等待状态)一个或者多个线程执行.
- 线程间通信
namespace TestSimple
{public class EventWaitHandleSimple{// 1. 第一个参数false,表示开始创建的时候是有信号,还是无信号,就是如果设置为true,就是一开始会先发一个信号// 2. AutoReset 的意思就是是否自动重置,这里是自动重置,什么是自动重置,意思就是清空信号,如果一个信号被接收到了之后// 后面有两种处理方式,一个是这个信号继续往后传递,一个是这个信号就中断, AutoReset意思就是自动重置public static EventWaitHandle ewh = new EventWaitHandle(false, EventResetMode.AutoReset);public static void RunTask(){Task.Run(() =>{Console.WriteLine("线程1等待信号...");ewh.WaitOne(); // 等待信号Console.WriteLine("线程1接收到信号...");});Task.Run(() =>{Console.WriteLine("线程2等待信号...");ewh.WaitOne(); // 等待信号Console.WriteLine("线程2接收到信号...");ewh.Set(); // 发送信号});}}
}
- 进程间通信
EventWaitHandle
也可以用于不同的进程之间的通信.通过使用具有全局命名空间的中的eventId
创建EventWaitHandle
,不同的进程可以访问同一个EventWaitHandle
对象.这使得一个进程可以等待另外一个进程发出的信号.从而实现进程间的协调和同步.
public static EventWaitHandle ewhConst = new EventWaitHandle(false, EventResetMode.AutoReset, "GlobalEventIdFioman");public static void SendSignal(){ewhConst.Set();}public static void ReceiveSignal(){while (true){ewhConst.WaitOne(); // 等待信号Console.WriteLine("接收到来自其他进程的信号");}}
注意,这里我们循环等待信号,发现在重新打开这个程序的时候,有可能是进程A收到的信号,也有可能是进程B收到这个信号,具体是哪个进程收到了这个信号是随机的.比如我们第一次启动的为进程1,第二次同时启动相同的应用为进程2,以此类推,后面每次启动一次应用都会发送一次信号,但是具体这个信号是被进程1接收到了还是进程2,进程3接收到了,都是有可能的.
三. 在WPF应用程序中启动程序的时候检查应用是否已经启动,如果已经启动就将主窗口显示出来
思路就是使用互斥体和事件,每次设备启动就创建一个互斥体,根据互斥体Id,如果发现互斥体是创建的新的就正常运行,如果发现互斥体已经存在了,就发送应用重复启动事件信号,通知已经启动的程序在已经启动的情况下又启动了一次,然后已经启动的程序在接收到这个事件信号之后,就调用回调(具体是通过委托来实现的),通过这个委托,如果发现主窗口在最小化的状态就让其正常状态并且激活显示到前台.
① 单一应用管理类
namespace IdealSelf.Common
{public class SingleInstanceManager{private const string _mutexId = "MutexId-Fioman-2024-04-09-IdealSelf";private const string _eventHandleId = "EventId-Fioman-2024-04-09-IdealSelf";private static Mutex? _mutex;private static EventWaitHandle? _eWaitHandle;public static void SingleCheck(){/** 创建互斥体,互斥体构造方法,各个参数的含义:* 1) false/true, 表示立即要想获取当前的资源,如果这个资源已经存在了,就会使用旧的,createNew就返回了false.* 2) 互斥体的Id,用来唯一标识一个互斥体,如果Id相同,那么就是同一个互斥体,创建的时候就不会创建新的,* createNew就返回了false* 3) createNew,是否重新创建了一个互斥体,如果为true,表示重新创建了一个互斥体,如果为false,* 表示这个互斥体已经存在,没有重新创建*/_mutex = new Mutex(false, _mutexId, out bool createdNew);if (!createdNew){// 互斥体已经存在,证明程序已经在运行,这个时候,就去创建EventWaitHanle事件句柄// false,表示创建的这个事件句柄一开始是没有信号的, AutoReset表示会自动清空信号_eWaitHandle = new EventWaitHandle(false, EventResetMode.AutoReset, _eventHandleId);// 发送信号,发送完信号之后,其实这里应该是给之前的进程发送的信号_eWaitHandle.Set();// 关闭当前应用Environment.Exit(0);}}// 监听程序启动事件,在window加载的时候调用public static void AppStartEventListen(Action? showWindow){/** 这里为什么要重新开启一个任务,如果不重新开启会造成什么后果?* 因为这个函数是UI线程进行调用的,所以它会在UI线程上进行执行,在UI线程上进行执行的时候,* while(true)后面的waitOne会阻塞* UI线程,所以这里要创建一个后台线程,目的就是为了避免阻塞UI线程*/Task.Run(() =>{while (true){_eWaitHandle = new EventWaitHandle(false, EventResetMode.AutoReset, _eventHandleId);_eWaitHandle.WaitOne();showWindow?.Invoke();}});}}
}
② 在app.cs中 调用单一应用检测,如果发现应用已经启动就发送应用重复启动事件信号
namespace IdealSelf
{/// <summary>/// Interaction logic for App.xaml/// </summary>public partial class App : Application{protected override void OnStartup(StartupEventArgs e){base.OnStartup(e);SingleInstanceManager.SingleCheck();}}
}
③ 在主窗口的UI后台代码中去调用监听应用启动事件,目的就是一旦接收到了应用重新启动就显示和激活主窗口
namespace IdealSelf.Views
{/// <summary>/// Interaction logic for MainView.xaml/// </summary>public partial class MainView : Window{public MainView(){InitializeComponent();Loaded += MainView_Loaded;}private void MainView_Loaded(object sender, RoutedEventArgs e){SingleInstanceManager.AppStartEventListen(WakeApp);}// 唤醒当前的应用,这里应为要操作UI,所以要确保是在UI线程上进行执行的// 因为AppStartEventListen是重新创建了一个新的线程,所以执行WakeApp的线程并不是UI线程,这里要确保是UI线程执行private void WakeApp(){Dispatcher.Invoke(() =>{if (WindowState == WindowState.Minimized){WindowState = WindowState.Normal;}Activate();});}}
}
结论:
自此这个功能算是完成了,这个功能我们主要收获到的点是什么?
- 进程线程间通信(
EventWaitHandle
)- 进城线程间同步(
Mutex
)UI
线程上不能直接创建循环(while
),如果有阻塞事件不局限于循环,需要创建一个新线程来进行操作.非UI线程
上不能操作UI
,比如UI
窗口更新,最大化和最小化,关闭窗口等,要使用Dispatcher.Invoke
派发UI线程去做这件事