Python3 与 C# 并发编程之~ 进程先导篇

Posted 逸鹏说道

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Python3 与 C# 并发编程之~ 进程先导篇相关的知识,希望对你有一定的参考价值。

 

在线预览:http://github.lesschina.com/python/base/concurrency/1.并发编程~进程先导篇.html

Python3 与 C# 并发编程之~ 进程篇:https://www.cnblogs.com/dotnetcrazy/p/9426279.html

Linux专项

先写几个问号来概况下今天准备说的内容:(谜底自己解开,文中都有)

  1. 你知道Ctrl+C终止进程的本质吗?你知道Kill -9 pid的真正含义吗?
  2. 你知道那些跨平台框架(Python,NetCore)在Linux下创建进程干了啥?
  3. 你了解僵尸进程孤儿进程的悲催生产史吗?孤儿找干爹僵尸送往生想知道不?
  4. 想知道创建子进程后怎么李代桃僵吗?ps aux | grep xxx的背后到底隐藏了什么?
  5. 你了解Linux磁盘中p类型的文件到底是个啥吗?
  6. 为什么要realase发布而不用debug直接部署?这背后的性能相差几何?
  7. 还有更多进程间的密密私语等着你来查看哦~

关于帮助文档的说明:

  • 所有用到的系统函数你都可以使用man查看,eg:man 2 pipe
  • Python里面的方法你都可以通过help查看,eg:help(os.pipe)

1.概念引入

正式讲课之前,先说些基本概念,难理解的用程序跑跑然后再理解:如有错误欢迎批评指正

并发 :一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行,但任一个时刻点上只有一个程序在处理机上运行。

并行 :当一个CPU执行一个线程时,另一个CPU可以执行另一个线程,两个线程互不抢占CPU资源,可以同时进行,这种方式我们称之为并行(Parallel)。(在同一个时间段内,两个或多个程序执行,有时间上的重叠)


通俗的举个例子:

小明、小潘、小张、小康去食堂打饭,4个小伙子Coding了3天,饿爆了,现在需要1分钟内让他们都吃上饭,不然就有可怕的事情发生。

按照正常的流程,1分钟可能只够他们一个人打饭,这不行啊,于是有了几种处理方法:

并发:快快快,一人先吃一口,轮着来,一直喂到你们都饱了(只有一个食堂打饭的窗口)(单核CPU)

并行

  • 开了2~3个窗口,和上面处理一样,只是竞争的强度没那么大了
  • 开了4个窗口,不着急,一人一个窗口妥妥的

对于操作系统来说,一个任务就是一个进程(Process),比如打开一个浏览器就是启动一个浏览器进程,打开两个浏览器就启动了两个浏览器进程。

有些进程还不止同时干一件事,比如Word,它可以同时进行打字、拼写检查、打印等事情。在一个进程内部,要同时干多件事,就需要同时运行多个“子任务”,我们把进程内的这些“子任务”称为线程(Thread)

由于每个进程至少要干一件事,所以,一个进程至少有一个线程。像Word这种复杂的进程可以有多个线程,多个线程可以同时执行,多线程的执行方式和多进程是一样的,也是由操作系统在多个线程之间快速切换,让每个线程都短暂地交替运行,看起来就像同时执行一样。

通俗讲:线程是最小的执行单元,而进程由至少一个线程组成。如何调度进程和线程,完全由操作系统决定,程序自己不能决定什么时候执行,执行多长时间


PS:进程5态下次正式讲程序的时候会说,然后就是==> 程序实战不会像今天这样繁琐的,Code很简单,但是不懂这些基本概念往后会吃很多亏,逆天遇到太多坑了,所以避免大家入坑,简单说说这些概念和一些偏底层的东西~看不懂没事,有个印象即可,以后遇到问题至少知道从哪个方向去解决

 

2.进程相关

示例代码:https://github.com/lotapp/BaseCode/tree/master/python/5.concurrent/Linux

2.1.fork引入

示例代码:https://github.com/lotapp/BaseCode/tree/master/python/5.concurrent/Linux/base

(linux/unix)操作系统提供了一个fork()系统调用。普通的函数调用,调用一次,返回一次,但是fork()一次调用,两次返回

因为操作系统自动把父进程复制了一份,分别在父进程和子进程内返回。为了便于区分,操作系统是这样做的:子进程永远返回0,而父进程返回子进程的ID

