Python并行系统工具_程序退出和进程间通信

与C语言不同,Python中没有"主"函数,运行程序时,仅仅是自上而下,运行顶级文件里的所有代码

正常情况下,执行完最后一行语句时,自动退出

Sys模块退出

使用sysos模块中的工具显示调用程序退出

sys.exit函数在调用时可以让程序提前结束

  • 参数N:状态

  • 其实只是抛出了一个SystemExit异常

    • Exit the interpreter by raising SystemExit(status)

  • 可以捕获异常,拦截程序的过早退出并执行清理活动

  • 如果未被捕获,则解释器正常退出

  • raise语句显示抛出SystemExit异常和调用sys.exit作用一样

SystemExit异常

  • Exception都继承自BaseException

  • 可以被捕获

  • 如果捕获它,解释器在该处正常退出

def later():
    import sys
    print("Bye sys world")
    sys.exit(42)
    print("Never reached")


if __name__ == '__main__':
    later()
    
""" 程序运行结果
Bye sys world
Process finished with exit code 42
"""
sys.exit

OS模块退出

os.exit()

  • 调用进程立即退出,而不是抛出可以捕获或忽略的异常

  • 进程退出时进行输出流缓冲和运行清理处理器(atexit模块定义)

  • 一般应当只在分支出的子进程上进行,最好不要在整个程序中进行

  • Exit to the system with specified status, without normal exit processing.

Shell 命令退出状态代码

sysos退出调用接受进程退出状态代码作为参数

  • sys可选

  • os 必须的

退出后,这个状态代码可以在shell中查询,也可以在以子进程运行该脚本的程序中查询

  • Shell命令行程序中,退出状态可以在执行过程中以跨程序通信的简单形式得以检查

  • 通过分支进程运行程序时,退出状态可以在父进程中通过os.waitos.watipid调用获取

os.systemos.popen运行shell名称,在退出时获取close(),之后需要从位掩码中提取(>>8)

退出状态实际上被包装进返回值的特定比特位置需要将结果右移8比特才能读取

❌但是在windows中,不需要进行右移,可以直接读取

进程的退出状态和共享状态

"""
分支子进程,用os.wait观察其退出状态
能在Unix上运行,但是在Windows上不能运行
派生线程共享全局变量,但分支进程拥有自己的全局变量副本
分支进程复制并因此共享文件描述符
"""

import os

exit_stat = 0


def child():
    global exit_stat
    exit_stat += 1 # 复制全局变量,都是0,改变不影响其他分进程
    print('Hello from Child', os.getpid(), exit_stat)
    os._exit(exit_stat) # 发送到父进程的wait函数的退出状态
    print("Never reached")


def parent():
    while True:
        newpid = os.fork()
        if newpid == 0:
            child()
        else:
            pid, status = os.wait()
            print('Parent got', pid, status, (status >> 8))
            if input() == 'q':
                break


if __name__ == '__main__':
    parent()
Hello from Child 10271 1
Parent got 10271 256 1

Hello from Child 10272 1
Parent got 10272 256 1

Hello from Child 10273 1
Parent got 10273 256 1

Hello from Child 10274 1
Parent got 10274 256 1

Hello from Child 10275 1
Parent got 10275 256 1
退出状态
  • 线程通常在其运行的函数返回时默默退出,且函数的返回值被忽略

  • 但是可以调用_thread.exit()结束其调用线程

  • _thread.exit()sys.exit相同,也会抛出SystemExit异常

    • 所以也可以在线程中使用sys.exit

    • 但是一定不要使用os._exit(),会产生奇怪的结果

  • 线程一般不利用退出状态,而是给模块水平的全局变量复制或原位修改共享的可变对象以发送信号,并在必要时用线程的模块锁和队列来同步化共享对象的访问

 

进程间通信

程序派生线程时,有一个自然的通信机制,即改变和检查共享内容中的名称和对象,包括可访问的变量和属性,以及被应用的可变对象,对于可能发生并发更新的共享对象需要小心的通过锁来同步化对它们的访问

  • 但是 线程还提供了一个相当直接的通信模型queue模块

