背景

目前我主要负责的一个项目是一个 C/S 架构的客户端开发,前端主要是通过 WPF 相关技术来实现,后端是通过 Python 来实现,前后端的数据通信则是通过 MQ 的方式来进行处理。由于 Python 进程是需要依赖客户端进程来运行,为了保证后端业务进程的稳定性,就需要通过一个 守护进程 来守护 Python 进程,防止其由于未知原因而出现进程退出的情况。这里简单记录一下我的一种实现方式。

实现

对于我们的系统而言,我们的 Python 进程只允许存在一个,因此,对应的服务类型要采用单例模式,这一部分代码相对简单,就直接贴出来了,示例代码如下所示:

public partial class PythonService
{
private static readonly object _locker = new object(); private static PythonService _instance;
public static PythonService Current
{
get
{
if (_instance == null)
{
lock (_locker)
{
if (_instance == null)
{
_instance = new PythonService();
}
}
}
return _instance;
}
} private PythonService()
{ }
}

创建独立进程

由于后端的 Python 代码运行需要安装一些第三方的扩展库,所以为了方便,我们采用的方式是总结将 python 安装文件及扩展包和他们的代码一并打包到我们的项目目录中,然后创建一个 Python 进程,在该进程中通过设置环境变量的方式来为 Python 进程进行一些环境配置。示例代码如下所示:

public partial class PythonService
{
private string _workPath => Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "scripts");
private string _pythonPath => Path.Combine(_workPath, "python27"); private bool isRunning = false;
private int taskPID = -1; public void Start()
{
taskPID = CreateProcess();
isRunning = taskPID != -1; var msg = isRunning ? "服务启动成功..." : "服务启动失败...";
Trace.WriteLine(msg);
} public void Stop()
{
KillProcessAndChildren(taskPID); isRunning = false;
taskPID = -1;
} private int CreateProcess()
{
KillProcessAndChildren(taskPID); int pid = -1;
var psi = new ProcessStartInfo(Path.Combine(_pythonPath, "python.exe"))
{
UseShellExecute = false,
WorkingDirectory = _workPath,
ErrorDialog = false
}; psi.CreateNoWindow = true; var path = psi.EnvironmentVariables["PATH"];
if (path != null)
{
var array = path.Split(new[] { ';' }).Where(p => !p.ToLower().Contains("python")).ToList();
array.AddRange(new[] { _pythonPath, Path.Combine(_pythonPath, "Scripts"), _workPath });
psi.EnvironmentVariables["PATH"] = string.Join(";", array);
}
var ps = new Process { StartInfo = psi };
if (ps.Start())
{
pid = ps.Id;
}
return pid;
} private static void KillProcessAndChildren(int pid)
{
// Cannot close 'system idle process'.
if (pid <= 0)
{
return;
} ManagementObjectSearcher searcher = new ManagementObjectSearcher("Select * From Win32_Process Where ParentProcessID=" + pid);
ManagementObjectCollection moc = searcher.Get();
foreach (ManagementObject mo in moc)
{
KillProcessAndChildren(Convert.ToInt32(mo["ProcessID"]));
}
try
{
Process proc = Process.GetProcessById(pid);
proc.Kill();
}
catch (ArgumentException)
{
// Process already exited.
}
catch (Win32Exception)
{
// Access denied
}
}
}

这里有一点需要注意一下,建议使用 PID 来标识我们的 Python 进程,因为如果你使用进程实例或其它方式来对当前运行的进程设置一个引用,当该进程出现一些未知退出,这个时候你通过哪个引用来进行相关操作是会出问题的。

创建守护进程

上面我们的通过记录当前正在运行的进程的 PID 来标识我们的进程,那对应守护进程,我们就可以通过进程列表查询的方式来进行创建,在轮询的过程中,如果未找到对应 PID 的进程则表明该进程已经退出,需要重新创建该进程,否则就不执行任何操作,示例代码如下所示:

public partial class PythonService
{
private CancellationTokenSource cts; private void StartWatch(CancellationToken token)
{
Task.Factory.StartNew(() =>
{
while (!token.IsCancellationRequested)
{
var has = Process.GetProcesses().Any(p => p.Id == taskPID);
Trace.WriteLine($"MQ状态:{DateTime.Now}-{has}");
if (!has)
{
taskPID = CreateProcess(_reqhost, _subhost, _debug);
isRunning = taskPID > 0; var msg = isRunning ? "MQ重启成功" : "MQ重启失败,等待下次重启";
Trace.WriteLine($"MQ状态:{DateTime.Now}-{msg}");
} Thread.Sleep(2000);
}
}, token);
}
}

这里我使用的是 Thread.Sleep(2000) 方式来继续线程等待,你也可以使用 await Task.Delay(2000,token),但是使用这种方式在发送取消请求时会产生一个 TaskCanceledException 的异常。所以为了不产生不必要的异常信息,我采用第一种解决方案。

接着,完善我们的 StartStop 方法,示例代码如下所示:

public void Start()
{
taskPID = CreateProcess();
isRunning = taskPID != -1; if (isRunning)
{
cts = new CancellationTokenSource();
StartWatch(cts.Token);
} var msg = isRunning ? "服务启动成功..." : "服务启动失败...";
Trace.WriteLine(msg);
} public void Stop()
{
cts?.Cancel(false);
cts?.Dispose(); KillProcessAndChildren(taskPID);
taskPID = -1; isRunning = false;
}

最后,上层调用就相对简单一下,直接调用 Start 方法和 Stop 方法即可。

总结

在我们的实际项目代码中,PythonService 的代码要比上面的代码稍微复杂一些,我们内部还添加了一个 MQ 的 消息队列。所以为了演示方便,我这里只列出了和本文相关的核心代码,在具体的使用过程中,可以依据本文提供的一种实现方法来进行加工处理。

相关参考

补充

这篇文章很荣幸能被 张队 转载到他的公众号上面让更多的技术爱好者看到了。我看到文章的评论区里有朋友说了为什么不用 pythonnet 这种第三方集成框架以及为什么需要守护进程,这里我对这两个问题解答一下

  • 为什么不使用 pythonnet 这种第三方的成熟框架?

这里我需要说明一下,我们的客户端对应的后端服务是 python 写的,并且脚本数量巨多无比,每个脚本之间又是相互独立的模块,相关的依赖库都不一样,所以这就导致一个问题,如果使 pythonnetironpython 这种集成框架,那么每个模块需要使用到的依赖包就需要放到我们客户端来维护管理安装。从工程设计的角度来讲,这个工作由我们客户端组来做是不太合适的,虽然技术上是可行的,但是这无疑是一个坑。

  • 为什么需要一个守护进程?

因为我们的 python 后端服务调用的很多第三方组件(部分是非自研)是多种类型的,后端服务无法保证能稳定调用每一个第三方组件不崩溃,这就要求我们客户端必须要做一个守护进程来监测后端服务进程的状态,当其崩溃后要能重新启动。

我很能理解为什么有很多朋友会有上面两个疑惑,其实做技术的很多都会陷入一个误区:单纯的考虑技术实现,而不关注业务解耦。这个怎么说呢,有好处也有不好的地方,但是我个人觉得,如果只是关注技术,而不切入业务,最后即使每个技术细节实现的很完美,但是业务紧耦合,这个项目依旧难以 可持续发展