查看下帮助文档:

import os

help(os.fork)
Help on built-in function fork in module posix:

fork()
    Fork a child process.

    Return 0 to child process and PID of child to parent process.

我们来跑个程序验证一下:(PID返回值如果小于0一般都是出错了)

import os

def main():
    print("准备测试~PID:%d" % os.getpid())
    pid = os.fork()
    if pid == 0:
        print("子进程:PID:%d,PPID:%d" % (os.getpid(), os.getppid()))
    elif pid > 0:
        print("父进程:PID:%d,PPID:%d" % (os.getpid(), os.getppid()))

if __name__ == \'__main__\':
    main()

结果:

准备测试~PID:11247
父进程:PID:11247,PPID:11229
子进程:PID:11248,PPID:11247

可以查看下这个进程是啥: PPID.png

这个指令如果还不熟悉,Linux基础得好好复习下了:https://www.cnblogs.com/dunitian/p/4822807.html,简单分析下吧:a是查看所有(可以联想ls -a),u是显示详细信息,x是把不依赖终端的进程也显示出来(终端可以理解为:人与机器交互的那些)

技巧:指令学习可以递增式学习:psps a ps au ps aux ps ajx

现在验证一下复制一份是什么意思:(代码原封不动,只是在最下面添加了一行输出)

import os

def main():
    print("准备测试~PID:%d" % os.getpid())
    pid = os.fork() # 子进程被父进程fork出来后,在fork处往下执行

    if pid == 0:
        print("子进程:PID:%d,PPID:%d" % (os.getpid(), os.getppid()))
    elif pid > 0:
        print("父进程:PID:%d,PPID:%d" % (os.getpid(), os.getppid()))

    print("PID:%d,我是卖报的小行家,大风大雨都不怕" % os.getpid())

if __name__ == \'__main__\':
    main()

输出:(父子进程的执行顺序是系统调度决定的)

准备测试~PID:13081
父进程:PID:13081,PPID:9388
PID:13081,我是卖报的小行家,大风大雨都不怕
子进程:PID:13083,PPID:13081
PID:13083,我是卖报的小行家,大风大雨都不怕