当程序开始一些子进程,以及一些不共享内存的独立程序时,通信就变得复杂了

 

  • 程序间通信的机制(限制程序间可以通信的类型)

    • 简单的文件

    • 命令行参数

    • 程序退出状态码

    • shell环境变量

    • 标准流重定向

    • os.popensubporcess 管理的管道流

  • Python库提供的进程间通信IPC工具

    • 信号允许程序向其他程序发送简单的通知时间

    • 匿名管道允许共享文件描述符的线程即相关进程传递数据,但是依赖于Unix下的进程分支模型

    • 命名管道映射到系统的文件系统,允许完全不相关的程序进行交流,但是并非所有的Python平台都提供

    • 套接字映射到系统级别的端口号,不仅允许在同一台计算机上的任意两个程序间传递数据,还允许远程联网的机器上的程序之间通信

      虽然可以作为线程间通信的手段,但在不共享内存空间的单独进程的作用下,全部能力更能淋漓精致的表现出来

管道

管道:程序间的通信手段,是由操作系统实现并通过Python标准库提供的

管道是单向的通道, 像一个共享的内存缓冲,但是对于两端来说接口类似一个简单的文件

用法:一个程序在管道的一端写入数据,而另一个程序在管道的另一端读取数据,每个程序仅能看到自己一端的管道,并且使用正常的Python文件调用方法来处理

