进程(Process)
进程是操作系统中资源分配的基本单位,每个进程都有自己的内存空间、数据和代码。不同进程之间相互独立,通信比较复杂。多进程适合用于计算密集型任务,因为每个进程可以在不同的 CPU 上运行,充分利用多核处理器。
fork 实现多进程
Unix/Linux操作系统提供了一个fork()系统调用,fork() 操作系统自动把当前进程(称为父进程)复制了一份(称为子进程)。
通过 fork 调用,进程在接到新任务时能派生子进程处理。Apache 服务器通过父进程监听端口,收到请求时 fork 子进程处理。
os.fork() 是一个系统调用,用于创建一个新的子进程。它在当前进程(父进程)上调用,并返回两次:
- 在父进程中返回子进程的进程ID(PID)
- 在子进程中返回 0,表示它是一个子进程。
import os
# 返回当前进程ID
print(os.getpid())
# 调用os.fork 会有两次返回
pid = os.fork()
print(pid)
"""执行结果:
8723 # 父进程的 PID
8724 # 父进程中打印的子进程的 PID
0 # 子进程中打印的值,表示它是子进程
"""
multiprocessing 跨平台实现多进程
编写多进程服务程序,Unix/Linux无疑是最佳选择,因为Windows不支持 fork 调用。不过,Python作为跨平台语言,通过 multiprocessing 模块提供了跨平台的多进程支持。
import multiprocessing
import os
def runin_process(name):
print(f'run in process,the sub process id: {os.getpid()}, name: {name}')
if __name__ == '__main__':
print(f'parent process id is {os.getpid()}')
p = multiprocessing.Process(target=runin_process,args=('雨之仙界',))
print('sub process will start')
p.start() # 启动进程
p.join() # 等待子进程运行结束
print(f'sub process exec end')
"""执行结果
parent process id is 9430
sub process will start
run in process,the sub process id: 9435, name: 雨之仙界
sub process exec end
"""
multiprocessing pool 进程池
如果要启动大量的子进程,可以用进程池的方式批量创建子进程
import multiprocessing
import os
import random
import time
def runin_process_task(index):
print('subprocess[%s] %s started' % (os.getpid(), index))
start = time.time()
time.sleep(random.random() * 3)
end = time.time()
print('subprocess[%s] %s finished in %.2f seconds' % (os.getpid(), index, end - start))
if __name__ == '__main__':
print('parent process id is %s' % os.getpid())
pool = multiprocessing.Pool(3)
for i in range(5):
pool.apply_async(runin_process_task, args=(i,))
print('waiting all subprocess done')
pool.close()
pool.join()
print('all subprocess done')
"""执行结果
parent process id is 9752
waiting all subprocess done
subprocess[9754] 0 started
subprocess[9755] 1 started
subprocess[9756] 2 started
subprocess[9754] 0 finished in 0.34 seconds
subprocess[9754] 3 started
subprocess[9756] 2 finished in 2.18 seconds
subprocess[9756] 4 started
subprocess[9755] 1 finished in 2.38 seconds
subprocess[9754] 3 finished in 2.58 seconds
subprocess[9756] 4 finished in 0.77 seconds
all subprocess done
"""
-
pool = multiprocessing.Pool(3)表示最多同时执行3个进程。这是Pool设计的限制,并不是操作系统的限制。Pool的默认大小是CPU的核数。 -
pool.join()等待所有子进程执行完毕,调用join()之前必须先调用close() -
pool.close()调用之后就不能继续添加新的Process
请注意输出的结果,task 0,1,2是立刻执行的,而task 3要等待前面某个task完成后才执行。
subprocess 子进程
当子进程是一个外部进程。通常需要控制子进程的输入和输出。subprocess模块可以方便地启动一个子进程,然后控制其输入和输出。
在Python代码中运行命令ls -al
import subprocess
"""
subprocess.call() 是 subprocess 模块中的一个方法,用于执行外部命令并等待其执行完成。
它会阻塞程序,直到外部命令执行结束,然后返回该命令的退出码(exit code)。
"""
r0 = subprocess.call(['ls', '-al'])
print('Exit code:', r0)
在Python中执行命令nslookup,然后输入:
set q=mx
python.org
exit
import subprocess
"""
subprocess.Popen 启动一个子进程
1. ['nslookup']:命令和参数的列表,表示启动 nslookup。
2. stdin=subprocess.PIPE:允许向子进程的标准输入发送数据(即模拟键盘输入)。
3. stdout=subprocess.PIPE:允许从子进程的标准输出读取数据(即获取命令输出)。
4. stderr=subprocess.PIPE:允许从子进程的标准错误输出读取数据(即获取错误信息)。
"""
r1 = subprocess.Popen(['nslookup'], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
"""
communicate() 用于与子进程进行交互,发送数据到子进程的标准输入(stdin),并读取标准输出(stdout)和标准错误输出(stderr)
"""
output, err = r1.communicate(b'set q=mx\npython.org\nexit\n')
"""
utf-8编码转换输出
"""
print(output.decode('utf-8'))
"""
r1.returncode 获取子进程的退出状态码。如果返回值为 0,表示进程成功执行;其他值表示出错。
"""
print('Exit code:', r1.returncode)
print(r1)
IPC 进程之间的通信
在 Python 中,进程间通信(Inter-Process Communication,简称 IPC)通常是指不同进程之间交换数据的机制。由于每个进程都有自己的内存空间,不能直接共享数据,因此需要使用专门的 IPC 方法来实现数据传递。
Python 提供了几种常用的进程间通信方式,主要通过 multiprocessing 模块来实现。常见的进程间通信方式包括:
- Queue:适用于多个进程间的消息传递,支持先进先出(FIFO)的方式。
- Pipe:适用于两个进程之间的双向通信。
- Value 和 Array:适用于简单数据类型(如整数、浮点数)的共享内存。
- Manager:适用于复杂的数据结构(如字典、列表等)的共享。
主进程和子进程通过 Queue 进行通信,队列在多进程环境下是安全的,不需要额外的锁机制。
import multiprocessing
def worker(q0):
# 向队列中放入数据
q0.put('Hello from worker!')
if __name__ == '__main__':
# 创建队列对象
q = multiprocessing.Queue()
# 创建子进程
p = multiprocessing.Process(target=worker, args=(q,))
p.start()
# 从队列中获取数据
print(q.get()) # 输出 'Hello from worker!'
p.join() # 等待子进程结束
Pipe 是用于两个进程之间进行双向通信的机制。通过 Pipe() 创建的管道对象提供了两个连接端,分别用于发送和接收数据。
import multiprocessing
def worker(conn):
# 向管道发送数据
conn.send('Hello from worker!')
conn.close()
if __name__ == '__main__':
# 创建管道对象
parent_conn, child_conn = multiprocessing.Pipe()
# 创建子进程
p = multiprocessing.Process(target=worker, args=(child_conn,))
p.start()
# 从管道接收数据
print(parent_conn.recv()) # 输出 'Hello from worker!'
p.join() # 等待子进程结束
Value 和 Array 提供了共享内存,用于在进程之间共享数据。这种方式适用于共享基本数据类型(如整数、浮点数等)。
import multiprocessing
def worker(shared_value0):
# 修改共享值
shared_value0.value = 10
if __name__ == '__main__':
# 创建了一个共享内存对象,可以在多个进程间共享数据
shared_value = multiprocessing.Value('i', 0)
# 创建子进程
p = multiprocessing.Process(target=worker, args=(shared_value,))
p.start()
p.join() # 等待子进程结束
# 输出共享内存中的值
print(shared_value.value) # 输出 10
Manager 提供了一个更加灵活的共享对象机制,允许在进程间共享复杂的数据结构(如列表、字典等)。
import multiprocessing
def worker(shared_dict0):
# 向共享字典添加数据
shared_dict0['key'] = 'value'
if __name__ == '__main__':
# 创建一个共享管理器,允许多个进程访问同一个对象
with multiprocessing.Manager() as manager:
shared_dict = manager.dict() # 创建共享字典对象
# 创建子进程
p = multiprocessing.Process(target=worker, args=(shared_dict,))
p.start()
p.join() # 等待子进程结束
# 输出共享字典中的数据
print(shared_dict) # 输出 {'key': 'value'}
线程(Thread)
进程由若干线程组成,每个进程至少有一个线程,多任务可以通过多进程或单个进程内的多线程来实现。
由于线程是操作系统原生支持的执行单元,因此,许多高级编程语言,包括Python,都内置了多线程的支持。Python的线程是基于POSIX线程(真实的操作系统线程),而非模拟的线程。
Python提供了两个相关模块:_thread 和 threading。其中,_thread 是底层模块,而 threading 是封装了 _thread 的高级模块。绝大多数情况下,我们应优先使用 threading 模块。
import threading
def runin_threading():
print('thread[%s] started' % threading.current_thread().name)
for i in range(5):
print('thread[%s][%s] started' % (threading.current_thread().name, i))
print('thread[%s] finished' % threading.current_thread().name)
# 创建线程
t = threading.Thread(target=runin_threading, name='TestThread')
t.start()
t.join()
print('thread[%s] finished' % threading.current_thread().name)
"""执行结果
thread[TestThread] started
thread[TestThread][0] started
thread[TestThread][1] started
thread[TestThread][2] started
thread[TestThread][3] started
thread[TestThread][4] started
thread[TestThread] finished
thread[MainThread] finished
"""
线程锁 Lock
多线程和多进程的主要区别在于,多进程中每个进程有独立的变量拷贝,互不影响;而多线程中,所有线程共享同一变量,因此多个线程同时修改同一变量可能导致数据混乱。
import threading
import time
# 余额:
balance = 0
def add_balance(n):
global balance
a = balance
# 模拟程序执行1秒
time.sleep(1)
balance = a + n
def min_balance(n):
global balance
a = balance
# 模拟程序执行2秒
time.sleep(2)
balance = a - n
t1 = threading.Thread(target=add_balance, args=(3,))
t2 = threading.Thread(target=min_balance, args=(3,))
t1.start()
t2.start()
t1.join()
t2.join()
print(balance) # 结果为 -3
加锁执行
lock = threading.Lock()
def add_balance(n):
global balance
with lock:
a = balance
# 模拟程序执行1秒
time.sleep(1)
balance = a + n
def min_balance(n):
global balance
with lock:
a = balance
# 模拟程序执行2秒
time.sleep(2)
balance = a - n
多核 CPU
import multiprocessing
import threading
def loop():
x = 0
while True:
x = x ^ 1
for i in range(multiprocessing.cpu_count()):
t = threading.Thread(target=loop)
t.start()
启动与CPU核心数量相同的N个线程,在4核CPU上可以监控到CPU占用率仅有102%,也就是仅使用了一核。
而如果使用C、C++或Java来改写相同的死循环,直接可以把全部核心跑满,4核就跑到400%,8核就跑到800%,为什么Python不行呢?
因为Python的线程虽然是真正的线程,但解释器执行代码时,有一个GIL锁:Global Interpreter Lock,任何Python线程执行前,必须先获得GIL锁,然后,每执行100条字节码,解释器就自动释放GIL锁,让别的线程有机会执行。这个GIL全局锁实际上把所有线程的执行代码都给上了锁,所以,多线程在Python中只能交替执行,即使100个线程跑在100核CPU上,也只能用到1个核。
GIL是Python解释器设计的问题,通常我们用的解释器是官方实现的CPython,要真正利用多核,除非重写一个不带GIL的解释器。
所以,在Python中,可以使用多线程,但不要指望能有效利用多核。如果一定要通过多线程利用多核,那只能通过C扩展来实现。
Python虽然不能利用多线程实现多核任务,但可以通过多进程实现多核任务。多个Python进程有各自独立的GIL锁,互不影响。
ThreadLocal
在Python中,ThreadLocal是threading模块提供的一个类,用于在不同线程之间保持独立的数据。每个线程可以在ThreadLocal对象中存储自己的独立数据,这些数据对其他线程是不可见的。可通过ThreadLocal对象的_thread_local属性来存取属于该线程的特有数据
ThreadLocal使用场景:为每个线程绑定一个数据库连接,HTTP请求,用户身份信息等。
import threading
class DatabaseConnection:
def __init__(self, thread_name):
self.thread_name = thread_name
self.connection = f"Connection for {thread_name}"
def get_connection(self):
return self.connection
# 创建ThreadLocal对象来存储线程特定的数据库连接
db_connection = threading.local()
def get_db_connection():
if not hasattr(db_connection, "connection"):
# 如果当前线程没有连接,则创建一个新的
db_connection.connection = DatabaseConnection(threading.current_thread().name)
return db_connection.connection.get_connection()
def worker():
print(f"Thread {threading.current_thread().name} uses {get_db_connection()}")
threads = []
for i in range(3):
t = threading.Thread(target=worker)
threads.append(t)
t.start()
for t in threads:
t.join()
Process vs Thread
Master-Worker 模式
实现多任务通常采用 Master-Worker 模式
- Master 负责分配任务。
- Worker 负责执行任务。
在不同的实现方式下,Master 和 Worker 的角色有所不同:
- 多进程模式:主进程是 Master,其他进程是 Worker。
- 优点: 稳定性高:子进程崩溃不会影响主进程。进程相互独立,减少了线程间的资源冲突。
- 缺点:创建进程开销大,尤其在 Windows 下。操作系统能同时运行的进程数量有限,过多进程会影响系统调度。
- 多线程模式:主线程是 Master,其他线程是 Worker。
- 优点: 效率较高,特别在 Windows 下。
- 缺点: 稳定性差:任何一个线程崩溃可能导致整个进程崩溃,因为所有线程共享进程的内存。
- 应用:微软的 IIS 服务器采用多线程模式,但由于稳定性差,后来采取了混合模式。
线程切换的开销
无论多进程还是多线程,任务过多时,线程切换的开销会影响效率。
假设有多项任务,切换任务需要保存和恢复状态,类似操作系统在切换进程或线程时所做的工作。如果任务数量过多,系统可能会把大部分时间花费在任务切换上,导致效率下降,最常见的现象是硬盘狂响,系统假死。
计算密集型 vs IO 密集型任务
任务的类型决定是否需要多任务支持:
计算密集型任务
- 特点:需要大量的 CPU 计算,如圆周率计算、视频解码等。
- 适合场景:任务数量应等于 CPU 核心数,任务越多,切换的开销越大,效率下降。
- 语言选择:Python 不适合处理计算密集型任务,C 语言更适合。
IO 密集型任务
- 特点:涉及大量网络、磁盘 IO 操作,CPU 消耗较少。
- 适合场景:Web 应用等,任务越多,CPU 效率越高,但也有上限。
- 语言选择:Python 等脚本语言非常适合 IO 密集型任务。
异步 IO
由于 CPU 和 IO 操作速度差异,单进程单线程模型会导致任务无法并行执行。为此,现代操作系统支持 异步 IO,允许单进程处理多个任务,从而提高效率。 事件驱动模型
- 例子:Nginx 使用事件驱动模型,单进程就能高效支持多任务。
- 在多核 CPU 上,Nginx 可以使用多个进程来充分利用多核性能。 Python 协程
- 协程是单线程异步编程模型,使得多任务编程更高效。
- 通过协程,可以在单线程中高效处理多任务,广泛应用于 IO 密集型任务。
分布式进程
Thread和Process中,应当优选Process,因为Process更稳定,而且,Process可以分布到多台机器上,而Thread最多只能分布到同一台机器的多个CPU上。
Python的 multiprocessing 模块不但支持多进程,其中managers 子模块还支持把多进程分布到多台机器上。一个服务进程可以作为调度者,将任务分布到其他多个进程中。
Master/Worker模型
distributed_manager.py
启动一个 manager 并暴露网络端口
from multiprocessing.managers import BaseManager
from queue import Queue
class QueueManager(BaseManager):
pass
task_queue = Queue()
result_queue = Queue()
def get_task_queue():
return task_queue
def get_result_queue():
return result_queue
QueueManager.register('get_task_queue', callable=get_task_queue)
QueueManager.register('get_result_queue', callable=get_result_queue)
def start_manager():
queue_manager = QueueManager(address=('', 11202), authkey=b'a123456')
queue_manager.start()
return queue_manager
if __name__ == '__main__':
manager = start_manager()
print("Server started, waiting for client connections...")
distributed_server.py
启动服务端进程,将任务添加到队列中,等待客户端消费队列中的任务并且获取任务消费结果
from multiprocessing import Process
from dawn.process.mutiprocessing.distributed_manager import start_manager
def server():
manager = start_manager() # 启动Manager,绑定到11202端口
task_queue = manager.get_task_queue() # 获取任务队列
result_queue = manager.get_result_queue() # 获取结果队列
# 模拟添加任务到队列中
for i in range(5):
task_queue.put(i)
print(f"Task {i} added to the task queue.")
# 模拟获取任务的结果
for _ in range(5):
result = result_queue.get(timeout=100)
print(f"Received result: {result}")
# 关闭QueueManager
manager.shutdown()
if __name__ == '__main__':
# 启动服务器进程
server_process = Process(target=server)
server_process.start()
server_process.join()
distributed_client.py
连接服务,接收服务端任务并将消费结果放到另一个队列中
from multiprocessing.managers import BaseManager
class QueueManager(BaseManager):
pass
# 注册方法,使得客户端可以访问
QueueManager.register('get_task_queue')
QueueManager.register('get_result_queue')
# 连接到任务队列并从中获取任务
def client():
# 连接到服务器上指定的端口
manager = QueueManager(address=('', 11202), authkey=b'a123456')
manager.connect()
task_queue = manager.get_task_queue()
result_queue = manager.get_result_queue()
# 处理任务
while not task_queue.empty():
task = task_queue.get()
print(f"Processing task: {task}")
result = task * 2 # 假设任务是乘以2
result_queue.put(result)
if __name__ == '__main__':
client()
上面的示例,假设启动多个worker,就可以把任务分布到几台甚至几十台机器上。
协程(coroutines)
Python 中的 协程(coroutines) 又称微线程,纤程,是一种用于实现并发操作的编程方式,它是基于生成器(generator)和 asyncio 库的一种异步编程模型。协程可以使得 Python 在处理 I/O 密集型任务时,不需要等待任务完成,可以继续处理其他任务。
Python中的协程是基于generator实现的,在 generator 中,我们不但可以通过 for循环来迭代,还可以不断调用next()函数获取由yield语句返回的下一个值。
但Python的 yield 不但可以返回一个值,它还可以接收调用者发出的参数。
def consumer():
r = ''
while True:
# 接收生产者 send 的数据
n = yield r
if not n: # 如果没有数据直接结束
return
print('[CONSUMER] Consuming %s...' % n)
r = '200 OK'
def produce(c):
# 启动协程,调用生成器的 `__next__` 方法,进入 `yield` 暂停状态
c.send(None)
n = 0
while n < 5:
n = n + 1
print('[PRODUCER] Producing %s...' % n)
r = c.send(n) # 将数据发送给消费者,并接收消费者的响应
print('[PRODUCER] Consumer return: %s' % r)
c.close() # 关闭消费者协程
consumer = consumer()
produce(consumer)
协程本质上是一种 可暂停和恢复执行的函数,通常在执行过程中遇到 await 关键字时挂起,等到某个操作完成后再继续执行。
import asyncio
# `async def`:用于定义一个协程函数,表示该函数是异步的,
# 可以使用`await`来等待其他协程。
async def run():
print("Hello world!")
"""
asyncio.sleep()也是一个async函数,所以线程不会等待asyncio.sleep()
如果把asyncio.sleep(1)看成是一个耗时1秒的IO操作,
在此期间,主线程并未等待,而是去执行EventLoop中其他可以执行的async函数了,
因此可以实现并发执行。
"""
# 异步调用asyncio.sleep(1):
await asyncio.sleep(1)
print("Hello again!")
# 要运行协程,可通过事件循环(asyncio)来启动它。
# 可以通过 asyncio.run() 来运行协程。
asyncio.run(run())
asyncio.gather()同时调度多个async函数;如果把asyncio.sleep()换成真正的IO操作,则多个并发的IO操作实际上可以由一个线程并发执行。
import asyncio
import threading
async def task(name):
print("start task %s! (%s)" % (name, threading.current_thread))
await asyncio.sleep(2)
print("end task %s" % name)
return name
async def main():
# 并发运行任务, 等待任务的执行,并获取执行结果
r0 = await asyncio.gather(task('task1'), task('task2'))
print(r0)
asyncio.run(main())
asyncio的异步网络连接来获取sina、sohu和163的网站首页
import asyncio
async def wget(host):
print(f"wget {host}...")
# 连接80端口:
reader, writer = await asyncio.open_connection(host, 80)
# 发送HTTP请求:
header = f"GET / HTTP/1.0\r\nHost: {host}\r\n\r\n"
writer.write(header.encode("utf-8"))
# 等待直到缓冲区数据被完全发送
await writer.drain()
# 读取HTTP响应:
while True:
line = await reader.readline()
if line == b"\r\n":
break
print("%s header > %s" % (host, line.decode("utf-8").rstrip()))
# Ignore the body, close the socket
writer.close()
await writer.wait_closed()
print(f"Done {host}.")
async def main():
await asyncio.gather(wget("www.sina.com.cn"), wget("www.sohu.com"), wget("www.163.com"))
asyncio.run(main())
aiohttp
asyncio 可以实现单线程并发 I/O 操作,特别适用于服务器端的高并发支持,如 Web 服务器。它支持 TCP、UDP、SSL 等协议,aiohttp 是基于 asyncio 实现的 HTTP 框架。
定义处理不同URL的async函数,通过app.add_routes()添加映射,最后通过run_app()以asyncio的机制启动整个处理流程。
from aiohttp import web
async def index(request):
text = "<h1>Index Page</h1>"
return web.Response(text=text, content_type="text/html")
async def hello(request):
name = request.match_info.get("name", "World")
text = f"<h1>Hello, {name}</h1>"
return web.Response(text=text, content_type="text/html")
app = web.Application()
# 添加路由:
app.add_routes([web.get("/", index), web.get("/{name}", hello)])
if __name__ == "__main__":
web.run_app(app)