的确是Copy了一份,Code都一样(玩过逆向的应该知道,这份Code其实就放在了.text(代码段)里面

子进程被父进程fork出来后,在fork处往下执行(Code和父进程一样),这时候他们就为了抢CPU各自为战了

最后验证一下:各个进程地址空间中数据是完全独立的(有血缘关系的则是:读时共享,写时复制,比如父子进程等)

import os

def main():
    num = 100
    pid = os.fork()
    # 子进程
    if pid == 0:
        num += 10
    elif pid > 0:
        num += 20

    print("PID:%d,PPID:%d,Num=%d" % (os.getpid(), os.getppid(), num))

if __name__ == \'__main__\':
    main()

输出:(进程间通信下一节课会系统的讲,今天只谈Linux和概念)

PID:6369,PPID:6332,Num=120
PID:6376,PPID:6369,Num=110

扩展:(简单了解下即可)

  1. 程序:二进制文件(占用磁盘)
  2. 进程:启动的程序(所有数据都在内存中,需要占用CPU、内存等资源)
  3. 进程是CPU、内存、I/0设备的抽象(各个进程地址空间中数据是完全独立的
  4. 0号进程是Linux内核进程(这么理解:初代吸血鬼)
  5. 1号进程是用户进程,所有进程的创建或多或少和它有关系(init or systemd
  6. 2号进程和1号进程一样,都是0号进程创建的,所有线程调度都和他有关系

先看看Linux启动的图示:(图片来自网络) 2.Linux_Start

查看一下init进程 Ubuntu

CentOS进行了优化管理~systemd CentOS

其实程序开机启动方式也可以知道区别了:systemctl start mongodb.service and sudo /etc/init.d/ssh start

Win系列的0号进程: win


第5点的说明:(以远程CentOS服务器为例) pstree -ps

systemd(1)─┬─NetworkManager(646)─┬─{NetworkManager}(682)
           │                     └─{NetworkManager}(684)
           ├─agetty(1470)
           ├─auditd(600)───{auditd}(601)
           ├─crond(637)
           ├─dbus-daemon(629)───{dbus-daemon}(634)
           ├─firewalld(645)───{firewalld}(774)
           ├─lvmetad(483)
           ├─master(1059)─┬─pickup(52930)
           │              └─qmgr(1061)
           ├─polkitd(627)─┬─{polkitd}(636)
           │              ├─{polkitd}(639)
           │              ├─{polkitd}(640)
           │              ├─{polkitd}(642)
           │              └─{polkitd}(643)
           ├─rsyslogd(953)─┬─{rsyslogd}(960)
           │               └─{rsyslogd}(961)
           ├─sshd(950)───sshd(50312)───sshd(50325)───bash(50326)───pstree(54258)
           ├─systemd-journal(462)
           ├─systemd-logind(626)
           ├─systemd-udevd(492)
           └─tuned(951)─┬─{tuned}(1005)
                        ├─{tuned}(1006)
                        ├─{tuned}(1007)
                        └─{tuned}(1048)

再看一个例子:

[dnt@localhost ~]$ pstree dnt -ps
sshd(50325)───bash(50326)───pstree(54471)
[dnt@localhost ~]$ pstree 50325 -ps
systemd(1)───sshd(950)───sshd(50312)───sshd(50325)───bash(50326)───pstree(54489)

其实你可以在虚拟机试试干死1号进程,就到了登录页面了【现在大部分系统都不让你这么干了】 kill -9 1

-bash: kill: (1) - 不允许的操作
 

2.2.僵尸进程和孤儿进程

示例代码:https://github.com/lotapp/BaseCode/tree/master/python/5.concurrent/Linux/base

先看看定义:

孤儿进程 :一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。

僵尸进程 :一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵死进程。

通俗讲就是:

孤儿进程:你爸在你之前死了,你成了孤儿,然后你被进程1收养,你死后的事宜你干爹帮你解决

僵尸进程:你挂了,你爸忙着干其他事情没有帮你安葬,你变成了孤魂野鬼,你的怨念一直长存世间

举个例子看看:

import os
import time

def main():
    pid = os.fork()
    if pid == 0:
        print("子进程:Pid=%d,PPID=%d" % (os.getpid(), os.getppid()))
        time.sleep(1)  # 睡1s
    elif pid > 0:
        print("父进程:Pid=%d,PPID=%d" % (os.getpid(), os.getppid()))

    print("pid=%d,over" % os.getpid())

if __name__ == \'__main__\':
    main()

输出: 孤儿

import os
import time

def main():
    pid = os.fork()
    if pid == 0:
        print("子进程:Pid=%d,PPID=%d" % (os.getpid(), os.getppid()))
    elif pid > 0:
        print("父进程:Pid=%d,PPID=%d" % (os.getpid(), os.getppid()))
        while True:
            print("父亲我忙着呢,没时间管你个小屁孩")
            time.sleep(1)

    print("pid=%d,over" % os.getpid())

if __name__ == \'__main__\':
    main()

输出+测试: 僵尸

其实僵尸进程的危害真的很大,这也就是为什么有些人为了追求效率过度调用底层,不考虑自己实际情况最后发现还不如用自托管的效率高

僵尸进程是杀不死的,必须杀死父类才能彻底解决它们,下面说说怎么让父进程为子进程‘收尸’


2.3.父进程回收子进程(wait and waitpid)

讲解之前先简单分析一下上面的Linux指令(防止有人不太清楚)

kill -9 pid ==> 以前逆天说过,是无条件杀死进程,其实这种说法不准确,应该是发信号给某个进程

-9指的就是信号道里面的SIGKILL(信号终止),你写成kill -SIGKILL pid也一样

-9只是系统给的一种简化写法(好像记得1~31信号,各个Linux中都差不多,其他的有点不一样)

dnt@MZY-PC:~/桌面/work/PC/python/Thread/Linux kill -l
 1) SIGHUP   2) SIGINT   3) SIGQUIT  4) SIGILL   5) SIGTRAP
 6) SIGABRT  7) SIGBUS   8) SIGFPE   9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT   17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG  24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM   27) SIGPROF 28) SIGWINCH    29) SIGIO   30) SIGPWR
31) SIGSYS  34) SIGRTMIN    35) SIGRTMIN+1  36) SIGRTMIN+2  37) SIGRTMIN+3
38) SIGRTMIN+4  39) SIGRTMIN+5  40) SIGRTMIN+6  41) SIGRTMIN+7  42) SIGRTMIN+8
43) SIGRTMIN+9  44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9  56) SIGRTMAX-8  57) SIGRTMAX-7
58) SIGRTMAX-6  59) SIGRTMAX-5  60) SIGRTMAX-4  61) SIGRTMAX-3  62) SIGRTMAX-2
63) SIGRTMAX-1  64) SIGRTMAX