管道更加偏向于在操作系统内部使用

  • 读取管道的调用一般会阻塞调用程序,直到数据变得可用,而非返回一个文件尾指示器

  • 管道的读取调用总是返回最先写入的数据(队列,先进先出

  • 管道还可以用于同步化管独立程序的执行

分类

  1. 具名管道

    • 通常称为FIFO,计算机上由一个文件代表

    • 具名管道是真正的外部文件,进行通信的进程无需相关,可以是独立的启动程序

  2. 匿名管道

    • 仅在进程内部存在, 通常和进程分支合用,作为一种在应用程序内部连接父进程与子进程的手段

    • 父进程与子进程通过共享的管道文件描述符进行交流,后者为派生的进程所继承

    • 匿名管道对线程也适用

匿名管道

创建管道
  • 返回的是两端的描述符,分别代表着输入端和输出端

    • 但是没有具体的分别

    • 只是一端写入,另一端就只能读取

  • 描述符的读写工具分别是os.reados.write,传递的是字节字符串

  • 读取数据时,不会考虑数据本身是什么,只会按照指定的数量读取数据

    • 为了区分信息,在管道中要求一个分割字符

      • 可以使用os.fdopen将文件描述符封装进一个文件对象

      • 通过文件对象的readline方法在管道中搜索写一个/n分隔符

(read_fd, write_fd) = os.pipe()
## 匿名管道和进程分支
import os, time

def child(pipeout):
    zzz = 0
    while True:
        time.sleep(zzz)
        msg = ("Spam %03d" %zzz).encode()
        os.write(pipeout, msg)
        zzz = (zzz + 1) % 5 # 0,1,2,3,4,0 .。。。 循环

def parant():
    pipein, pipeout = os.pipe()
    if os.fork() == 0 :
        child(pipeout)
    else:
        while True:
            # 数据发送完之前保持阻塞
            line = os.read(pipein, 32)
            print("Parent %d got [%s] at %s" % (os.getpid(), line, time.time()))

parant()
匿名管道和进程分支
import os
import time

start_time = time.time()


def child(pipeout):
    zzz = 0
    while True:
        time.sleep(zzz)
        msg = ("Spam %03d
" % zzz).encode()
        os.write(pipeout, msg)
        zzz = (zzz + 1) % 5  # 0,1,2,3,4,0 .。。。 循环


def parant():
    pipein, pipeout = os.pipe()
    if os.fork() == 0:
        # 关闭输入端
        os.close(pipein)
        child(pipeout)
    else:
        # 监听管道,
        os.close(pipeout)  # 关闭输出端
        # 创建文本模式的文件对象
        pipein = os.fdopen(pipein)
        while True:
            # 数据发送完之前保持阻塞
            # line = os.read(pipein, 32)
            line = pipein.readline()[:-1]
            print("Parent %d got [%s] at %s" % (os.getpid(), line, time.time()))


parant()
文件文本模式
  • 在各进程中关闭管道未被使用的一端

  • 程序应当关闭未被使用的管道末端

## 匿名管道和线程
import os
import time
import threading
start_time = time.time()


def child(pipeout):
    zzz = 0
    while True:
        time.sleep(zzz)
        msg = ("Spam %03d" % zzz).encode()
        os.write(pipeout, msg)
        zzz = (zzz + 1) % 5  # 0,1,2,3,4,0 .。。。 循环


def parent(pipein):
    while True:
        # 数据发送完成之前保持阻塞
        line = os.read(pipein, 32)
        print("Parent %d got [%s] at %s" % (os.getpid(), line, time.time() - start_time))


pi·
parent(pipein)
匿名管道和线程
  • 通讯管道是单向的允许数据仅遵循一个方向流动,一段输入,另一端输出

  • 当需要数据来回流动时,就需要两个管道

    • 用一个管道向程序发送请求

    • 另一个向请求着发送答复

"""
派生一个子进程/程序,连接我的stdin/stdout和子进程的stdin/stdout
读写映射到派生程序的输入和输出上,类似subprocess目标绑定流
"""
import os,sys
def spawn(prop, *args):
    # 获取标准输入输出的文件描述符,一般而言,stdin=0,stdout=1
    stdinFd = sys.stdin.fileno()
    stdoutFd = sys.stdout.fileno()

    # 创建两个匿名管道,返回输入流,输出流文件描述符
    parentStdin, childStdout = os.pipe()
    childStdin, parentStdout = os.pipe()

    pid = os.fork()
    if pid:
        # 分支之后,在父进程中
        # 关闭管道的子进程端
        os.close(parentStdin)
        os.close(parentStdout)
        # 将parentStdin赋值给stdinFd,就是sys.stdin
        # 将stdin/stdout用parent表示了
        os.dup2(parentStdin, stdinFd)
        os.dup2(parentStdout, stdoutFd)
    else:
        os.close(childStdin)
        os.close(childStdout)
        os.dup2(childStdout, stdoutFd)
        os.dup2(childStdin, stdinFd)
        args = (prop, ) + args
        os.execlp(prop, args)
        assert False, "Excevp Failed"


if __name__ == '__main__':
    mypid = os.getpid()
    spawn('python', 'pipes-testchild.py', 'spam')
    
    print("Hello 1 from parent", mypid)
    sys.stdout.flush()
    reply = input()
    sys.stderr.write("pareat got", reply, '
')
    
    
    print("Hello 2 from parent", mypid)
    sys.stdout.flush()
    reply = sys.stdin.readline()
    sys.stderr.write("pareat got", reply, '
')
映射到派生程序的输入和输出上

命名管道

  • 作为文件系统中真实的命名文件而存在的长时运行的管道,这种文件被称为具名管道或者FIFO

  • 由于FIFO和计算机上的真实文件相关联,对于任何程序来说都是外部文件

  • 不依赖于任务间共享的内存,可以作为线程,进程及独立启动的程序间的IPC的机制

  • 具名管道创建之后,客户端通过名称打开并通过常用文件读写操作读写数据

  • FIFO是单向流

    • 典型操作中,服务器从FIFO中读取数据,客户端对其写入数据

    • 可以使用两个FIFO实现双向通信

  • FIFO存在于文件系统中,比进程内部的匿名管道持续时间长,还可以被独立的启动程序访问

  • 和正常文件不同的时,操作系统同步化FIFO的访问,使之适合IPC

  • 更适合作为独立客户端和服务器程序的一般IPC机制

    • FIFO是套接字端口接口的替代机制

    • 但是FIFO不支持远程网络连接

    • 对FIFO的访问是通过标准的文件接口实现的

    • 不支持目前的Window的Python版本

os.mkFIFO创建命名管道,但是Windows不支持

  • 不同仅创建了外部文件

  • 为了传输数据,需要像标准文件一样打开和处理

"""
命名管道:os.mkfifo在windows下不能用
没有分支的必要,英文ififo文件管道对于进程为外部文件
父进程/子进程中的共享文件描述符(标准输出输入)在这里没有效果
"""
import os
import sys
import time

fifoname = 'pipeinfo'


def child():
    # 作为文件描述符打开fifo'
    pipeout = os.open(fifoname, os.O_WRONLY)
    zzz = 0

    while True:
        time.sleep(zzz)
        msg = ('Span %03d
' % zzz).encode()
        os.write(pipeout, msg)
        zzz = (zzz + 1) % 5


def parent():
    # 作为文件对象代开fifo
    pipein = open(fifoname, 'r')
    while True:
        line = pipein.readline()[:-1]
        print("Parent %d got %s at %s" % (os.getpid(), line, time.time()))


if __name__ == '__main__':
    if not os.path.exists(fifoname):
        os.mkfifo(fifoname)
    if len(sys.argv) == 1:
        parent()
    else:
        child()
命名管道

  FIFO独立于子进程和父进程存在,不需要进程分支操作

套接字

由Python的scoket模块实现的,比管道更加泛化的IPC手段

使得我们可以让数据传输在同一台计算机生的不同程序间进行(经由本机层次的全局端口号连接套接字),也可以在互联网的机器上进行(提供机器名及端口号连接套接字)

  • 和FIFO的相同之处:

    • 套接字是机器水平的全局对象

    • 不要求线程或进程间共享内存,因此对独立程序也适用

  • 和FIFO的不同之处

    • 套接字根据端口号进行识别,而非文件系统的路径名称,利用大不相同的非文件API

    • 跨平台移植性好

"""
套接字用于跨任务通信,启动线程,相同通过套接字通信;也适用于独立的程序,因为套接字是系统级别的,类似FIFO
套接字传输字节字符串
"""

from socket import socket, AF_INET, SOCK_STREAM
from time import localtime

# 机器上套接字的端口号和机器名
port = 50008
host = "localhost"


def server():
    sock = socket(AF_INET, SOCK_STREAM)
    sock.bind(("", port))
    # 最多允许5个等待中的客户端
    sock.listen(5)

    while True:
        # 等待客户端连接
        conn, addr = sock.accept()
        # 从客户端连接读取字节数
        data = conn.recv(1024)
        print(f'收到:[{addr}]::: 的信息==> {data.decode()}')
        conn.send(f"server 已收到消息:{data.decode()} :: at [{localtime()}]".encode())


def client(name):
    sock = socket(AF_INET, SOCK_STREAM)
    sock.connect((host, port))
    sock.send(name.encode())
    reply = sock.recv(1024)
    sock.close()
    print(f'client got [{reply.decode()}]')


if __name__ == '__main__':
    from threading import Thread

    sthread = Thread(target=server)
    sthread.daemon = True  # 设置为守护进程,主线程不需要等待结束
    sthread.start()
    for i in range(5):
        Thread(target=client, args=('client%s' % i,)).start()
套接字
  • 虽然套接字对于线程适用,但是线程的共享内存模型通常允许它们词用较简单的通讯手段,如共享的名称,对象,队列

  • 套接字倾向于在单独的进程和独立启动的程序间的IPC中起作用

"""
套接字在独立的程序间通信
服务器种子进程中允许,为线程和进程中的客户端提供服务器
套接字是机器水平的全局对象,类似fifo,无需内存共享
"""
import os
import sys
from threading import Thread

from socket_preivew import server, client

mode = int(sys.argv[1])
print(mode)
if mode == 1:
    server()
elif mode == 2:
    client('client: process=%s' % os.getpid())
else:
    for i in range(5):
        Thread(target=client, args=('client: process=%s' % i,)).start()
套接字在独立的程序间通信

信号

程序生成信号触发另一个进程中该信号的处理器

信号: 是软件生成的事件,与跨进程的异常类似,不同于异常,信号根据编号来识别,而不是堆栈

某些信号由不常见的系统事件生成,如果信号没有得到处理,可能结束某个程序

其实是Python解释器的作用域之外的,受控于操作系统的非同步事件机制

Python提供了一个Signal模块,允许程序将Python函数登记为信号事件处理器,在window和Unix平台下都可以使用,但是windows平台下可能定义的待捕获信号较少

  • singal.singal: 接受信号编号和函数对象,布置该函数为此信号编号抛出时的处理器

    • Python在信号出现时自动恢复大多数信号处理器,没有必要在信号处理器内部再次调用这个函数来重新登记处理器

  • singal.pause: 使进程休眠,直到捕获到下一个信号

 

信号必须避免有明显的使用痕迹

只要使用得当,信号提供一个基于事件的通信机制,当需要告诉程序发送了某些事情而不必传入细节时,推荐使用

"""
在Python中捕获信号
将信号编号N作为命令行参数传入,利用shell命令kill-N pid向这个进程发送信号
大多数信号处理器在捕获信号后转到Python中处理
single在window下可用,不过仅定义了少数几个信号雷系,没有os.kill
"""

import sys, signal, time
def now():
    return time.ctime()

def onSingle(signum, stackframe):
    print("Got singal", signum, 'at', now())

signum = int(sys.argv[1])
# 布置信号处理器
signal.signal(signum, onSingle)
# 
while True:
    signal.pause()
信号
原文地址:https://www.cnblogs.com/ZhaoLong-study/p/13573330.html