并行系统工具(二)
- Python中,两种线程模型有所不同:在
_thread
中,大多数平台上的程序随着其父线程的退出而退出;但在threading
中,通常不会这样,除非子线程被设置为守护线程。 - 使用进程时,子进程通常比父进程存在的时间长,进程的独立性和自主性更强。
可以用于进程通信的简单机制:
- 简单的文件
- 命令行参数
- 程序退出状态码
- shell环境变量
- 标准流重定向
os.popen
和subprocess
管道的流管道
高级的进程通信方式:
- 信号:可以向其他程序发送简单的通知事件
- 匿名管道:允许共享文件描述符的线程及相关进程传递数据,但是一般来说依赖Unix下的进程分支模型,后者不够跨平台移植
- 命名管道:映射到系统的文件系统,允许完全不相关的程序交流,但并非所有平台的Python都提供该功能
- 套接字:映射到系统级别的端口号,支持本地和联网传输,移植性更强
- 共享内存
管道是单向的通道,管道两端的程序仅能看到自己一端的管道,使用Python文件调用方法来处理。
- 命名管道(FIFO)在计算上由一个文件表示,因为命名管道是真正的外部文件,所以进行通信的进程无须相关。
- 匿名管道仅在进程内部存在,并且通常与进程分支合用,作为一种在应用程序内部连接父进程及其子进程的手段。匿名管道对于线程也是适用的。父进程和子进程通过共享的管道文件描述符进行交流,后者会被派生进程继承。
def child(pipeout):
z = 0
while True:
time.sleep(z)
os.write(pipeout, ('Foo bar: {}'.format(z)).encode())
z = (z+1) % 5
def main():
pipein, pipeout = os.pipe()
if os.fork() == 0:
# 子进程关闭输入管道
os.close(pipein)
child(pipeout)
else:
os.close(pipeout)
while True:
# Parent process will be blocked before finishing reading.
line = os.read(pipein, 64)
print('Parent %d got [%s] at %s' % (os.getpid(),
line, time.time()))
main()
- 可以使用
os.fdopen(pipein)
的方法将匿名管道描述符封装到文件对象中。使用os.dup2(fd1, fd2)
将文件描述符fd1
命名的文件的所有相关系统信息复制到由fd2
命名的文件中。 - 命名管道(FIFO)会创建一个作为文件系统中真实的命名文件而存在的长时间运行的管道。可以作用于线程、进程及独立启动的程序之间的IPC机制。FIFO管道更适合作为独立客户端和服务器程序的一般IPC机制。
- 使用
os.mkfifo(filename)
可以创建具名管道。使用的示例如下,server
进程监听来自client
进程发送的消息,然后会在终端打印出来:
# server
fifo_file = '/tmp/fifo'
def main():
if not os.path.exists(fifo_file):
os.mkfifo(fifo_file)
pipein = open(fifo_file, 'r')
print('Server is running...')
while True:
line = pipein.readline()[:-1]
print('Message from client: {}'.format(line))
# client
fifo_file = '/tmp/fifo'
def main():
client_id = 0
pipeout = os.open(fifo_file, os.O_WRONLY)
while True:
input_str = input('Input (type q to quit): ')
if input_str != 'q':
os.write(pipeout, ('client id: {}, msg: {}\n'.format(client_id, input_str)).encode())
else:
exit(0)
- 套接字可以让数据传输在同一台计算机上的不同程序间进行,也可以在远程联网的机器上的程序间进行。
- Python的
signal
模块,允许程序将Python函数登记为信号事件处理器。在多线程程序中,只有主线程能够设置信号处理器并对信号进行应答。 multiprocessing
能够发挥多处理器的威力的同时,也能够保留线程模型带来的大部分简易性和可移植性。使用方法可以参照threading
模块,基本类似。
def whoami(label, lock):
with lock:
msg = '{}: name: {}, pid: {}'.format(label, __name__,
os.getpid())
print(msg)
def main():
lock = multiprocessing.Lock()
whoami('function call', lock)
p = multiprocessing.Process(target=whoami, args=('child', lock))
p.start()
p.join()
for i in range(5):
multiprocessing.Process(target=whoami, args=('run process', lock)).start()
with lock:
print('Main process exit.')
multiprocessing
模块创建的进程可以通过一般的系统级别工具进行通信,为派生进程提供了可移植的消息传递工具:- 模块的
Pipe
对象提供了一个可以连接两个进程的匿名管道。调用后会得到两个Connection
,后者表示管道的两端。管道默认是双向的,并可以发送可接受任何可以进行pickle的Python对象。 - 模块的
Value
和Array
对象实现共享的进程、线程安全的内存用于进程间通信。这些调用返回基于ctypes模块并在共享内存中创建的标量和数组对象,默认带有访问同步化设置。 - 模块的
Queue
对象可以用作Python对象的一个先进先处的列表,其中允许多个生产者和消费者。本质来看,队列是一个管道加上用来协调随机访问的锁机制,并几成了Pipe
关于可进行pickle
操作的限制。
- 模块的
多进程下生产者消费者模型:
class Counter(Process):
label = '@'
def __init__(self, start, queue):
self.state = start
self.post = queue
Process.__init__(self)
def run(self):
for i in range(4):
time.sleep(1)
self.state += 1
print(self.label, self.pid, self.state)
self.post.put([self.pid, self.state])
print(self.label, self.pid, '-')
def main():
print('start', os.getpid())
expected = 9
procs = []
post = Queue()
for i in range(3):
p = (Counter(i*100, post))
procs.append(p)
p.start()
while expected:
time.sleep(0.5)
try:
data = post.get(block=False)
except queue.Empty:
print('no data...')
else:
print('posted:', data)
expected -= 1
[p.join() for p in procs]
print('finish', os.getpid(), [p.exitcode for p in procs])
if __name__ == '__main__':
main()