一般搜索进程中的某个程序一般都是用这个:ps -aux | grep xxx|其实就是管道,用于有血缘关系进程间通信,等会讲)

如果安装了pstree就更方便了:pstree 13570 -ps (Ubuntu自带,CentOS要装下yum install psmisc

systemd(1)───systemd(1160)───gnome-terminal-(21604)───bash(8169)───python3(13570)───python3(13571)

扩展:我们平时Ctrl+C其实就是给 2)SIGINT发一个信号


2.3.1.wait

代码实例:https://github.com/lotapp/BaseCode/tree/master/python/5.concurrent/Linux/wait

步入正题:

Python的Wait和C系列的稍有不同,这边重点说说Python:

help(os.wait)

Help on built-in function wait in module posix:

wait()
    Wait for completion of a child process.

    Returns a tuple of information about the child process:
        (pid, status)

os.wait()返回一个元组,第一个是进程id,第二个是状态,正常退出是0,被九号信号干死就返回9

来个案例:

import os
import time

def main():
    pid = os.fork()
    if pid == 0:
        print("子进程:Pid=%d,PPID=%d" % (os.getpid(), os.getppid()))
    elif pid > 0:
        print("父进程:Pid=%d,PPID=%d" % (os.getpid(), os.getppid()))
        wpid, status = os.wait()
        print(wpid)
        print(status)

    print("pid=%d,over" % os.getpid())

if __name__ == \'__main__\':
    main()

输出:

父进程:Pid=22322,PPID=10139
子进程:Pid=22323,PPID=22322
pid=22323,over
22323
0
pid=22322,over

演示一下被9号信号干死的情况:

import os
import time

def main():
    pid = os.fork()
    if pid == 0:
        print("子进程:Pid=%d,PPID=%d" % (os.getpid(), os.getppid()))
        while True:
            print("孩子老卵,就是不听话")
            time.sleep(1)
    elif pid > 0:
        print("父进程:Pid=%d,PPID=%d" % (os.getpid(), os.getppid()))
        wpid, status = os.wait()  # 调用一次只能回收一次,想都回收,就来个while循环,-1则退出
        print(wpid)
        print(status)
        if status == 0:
            print("正常退出")
        elif status == 9:
            print("被信号9干死了")

    print("pid=%d,over" % os.getpid())

if __name__ == \'__main__\':
    main()

输出: 回收子进程


扩展:(回收所有子进程,status返回-1代表没有子进程了,Python里面没有子进程会触发异常)

import os
import time

def main():
    i = 0
    while i < 3:
        pid = os.fork()
        # 防止产生孙子进程(可以自己思索下)
        if pid == 0:
            break
        i += 1

    if i == 0:
        print("i=%d,子进程:Pid=%d,PPID=%d" % (i, os.getpid(), os.getppid()))
        time.sleep(1)
    elif i == 1:
        print("i=%d,子进程:Pid=%d,PPID=%d" % (i, os.getpid(), os.getppid()))
        time.sleep(1)
    elif i == 2:
        print("i=%d,子进程:Pid=%d,PPID=%d" % (i, os.getpid(), os.getppid()))
        time.sleep(3)
        while True:
            print("(PID=%d)我又老卵了,怎么滴~" % os.getpid())
            time.sleep(3)
    elif i==3: # 循环结束后,父进程才会退出,这时候i=3
        print("i=%d,父进程:Pid=%d,PPID=%d" % (i, os.getpid(), os.getppid()))
        while True:
            print("等待回收子进程")
            try:
                wpid, status = os.wait()
                print(wpid)
                print(status)
                if status == 0:
                    print("正常退出")
                elif status == 9:
                    print("被信号9干死了")
            except OSError as ex:
                print(ex)
                break

    print("pid=%d,over,ppid=%d" % (os.getpid(), os.getppid()))

if __name__ == \'__main__\':
    main()

演示:看最后一句输出,父进程扫尾工作做完就over了 回收所有子进程


2.3.2.waitpid

代码实例:https://github.com/lotapp/BaseCode/tree/master/python/5.concurrent/Linux/waitpid

