1. C# 进程间同步机制(Mutex和EventWaitHandle)实现单一应用启动

一. 技能目标

在开发应用程序的过程中,我们会遇到这样的情况,当我们启动一个应用的时候,如果这个应用已经启动了,我们就展示已经启动的应用就可以,如果没有启动,就正常启动这个应用.怎么实现这个功能呢? 答案是使用进程间通信,使用语言来表述就是在启动新进程的时候去检测某个进程间之间共享的一个变量,然后如果发现这个变量已经存在,就通知那个已经启动了的进程显示主窗口,然后关闭当前正在启动的新进程(新应用).

二. 技能知识点介绍

① 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();
            });
        }
    }
}

结论:

自此这个功能算是完成了,这个功能我们主要收获到的点是什么?

  1. 进程线程间通信(EventWaitHandle)
  2. 进程线程间同步(Mutex)
  3. UI线程上不能直接创建循环(while),如果有阻塞事件不局限于循环,需要创建一个新线程来进行操作.
  4. 非UI线程上不能操作UI,比如UI窗口更新,最大化和最小化,关闭窗口等,要使用Dispatcher.Invoke派发UI线程去做这件事
  • 21
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在C语言中,进程同步和异步通信可以通过多种方式来实现。下面将简要介绍几种常见的方法。 1. 进程同步通信: 进程同步通信是指多个进程按照一定的顺序进行数据交换和操作的过程。常见的实现方式有互斥锁、条件变量和信号量。 互斥锁(mutex):互斥锁用于保护共享资源,同一时只允许一个进程访问该资源。当一个进程需要访问共享资源时,它会尝试获取互斥锁,如果锁已被其他进程持有,则该进程会被阻塞,直到锁被释放为止。 条件变量(condition variable):条件变量用于实现进程的等待/通知机制。当一个进程需要等待某个条件满足时,它会调用条件变量的等待操作,并释放相应的互斥锁。其他进程可以在满足条件时通过通知操作唤醒等待的进程。 信号量(semaphore):信号量是一种计数器,用于控制对共享资源的访问。进程在访问共享资源之前必须先获取信号量,如果信号量的值大于0,则进程可以继续执行;否则,进程会被阻塞,直到信号量的值大于0。 2. 进程异步通信: 进程异步通信是指多个进程可以并发地进行数据传输和操作,不需要按照特定的顺序进行。常见的实现方式有管道、消息队列和共享内存。 管道(pipe):管道是一种半双工的通信机制,可以用于在父子进程或者兄弟进程传输数据。一个进程可以将数据写入管道的写端,另一个进程可以从管道的读端读取数据。 消息队列(message queue):消息队列提供了一种异步的、基于消息的进程通信方式。一个进程可以将消息发送到消息队列,另一个进程可以从消息队列中接收消息。 共享内存(shared memory):共享内存允许多个进程共享同一块内存区域,进程可以直接读写该内存区域来进行通信。共享内存提供了高效的数据传输方式,但需要进程进行同步操作以避免竞争条件。 总结: 在C语言中,进程同步和异步通信可以通过互斥锁、条件变量、信号量、管道、消息队列和共享内存等方式来实现开发者可以根据实际需求选择合适的通信方式,并结合同步机制来保证进程的正确交互和数据一致性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值