背景

目前我主要负责的一个项目是一个 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. 企查查app 初步探索

    企查查app sign算法破解初步探索 之前有说过企查查的sign的解密,但这次是企查查app的sign算法破解,目前是初步进程. 已删除!!!! 上边一些变量已经找到了,其中就有时间戳,其余两个需要 ...

  2. React引入AntD按需加载报错

    背景:React使用create-react-app脚手架创建,然后yarn run eject暴露了配置之后修改less配置, 需求:实现antd组件按需加载与修改主题. 一开始是按照webpack ...

  3. Python之基本数据类型概览

    Python之基本数据类型概览 什么是数据类型? 每一门编程语言都有自己的数据类型,例如最常见的数字1,2,3.....,字符串'小明','age','&D8'...,这些都是数据类型中的某一 ...

  4. Redis学习总结(六)--Redis集群伸缩

    我们在上一章讲了如何创建集群,今天我们来实现下集群的伸缩. 添加节点 操作流程 1.启动节点 2.将节点加入到集群中 3.将数据槽从原来的节点迁移部分到新节点上 实践 1)准备两个新节点并启动 [ro ...

  5. Spring学习之旅(八)--SpringMVC请求参数

    现在我们已经完成了一个无参的接口了,但是应用中有很多需要携带参数的场景,我们来看看 ** SpringMVC** 对它的支持. 参数绑定 SpringMVC 提供了一种绑定机制,通过这个机制可以从请求 ...

  6. Spark应用监控解决方案--使用Prometheus和Grafana监控Spark应用

    Spark任务启动后,我们通常都是通过跳板机去Spark UI界面查看对应任务的信息,一旦任务多了之后,这将会是让人头疼的问题.如果能将所有任务信息集中起来监控,那将会是很完美的事情. 通过Spark ...

  7. 详解javascript中的this的指向问题

    首先,要明白this 既不指向函数自身,也不指函数的词法作用域.this一般存在于函数中,表示当前函数的执行上下文,如果函数没有执行,那么this没有内容,只有函数在执行后this才有绑定. 然后,我 ...

  8. MSIL实用指南-生成内部类

    生成内部类用TypeBuilder的DefineNestedType方法,得到另一个TypeBuilder.内部类的可访问性都是TypeAttributes的“Nested”开头一些成员.实例代码:y ...

  9. 【Linux命令】lsmod命令

    lsmod(list modules)命令 lsmod命令用来显示已被内核加载的模块的状态 1)语法:lsmod 2)功能: lsmod命令可以美观地显示/prco/module中的内容,这些内容是被 ...

  10. codeforces 1041 E. Tree Reconstruction 和度数有关的构造树

    CF 1041E:http://codeforces.com/contest/1041/problem/E 题意: 告诉你一个树的节点个数,显然有n-1条边.已知去掉一条边后,两个集合中最大的节点值. ...