上面说的wait方法是阻塞进程的一种方式,waitpid可以设置不阻塞进程

help(os.waitpid)

Help on built-in function waitpid in module posix:

waitpid(pid, options, /)
    Wait for completion of a given child process.

    Returns a tuple of information regarding the child process:
        (pid, status)

    The options argument is ignored on Windows.

等待进程id为pid的进程结束,返回一个tuple,包括进程的进程ID和退出信息(和os.wait()一样),参数options会影响该函数的行为。在默认情况下,options的值为0。

  1. 如果pid是一个正数,waitpid()请求获取一个pid指定的进程的退出信息
  2. 如果pid为0,则等待并获取当前进程组中的任何子进程的值
  3. 如果pid为-1,则等待当前进程的任何子进程
  4. 如果pid小于-1,则获取进程组id为pid的绝对值的任何一个进程
  5. 当系统调用返回-1时,抛出一个OSError异常。

官方原话是这样的:(英语好的可以看看我有没有翻译错)

If pid is greater than 0, waitpid() requests status information for that specific process.
If pid is 0, the request is for the status of any child in the process group of the current process. 
If pid is -1, the request pertains to any child of the current process. 
If pid is less than -1, status is requested for any process in the process group -pid (the absolute value of pid).

options:(宏)

os.WNOHANG - 如果没有子进程退出,则不阻塞waitpid()调用
os.WCONTINUED - 如果子进程从stop状态变为继续执行,则返回进程自前一次报告以来的信息。
os.WUNTRACED - 如果子进程被停止过而且其状态信息还没有报告过,则报告子进程的信息。

补充:

  1. 进程组:每一个进程都属于一个“进程组”,当一个进程被创建的时候,它默认是其父进程所在组的成员(你们一家
  2. 会 话:几个进程组又构成一个会话(你们小区

用法和wait差不多,就是多了一个不阻塞线程的方法:

import os
import time

def main():
    pid = os.fork()

    if pid == 0:
        print("[子进程]PID:%d,PPID:%d" % (os.getpid(), os.getppid()))
        time.sleep(2)

    elif pid > 0:
        print("[父进程]PID:%d,PPID:%d" % (os.getpid(), os.getppid()))
        while True:
            try:
                wpid, status = os.waitpid(-1, os.WNOHANG)
                if wpid > 0:
                    print("回收子进程wpid:%d,状态status:%d" % (wpid, status))
            except OSError:
                print("没有子进程了")
                break

            print("父进程忙着挣钱养家呢~")
            time.sleep(3)

    print("[over]PID:%d,PPID:%d" % (os.getpid(), os.getppid()))

if __name__ == \'__main__\':
    main()

输出:

[父进程]PID:1371,PPID:29604
[子进程]PID:1372,PPID:1371
父进程忙着挣钱养家呢~
[over]PID:1372,PPID:1371
回收子进程wpid:1372,状态status:0
父进程忙着挣钱养家呢~
没有子进程了
[over]PID:1371,PPID:29604

2.3.3.wait3 and wait4

代码实例:https://github.com/lotapp/BaseCode/blob/master/python/5.concurrent/Linux/wait3.py

help(os.wait3)

Help on built-in function wait3 in module posix:

wait3(options)
    Wait for completion of a child process.

    Returns a tuple of information about the child process:
      (pid, status, rusage)
help(os.wait4)

Help on built-in function wait4 in module posix:

wait4(pid, options)
    Wait for completion of a specific child process.

    Returns a tuple of information about the child process:
      (pid, status, rusage)

这个是Python扩展的方法,用法和wait、waitpid差不多,我就不一个个的举例子了,以wait3为例

import os
import time

def main():
    pid = os.fork()

    if pid == 0:
        print("[子进程]PID:%d,PPID:%d" % (os.getpid(), os.getppid()))
        time.sleep(2)

    elif pid > 0:
        print("[父进程]PID:%d,PPID:%d" %<

以上是关于Python3 与 C# 并发编程之~ 进程先导篇的主要内容,如果未能解决你的问题,请参考以下文章

Python3 与 C# 并发编程之~ 上篇

Python3 加C# 并发编程!强强组合!会产生什么样的化学反应?

Python3 异步编程之进程与线程-1

Python3 并发编程之多进程

并发编程之多线程基础篇及面试

测开之并发编程篇・《并发并行线程队列进程和协程》