only in Linux/Mac/Unix,系统调用,每调用一次返回两次,因为操作系统自动把当前进程(称为父进程)复制了一份(称为子进程),子进程永远返回0
,而父进程返回子进程的ID。理由是,一个父进程可以fork出很多子进程,所以,父进程要记下每个子进程的ID,而子进程只需要调用getppid()
就可以拿到父进程的ID。
有了fork
调用,一个进程在接到新任务时就可以复制出一个子进程来处理新任务,常见的Apache服务器就是由父进程监听端口,每当有新的http请求时,就fork出子进程来处理新的http请求。
import os
print('Process (%s) start...' % os.getpid())
# Only works on Unix/Linux/Mac:
pid = os.fork()
if pid == 0:
print('I am child process (%s) and my parent is %s.' % (os.getpid(), os.getppid()))
else:
print('I (%s) just created a child process (%s).' % (os.getpid(), pid))
运行结果如下:
Process (876) start...
I (876) just created a child process (877).
I am child process (877) and my parent is 876.
multiprocessing
模块就是跨平台版本的多进程模块。
multiprocessing
模块Process
类来代表一个进程对象,下面的例子演示了启动一个子进程并等待其结束:from multiprocessing import Process
import os
# 子进程要执行的代码
def run_proc(name):
print('Run child process %s (%s)...' % (name, os.getpid()))
if __name__=='__main__':
print('Parent process %s.' % os.getpid())
p = Process(target=run_proc, args=('test',)) # 创建子进程
print('Child process will start.')
p.start() # 启动子进程
p.join() # 等待子进程结束后继续往下运行,通常用于进程间同步
print('Child process end.')
执行结果如下:
Parent process 928.
Child process will start.
Run child process test (929)...
Process end.
如果要启动大量的子进程,可以用进程池的方式批量创建子进程:
from multiprocessing import Pool
import os, time, random
def long_time_task(name):
print('Run task %s (%s)...' % (name, os.getpid()))
start = time.time()
time.sleep(random.random() * 3)
end = time.time()
print('Task %s runs %0.2f seconds.' % (name, (end - start)))
if __name__=='__main__':
print('Parent process %s.' % os.getpid())
p = Pool(4)
for i in range(5):
p.apply_async(long_time_task, args=(i,))
print('Waiting for all subprocesses done...')
p.close()
p.join()
print('All subprocesses done.')
执行结果如下:
Parent process 669.
Waiting for all subprocesses done...
Run task 0 (671)...
Run task 1 (672)...
Run task 2 (673)...
Run task 3 (674)...
Task 2 runs 0.14 seconds.
Run task 4 (673)...
Task 1 runs 0.27 seconds.
Task 3 runs 0.86 seconds.
Task 0 runs 1.41 seconds.
Task 4 runs 1.91 seconds.
All subprocesses done.
代码解读:
对Pool
对象调用join()
方法会等待所有子进程执行完毕,调用join()
之前必须先调用close()
,调用close()
之后就不能继续添加新的Process
了。
请注意输出的结果,task 0
,1
,2
,3
是立刻执行的,而task 4
要等待前面某个task完成后才执行,这是因为Pool
的默认大小是CPU的核数, 4核CPU即为最多同时执行4个进程。这是Pool
有意设计的限制,并不是操作系统的限制。如果改成p = Pool(5)
就可以同时跑5个进程。
subprocess
模块可以让我们非常方便地启动一个子进程,然后控制其输入和输出。
下面的例子演示了如何在Python代码中运行命令nslookup www.python.org
,这和命令行直接运行的效果是一样的:
import subprocess
print('$ nslookup www.python.org')
r = subprocess.call(['nslookup', 'www.python.org'])
print('Exit code:', r)
运行结果:
$ nslookup www.python.org
Server: 192.168.19.4
Address: 192.168.19.4#53
Non-authoritative answer:
www.python.org canonical name = python.map.fastly.net.
Name: python.map.fastly.net
Address: 199.27.79.223
Exit code: 0
如果子进程还需要输入,则可以通过communicate()
方法输入:
import subprocess
print('$ nslookup')
p = subprocess.Popen(['nslookup'], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
output, err = p.communicate(b'set q=mx\npython.org\nexit\n')
print(output.decode('utf-8'))
print('Exit code:', p.returncode)
上面的代码相当于在命令行执行命令nslookup
,然后手动输入:
set q=mx
python.org
exit
运行结果如下:
$ nslookup
Server: 192.168.19.4
Address: 192.168.19.4#53
Non-authoritative answer:
python.org mail exchanger = 50 mail.python.org.
Authoritative answers can be found from:
mail.python.org internet address = 82.94.164.166
mail.python.org has AAAA address 2001:888:2000:d::a6
Exit code: 0
Process
之间肯定是需要通信的,操作系统提供了很多机制来实现进程间的通信。Python的multiprocessing
模块包装了底层的机制,提供了Queue
、Pipes
等多种方式来交换数据。
我们以Queue
为例,在父进程中创建两个子进程,一个往Queue
里写数据,一个从Queue
里读数据:
from multiprocessing import Process, Queue
import os, time, random
# 写数据进程执行的代码:
def write(q):
print('Process to write: %s' % os.getpid())
for value in ['A', 'B', 'C']:
print('Put %s to queue...' % value)
q.put(value)
time.sleep(random.random())
# 读数据进程执行的代码:
def read(q):
print('Process to read: %s' % os.getpid())
while True:
value = q.get(True)
print('Get %s from queue.' % value)
if __name__=='__main__':
# 父进程创建Queue,并传给各个子进程:
q = Queue()
pw = Process(target=write, args=(q,))
pr = Process(target=read, args=(q,))
# 启动子进程pw,写入:
pw.start()
# 启动子进程pr,读取:
pr.start()
# 等待pw结束:
pw.join()
# pr进程里是死循环,无法等待其结束,只能强行终止:
pr.terminate()
运行结果如下:
Process to write: 50563
Put A to queue...
Process to read: 50564
Get A from queue.
Put B to queue...
Get B from queue.
Put C to queue...
Get C from queue.
在Unix/Linux下,
multiprocessing
模块封装了fork()
调用,使我们不需要关注fork()
的细节。由于Windows没有fork
调用,因此,multiprocessing
需要“模拟”出fork
的效果,父进程所有Python对象都必须通过pickle序列化再传到子进程去,所以,如果multiprocessing
在Windows下调用失败了,要先考虑是不是pickle失败了。
Python的线程是真正的Posix Thread,而不是模拟出来的线程。
多线程实现Master-Worker,主线程就是Master,其他线程就是Worker。多线程模式通常比多进程快一点,但是也快不到哪去,而且,多线程模式致命的缺点就是任何一个线程挂掉都可能直接造成整个进程崩溃,因为所有线程共享进程的内存。
Python的标准库提供了两个模块:
_thread
低级模块
threading
,高级模块,是对_thread
进行了封装。绝大多数情况下,我们只需要使用threading
。
启动一个线程就是把一个函数传入并创建Thread
实例,然后调用start()
开始执行,
threading.current_thread()
它永远返回当前线程的实例MainThread
,子线程的名字在创建Thread实例时指定(仅做显示无实际意义)
#!/usr/bin/env python3
# -*- coding: UTF-8 -*-
import time, threading
def loop():
print('The thread %s is running...' % threading.current_thread().name)
n = 0
while n < 5:
n += 1
print('The thread %s is running >>> %s' %(threading.current_thread().name, n))
time.sleep(1)
print('The thread %s ended' % threading.current_thread().name)
print('The thread %s is running' % threading.current_thread().name)
t = threading.Thread(target=loop, name= 'loopthread')
t.start()
t.join()
print('The thread %s ended' % threading.current_thread().name)
>>>meij1/Videos/OdoCSV/t3.py
The thread MainThread is running
The thread loopthread is running...
The thread loopthread is running >>> 1
The thread loopthread is running >>> 2
The thread loopthread is running >>> 3
The thread loopthread is running >>> 4
The thread loopthread is running >>> 5
The thread loopthread ended
The thread MainThread ended
在多进程中,同一个变量会copy到每个进程中互不影响,但是在多线程中大家共享同一个变量,为避免大家同时更改同一个变量,需要用到lock机制
看这个例子
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from ast import arg
from cProfile import run
import time, threading
balance = 0
def deposit(n):
global balance
balance += n # 先存后取,结果应该为0
balance -= n
def run_it(n):
for i in range(200000):
deposit(n)
t1 = threading.Thread(target=run_it, args=(5,))
t2 = threading.Thread(target=run_it, args=(8,))
t1.start()
t2.start()
t1.join()
t2.join()
print(balance)
>>>meij1/Videos/OdoCSV/t2.py
0
>>>meij1/Videos/OdoCSV/t2.py
-8
上面的结果理论上是0,但是只要循环次数足够多也会产生其他结果。
原因是因为高级语言的一条语句在CPU执行时是若干条语句,即使一个简单的计算也分几步执行,而执行这几条语句时,线程可能中断,从而导致多个线程把同一个对象的内容改乱了。
balance = balance + n
在内存中执行为2步:
1. 计算`balance + n`,存入临时变量中;
2. 将临时变量的值赋给`balance`。
x = balance + n
balance = x
由于x是局部变量,两个线程各自都有自己的x,当代码正常执行时:
初始值 balance = 0
t1: x1 = balance + 5 # x1 = 0 + 5 = 5
t1: balance = x1 # balance = 5
t1: x1 = balance - 5 # x1 = 5 - 5 = 0
t1: balance = x1 # balance = 0
t2: x2 = balance + 8 # x2 = 0 + 8 = 8
t2: balance = x2 # balance = 8
t2: x2 = balance - 8 # x2 = 8 - 8 = 0
t2: balance = x2 # balance = 0
结果 balance = 0
但是t1和t2是交替运行的,如果操作系统以下面的顺序执行t1、t2:
初始值 balance = 0
t1: x1 = balance + 5 # x1 = 0 + 5 = 5
t2: x2 = balance + 8 # x2 = 0 + 8 = 8
t2: balance = x2 # balance = 8
t1: balance = x1 # balance = 5
t1: x1 = balance - 5 # x1 = 5 - 5 = 0
t1: balance = x1 # balance = 0
t2: x2 = balance - 8 # x2 = 0 - 8 = -8
t2: balance = x2 # balance = -8
结果 balance = -8
创建一个锁通过threading.Lock()
来保证同时只能有一个线程拿到lock,使用完后释放, 例子可以改为, 获得的锁的线程用完后必须得释放锁,为避免其他线程等待无果,所以用try...finally
确保锁一定被释放。
balance = 0
lock = threading.Lock()
def run_thread(n):
for i in range(100000):
# 先要获取锁:
lock.acquire()
try:
# 放心地改吧:
change_it(n)
finally:
# 改完了一定要释放锁:
lock.release()
多核CPU可以支持多个线程,正常情况下一个死循环程序可以100%掉一个CPU,那么4核CPU可以被4个死循环线程干掉(哈哈,在用C、C++或Java来改写的死循环确实会),但是在python中再多的死循环线程也只会100%掉一个核。
解释器执行代码时,有一个GIL锁:Global Interpreter Lock,任何Python线程执行前,必须先获得GIL锁,然后,每执行100条字节码,解释器就自动释放GIL锁,让别的线程有机会执行。这个GIL全局锁实际上把所有线程的执行代码都给上了锁,所以,多线程在Python中只能交替执行,即使100个线程跑在100核CPU上,也只能用到1个核。
GIL是Python解释器设计的历史遗留问题,通常我们用的解释器是官方实现的CPython,要真正利用多核,除非重写一个不带GIL的解释器。
在Python中,可以使用多线程,但不要指望能有效利用多核。可以通过多进程实现多核任务。多个Python进程有各自独立的GIL锁,互不影响。
在多线程环境下,每个线程都有自己的数据。一个线程使用自己的局部变量比使用全局变量好,因为局部变量只有线程自己能看见,不会影响其他线程,而全局变量的修改必须加锁。
但是局部变量也有问题,就是在函数调用的时候,传递起来很麻烦:
def process_student(name):
std = Student(name)
# std是局部变量,但是每个函数都要用它,因此必须传进去:
do_task_1(std)
do_task_2(std)
def do_task_1(std):
do_subtask_1(std)
do_subtask_2(std)
def do_task_2(std):
do_subtask_2(std)
do_subtask_2(std)
ThreadLocal
解决了参数在一个线程中各个函数之间互相传递的问题,一个ThreadLocal
变量虽然是全局变量,但每个线程都只能读写自己线程的独立副本,互不干扰。
import threading
# 创建全局ThreadLocal对象:
local_school = threading.local()
def process_student():
# 获取当前线程关联的student:
std = local_school.student
print('Hello, %s (in %s)' % (std, threading.current_thread().name))
def process_thread(name):
# 绑定ThreadLocal的student:
local_school.student = name
process_student()
t1 = threading.Thread(target= process_thread, args=('Alice',), name='Thread-A')
t2 = threading.Thread(target= process_thread, args=('Bob',), name='Thread-B')
t1.start()
t2.start()
t1.join()
t2.join()
执行结果:
Hello, Alice (in Thread-A)
Hello, Bob (in Thread-B)
全局变量local_school
就是一个ThreadLocal
对象,每个Thread
对它都可以读写student
属性,但互不影响。你可以把local_school
看成全局变量,但每个属性如local_school.student
都是线程的局部变量,可以任意读写而互不干扰,也不用管理锁的问题,ThreadLocal
内部会处理。
ThreadLocal
最常用的地方就是为每个线程绑定一个数据库连接,HTTP请求,用户身份信息等,这样一个线程的所有调用到的处理函数都可以非常方便地访问这些资源。
注意
分布式进程实例
# 服务进程在windows系统和Linux系统上有所不同
# 创建一个分布式进程:包括服务进程和任务进程
# 多个进程之间的通信使用Queue
# 该代码为服务进程
# 注意,运行时先运行服务进程,再运行任务进程
# 任务执行循序:
# 服务进程和任务进行都创建了相同的两个队列,一个用来放任务,一个用来放结果
# 第一步:服务进程运行,比如将数字2放进任务队列,任务进程从任务队列中取出数字2
# 第二步:取出数字,执行任务,就是2*2=4, 任务执行完后,放进结果队列
# 第三步:服务进程从结果队列中,取出结果。
# 第四步:所有任务执行完毕,所有结果都已经取出,最终任务队列和结果队列都是空的了
# -*- coding:utf-8 -*-
import random, queue
from multiprocessing.managers import BaseManager
from multiprocessing import freeze_support
# 第一步:定义两个Queue队列,一个用于发送任务,一个接收结果
task_queue = queue.Queue()
result_queue = queue.Queue()
# 创建类似的QueueManager,继承BaseManager,用于后面创建管理器
class QueueManager(BaseManager):
pass
# 定义两个函数,返回结果就是Queue队列, 在linux/mac中直接用lambda
def return_task_queue():
global task_queue # 定义成全局变量
return task_queue # 返回发送任务的队列
def return_result_queue():
global result_queue
return result_queue # 返回接收结果的队列
# 第二步:把上面创建的两个队列注册在网络上,利用register方法
# callable参数关联了Queue对象,将Queue对象在网络中暴露
# 在Linux/Mac中用lambda就可以 callable=lambda: task_queue,wins必须用自建的函数,因为lambda不支持序列化
# 第一个参数是注册在网络上队列的名称
def test():
QueueManager.register('get_task_queue', callable=return_task_queue)
QueueManager.register('get_result_queue', callable=return_result_queue)
# 第三步:绑定端口8001,设置验证口令,这个相当于对象的初始化
# 绑定端口并填写验证口令,windows下需要填写IP地址,Linux/Mac下默认为本地,地址可直接为空
manager = QueueManager(address=('127.0.0.1', 8001), authkey=b'abc') # 口令必须写成类似b'abc'形式,只写'abc'运行错误
# 第四步:启动管理器,启动Queue队列,监听信息通道
manager.start()
# 第五步:通过管理实例的方法获访问网络中的Queue对象
# 即通过网络访问获取任务队列和结果队列,创建了两个Queue实例,
task = manager.get_task_queue()
result = manager.get_result_queue()
# 第六步:添加任务,获取返回的结果
# 将任务放到Queue队列中
for i in range(10):
n = random.randint(0, 10) # 返回0到10之间的随机数
print("Put task %s ..." % n)
task.put(n) # 将n放入到任务队列中
# 从结果队列中取出结果
print("Try get results...")
try:
for i in range(10): # #监听结果队列,获取结果数
r = result.get(timeout=5) # 每次等待5秒,取结果队列中的值
print("Result: %s" % r)
except queue.Empty:
print("result queue is empty.")
finally:
# 最后一定要关闭服务,不然会报管道未关闭的错误
manager.shutdown()
print("master exit.")
if __name__ == '__main__':
freeze_support() # Windows下多进程可能出现问题,添加以下代码可以缓解
print("Start!")
test()
# coding: utf-8
# 定义具体的任务进程,具体的工作任务是什么
import time, sys, queue
from multiprocessing.managers import BaseManager
# 创建类似的QueueManager,继承BaseManager,用于后面创建管理器
class QueueManager(BaseManager):
pass
# 第一步:使用QueueManager注册用于获取Queue的方法名称
# 前面服务进程已经将队列名称暴露到网络中,
# 该任务进程注册时只需要提供名称即可,与服务进程中队列名称一致
QueueManager.register('get_task_queue')
QueueManager.register('get_result_queue')
# 第二步:连接到服务器,也就是运行服务进程代码的机器
server_addr = '127.0.0.1'
print("Connet to server %s..." % server_addr)
# 创建一个管理器实例,端口和验证口令保持与服务进程中完全一致
m = QueueManager(address=(server_addr, 8001), authkey=b'abc')
# 连接到网络服务器
m.connect()
# 第三步:从网络上获取Queue对象,并进行本地化,与服务进程是同一个队列
task = m.get_task_queue()
result = m.get_result_queue()
# 第四步:从task队列获取任务,并把结果写入到resul队列
for i in range(10):
try:
# 前面服务进程向task队列中放入了n,这里取出n
# n和n相乘,并将相乘的算式和结果放入到result队列中去
n = task.get(timeout=1) # 每次等待1秒后取出任务
print("run task %d * %d..." % (n, n))
r = '%d * %d = %d' % (n, n, n*n)
time.sleep(1)
result.put(r)
except queue.Empty:
print("task queue is empty.")
# 任务处理结束
print("worker exit.")
启动分布式进程
先运行服务进程,再运行任务进程
服务进程运行结果
(py3.9) C:\Users\meij1\Videos\OdoCSV>python t2.py
Start!
Put task 799 into queue
Put task 277 into queue
Put task 709 into queue
Put task 906 into queue
Put task 332 into queue
Put task 608 into queue
Put task 583 into queue
Put task 95 into queue
Put task 806 into queue
Put task 867 into queue
try to get result...
Results: 799 * 799 = 638401
Results: 277 * 277 = 76729
Results: 709 * 709 = 502681
Results: 906 * 906 = 820836
Results: 332 * 332 = 110224
Results: 608 * 608 = 369664
Results: 583 * 583 = 339889
Results: 95 * 95 = 9025
Results: 806 * 806 = 649636
Results: 867 * 867 = 751689
master exit.
任务进程运行结果
(py3.9) C:\Users\meij1\Videos\OdoCSV>python t1.py
Connecting to server 127.0.0.1...
Run task 799 * 799...
Run task 277 * 277...
Run task 709 * 709...
Run task 906 * 906...
Run task 332 * 332...
Run task 608 * 608...
Run task 583 * 583...
Run task 95 * 95...
Run task 806 * 806...
Run task 867 * 867...
Worker exit.
QueueManager.register(‘get_task_queue’, callable=return_task_queue)
QueueManager.register(‘get_result_queue’, callable=return_result_queue)
这个简单的Master/Worker模型有什么用?其实这就是一个简单但真正的分布式计算,把代码稍加改造,启动多个worker,就可以把任务分布到几台甚至几十台机器上,比如把计算n*n的代码换成发送邮件,就实现了邮件队列的异步发送。
Queue对象存储在哪?注意到task_worker.py中根本没有创建Queue的代码,所以,Queue对象存储在task_master.py进程中:
而Queue之所以能通过网络访问,就是通过QueueManager实现的。由于QueueManager管理的不止一个Queue,所以,要给每个Queue的网络调用接口起个名字,比如get_task_queue。task_worker这里的QueueManager注册的名字必须和task_manager中的一样。对比上面的例子,可以看出Queue对象从另一个进程通过网络传递了过来。只不过这里的传递和网络通信由QueueManager完成。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pB1H39gy-1651988964422)(…/resources/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTEzMTgwNzc=,size_16,color_FFFFFF,t_70.png)]
authkey有什么用?这是为了保证两台机器正常通信,不被其他机器恶意干扰。如果task_worker.py的authkey和task_master.py的authkey不一致,肯定连接不上。
分布式进程 - 廖雪峰的官方网站 (liaoxuefeng.com)
Python分布式进程使用(Queue和BaseManager使用)_Felix-微信(Felixzfb)的博客-CSDN博客
Python分布式进程中你会遇到的坑