用 C# 来守护 Python 进程的更多相关文章

  1. 使用Supervisor守护Python进程

    1.需求 现在有一个进程需要每时每刻不断的跑,但是这个进程又有可能由于各种原因有可能中断.当进程中断的时候我希望能自动重新启动它,此时,就需要使用到了Supervisor.Supervisor起到守护 ...

  2. python进程、线程、协程(转载)

    python 线程与进程简介 进程与线程的历史 我们都知道计算机是由硬件和软件组成的.硬件中的CPU是计算机的核心,它承担计算机的所有任务. 操作系统是运行在硬件之上的软件,是计算机的管理者,它负责资 ...

  3. Python进程、线程、协程详解

    进程与线程的历史 我们都知道计算机是由硬件和软件组成的.硬件中的CPU是计算机的核心,它承担计算机的所有任务. 操作系统是运行在硬件之上的软件,是计算机的管理者,它负责资源的管理和分配.任务的调度. ...

  4. python/进程线程的总结

    python/进程线程的总结 一.进程和线程的描述: 进程:最小的资源管理单位 线程:最小的执行单位 执行一个进程时就默认执行一个线程(主线程) 进程和线程的工作方式: 串行: 假如共有A.B.C任务 ...

  5. python2.0 s12 day8 _ python线程&python进程

    1.进程.与线程区别2.cpu运行原理3.python GIL全局解释器锁4.线程 1.语法 2.join 3.线程锁之Lock\Rlock\信号量 4.将线程变为守护进程 5.Event事件 6.q ...

  6. python 进程和线程(代码知识部分)

    二.代码知识部分 一 multiprocessing模块介绍: python中的多线程无法利用多核优势,如果想要充分地使用多核CPU的资源(os.cpu_count()查看),在python中大部分情 ...

  7. Supervisor4.0和python2.7的crit问题,导致python进程阻塞

    1.问题原因 Supervisor高版本在守护python2.7的服务时,会crit并报错并倒至进程阻塞(python进程存在,但不在运行)的问题,一般会和字符集有关系 <type 'excep ...

  8. [ python ] 进程的操作

    目录 (见右侧目录栏导航)- 1. 前言- 2. multiprocess模块- 2.1 multiprocess.Process模块    - 2.2 使用Process模块创建进程    - 2. ...

  9. python进程概要

    进程 狭义:正在运行的程序实例. 广义:进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动,他是操作系统动态执行的基本单元. python的进程都是并行的. 并行:两个进程同时执行一起走. ...

随机推荐

  1. 使用okHttp登录、Md5密码加密

    1.使用okHttp3登录 2.Md5密码加密 3.完整代码 4.项目案例 使用okHttp3登录: 使用okHttp3之前要在build.gradle引入okHttp3的依赖(顺便引入解析数据的gs ...

  2. Centos安装和配置Mysql5.7

    [root@localhost ~]# wget https://dev.mysql.com/get/mysql57-community-release-el7-11.noarch.rpm -bash ...

  3. Python之流程控制——if...else...

    Python之流程控制--if...else... 一.流程控制 假如把程序比做走路,那我们到现在为止,一直走的都是直路,还没遇到过分岔口.当遇到分岔口时,你得判断哪条岔路是你要走的路,如果我们想让程 ...

  4. Mac系统中idea配置git总结

    系统配置git相关文章,网上有很多,这里只总结我所遇到的问题 配置git相关信息命令: vim ~/.gitconfig 然后就可以配置相关信息,git配置文件详解请参考以下网址: https://b ...

  5. 从零开始搭建Java开发环境第一篇:Java工程师必备软件大合集

    1.JDK https://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html 目前主流的JDK版 ...

  6. String类的intern()方法,随常量池发生的变化

    JVM的知识这里总结的很详细:https://github.com/doocs/jvm/blob/master/README.md,因此在本博客也不会再对其中的东西重复总结了. intern的作用 简 ...

  7. 老司机也晕车--java字符串String晕车之旅

    首先声明,有晕车经历的司机请自备药物,String也可能让你怀疑人生! 第一道 开胃菜 请听题!第一道题: String hello="hello world!"; String ...

  8. Eclipse Paho MQTT Utility

    下载地址: https://repo.eclipse.org/content/repositories/paho-releases/org/eclipse/paho/org.eclipse.paho. ...

  9. redis之mq实现发布订阅模式

    示例代码-github 概述 Redis不仅可作为缓存服务器,还可用作消息队列,本示例演示如何使用redis实现发布/订阅消息队列. 在Redis中,发布者没有将消息发送给特定订阅者的程序.相反,发布 ...

  10. 简单的 Python 人脸识别实例

    案例一 导入图片 思路: 1.导入库 2.加载图片 3.创建窗口 4.显示图片 5.暂停窗口 6.关闭窗口 # 1.导入库 import cv2 # 2.加载图片 img = cv2.imread(' ...