文章目录
1 一些概念
1.1 并行与并发
并发concurrency
并发指计算机同时管理多个和任务的能力。这些任务并不要求在每时每刻同时执行,但是可能在一段时间内交错执行。并发的任务是处理多个用户输入,管理多个I/O任务,或者处理多个独立任务。并发适合依赖诸如文件和网络数据等外部资源的I/O约束型的操作。
并行parallelism
并行是并发的子集。并发要求多个任务每时每刻同时执行。并行的任务是提升计算效率和加速系统性能。并行需要利用多核CPU特性,适合CPU密集型任务。
concurrency in Python
并发类型
- processed-based concurrency:multiprocessing
- thread-based concurrency:threading
- coroutine-based concurrency:async/await关键字
补充:python提供concurent.future库来提供进程/线程并发
1.2 多处理器系统
多核系统:CPU芯片上集成多个核心,结构上可以是对称多处理SMP(所有核心地位相同)和非对称处理(核心有主从之分)。
集群系统:两个或者多个独立系统通过计算机网络组成。将应用程序的不同部分并行运行在集群的不同系统上的技术称为并行计算技术。集群系统结构上可以是对称或者非对称的。
1.3 CPU,物理核心,逻辑核心,超线程
CPU:中央处理器,一个CPU芯片可以集成多个CPU核心。
CPU物理核心:有独立的电路,L1,L2高速缓存,能独立执行指令。
CPU逻辑核心:应用程序将一个高速运转的CPU物理核心看做一个或者多个核心,应用程序视角下的核心就是逻辑核心。如果一个物理核心对应两个逻辑核心,意味着CPU采用了超线程技术。逻辑CPU数量=同时可执行的线程数。
超线程:超线程可以在一个逻辑核等待指令执行的间隔(等待从cache或内存中获取下一条指令),把时间片分配到另一个逻辑核。CPU在这两个逻辑核之间高速切换,让应用程序感知不到这个间隔,误认为自己是独占了一个核。实现超线程需要在CPU物理核心内设置冗余部件,才能让多个线程运行在一个物理核心上,从而实现一个物理核心对应多个逻辑核心。

# mac
# 查看CPU型号
sysctl machdep.cpu.brand_string
# 查看CPU物理核心数
sysctl hw.physicalcpu
# 查看CPU逻辑核心数
sysctl hw.logicalcpu
# linux
# 查看CPU型号
cat /proc/cpuinfo | grep "model name"
# 查看CPU物理核心数
~$ lscpu -p | egrep -v '^#' | wc -l
# 查看CPU逻辑核心数
~$ lscpu -p | egrep -v '^#' | sort -u -t, -k 2,4 | wc -l
2 Python多进程
python多线程无法利用多核优势,在执行并发任务需要使用多进程来完成
2.1 进程池
2.1.1 阻塞调用方式
调用进程会在执行p.map()时阻塞,直到所有任务执行完毕返回结果。p.map()返回的结果列表顺序和参数列表顺序一致。p.imap_unordered()返回的结果列表顺序和参数列表顺序不一致。
from multiprocessing import Pool
def f(x):
return x*x
if __name__ == '__main__':
with Pool(5) as p:
print(p.map(f, [1, 2, 3])) # [1, 4, 9]
print(p.imap_unordered(f, range(10))) # arbitrary order
# 数据并行:将数据分成多个部分,分配给多个进程执行
2.1.2 非阻塞调用方式
方式一:使用apply_async
from multiprocessing import Pool, TimeoutError
import time
import os
def f(x):
return x*x
if __name__ == '__main__':
# start 4 worker processes
with Pool(processes=4) as pool:
# evaluate "f(20)" asynchronously
res = pool.apply_async(f, (20,)) # runs in *only* one process
print(res.get(timeout=1)) # prints "400"
# evaluate "os.getpid()" asynchronously
res = pool.apply_async(os.getpid, ()) # runs in *only* one process
print(res.get(timeout=1)) # prints the PID of that process
# launching multiple evaluations asynchronously *may* use more processes
multiple_results = [pool.apply_async(os.getpid, ()) for i in range(4)]
print([res.get(timeout=1) for res in multiple_results])
# make a single worker sleep for 10 seconds
res = pool.apply_async(time.sleep, (10,))
try:
print(res.get(timeout=1))
except TimeoutError:
print("We lacked patience and got a multiprocessing.TimeoutError")
print("For the moment, the pool remains available for more work")
# exiting the 'with'-block has stopped the pool
print("Now the pool is closed and no longer available")
方式二:使用concurrent.futures
import concurrent.futures
import math
PRIMES = [
112272535095293,
112582705942171
]
def is_prime(n):
pass
def main():
with concurrent.futures.ProcessPoolExecutor() as executor:
for number, prime in zip(PRIMES, executor.map(is_prime, PRIMES)):
print('%d is prime: %s' % (number, prime))
if __name__ == '__main__':
main()
2.1.3 补充
Python built-in apply
apply(function, args=None, kwargs=None)
def f1(a, b=0, c=0):
return a + b + c
def f2(name, age):
return name + ': ' + str(age)
ret = apply(f1, (1), {'b': 2, 'c': 3}) # ret=6,传递的实参为a=1, b=2, c=3
ret = apply(f2, (), {'name': 'aa', 'age': 12}) # ret='aa: 12'
ret = apply(f2, ('bb'), {'age': 12}) # ret='bb: 12'
Python built-in map
map(function, iterable, *iterables):返回一个迭代器生成用iterable的每一项作为参数调用function而得到的结果。如果function需要n个参数,则后续需要传入n个iterable
list(map(lambda x: 2**x, [1, 2, 3, 4, 5, 6, 8])) # [2, 4, 8, 16, 32, 64, 256]
first_it = [1, 2, 3]
second_it = [4, 5, 6, 7]
list(map(pow, first_it, second_it)) # [1, 32, 729]
什么时候使用map,apply,apply_async
- ret_list = concurrent.futures.ProcessPoolExecutor().map(func, [p1, p2, …])本身不阻塞,ret_list为迭代对象,在迭代ret_list时,可能会阻塞,因为结果列表和参数列表保持一致的,如果第i个进程没有执行完毕,则迭代会阻塞在第i个结果元素上。
- Pool.async_apply()调用完立刻返回一个AsyncResuult对象。可以调用AsyncResuult对象的get()来获取调用结果。get()将会阻塞直到func执行结束。
- Pool.apply((func, args, kwargs))将会阻塞直到func执行完毕,Pool.apply(func, args, kwargs) 等价于Pool.apply_async(func, args, kwargs, callback, error_callback).get()。Pool.apply_async有callback,一旦func执行完毕,callback开始执行,因此也可通过callback获取func执行的结果。
import multiprocessing as mp
import time
def foo_pool(x):
time.sleep(2)
return x*x
result_list = []
def log_result(result):
# This is called whenever foo_pool(i) returns a result.
# result_list is modified only by the main process, not the pool workers.
result_list.append(result)
def apply_async_with_callback():
pool = mp.Pool()
for i in range(10):
pool.apply_async(foo_pool, args = (i, ), callback = log_result)
pool.close() # 阻止任何任务向pool提交
pool.join() # 阻塞直到所有工作进程退出,执行join前必须先执行close
print(result_list)
if __name__ == '__main__':
apply_async_with_callback()
2.2 Process对象
2.2.1 使用方式
# 直接使用
from multiprocessing import Process
def f(name):
print('hello', name)
if __name__ == '__main__':
p = Process(target=f, args=('bob',))
p.start()
p.join() # 阻塞直到p执行完毕
# 继承使用
import time
import random
from multiprocessing import Process
class Run(Process):
def __init__(self,name):
super().__init__()
self.name=name
def run(self):
print('%s runing' %self.name)
time.sleep(random.randrange(1,5))
print('%s runing end' %self.name)
if __name__ == '__main__':
p1=Run('anne')
p2=Run('alex')
p1.start() #start会自动调用run
p2.start()
p1.join()
p2.join()
print('主线程')
2.2.2 start method
创建子进程的三种方式
• spawn: 创建一个新的解释器进程,仅仅继承足以支持进程对象执行run()的资源,不继承不必要的资源(内存、文件描述符、线程等),相比于fork方式速度很慢。每个子进程都有全新的状态。(fork() & execv())。
• fork: 对父进程虚拟内存(已初始化的Python解释器、已加载的模块,内存中的对象)进行写时复制,但不复制父进程的线程。父进程所有资源被子进程继承,子进程维护一份父进程状态副本。
• forkserver: 父进程spawn一个服务进程(fork server),如果父进程需要创建子进程,用这个服务进程来fork()子进程。
platform support
• Windows (win32): spawn(default)
• macOS (darwin): spawn(default), fork, forkserver.
• Linux (unix): spawn, fork(default), forkserver.
2.2.3 IPC
IPC有共享内存和消息机制两种类型,服务器/客户端系统还可以通过socket,管道,RPC进行通信,此处不展开。shared memory和server process两节中的例程展现的共享存储方式属于基于数据结构的共享(低级方式),而非基于存储区的共享(高级方式)。
队列
队列是线程安全或者进程安全(不需要程序员考虑临界区问题),对象被放入队列时会被序列化。
from multiprocessing import Process, Queue
def f(q):
q.put([42, None, 'hello'])
if __name__ == '__main__':
q = Queue()
p = Process(target=f, args=(q,))
p.start()
print(q.get()) # prints "[42, None, 'hello']"
p.join()
管道
全双工,不同进程使用同时同一端读取或者写入数据,则数据可能被破坏,因此可能多个进程需要互斥使用管道同一端。2个不同进程同时使用管道不同端,不会破坏数据。parent_conn和child_conn代表管道的两端。都可以调用send()和recv()。调用send()将序列化对象,调用recv()将恢复对象。
from multiprocessing import Process, Pipe
def f(conn):
conn.send([42, None, 'hello'])
conn.close()
if __name__ == '__main__':
parent_conn, child_conn = Pipe()
p = Process(target=f, args=(child_conn,))
p.start()
print(parent_conn.recv()) # prints "[42, None, 'hello']"
p.join()
shared memory
shared memory用于进程之间共享状态。应该尽可能避免进程之间共享状态。multiprocessing提供基于数据结构的内存共享方式(Value,Array)而非基于存储区的共享方式。这些共享对象将是进程和线程安全的(不需要程序员考虑临界区问题)。
from multiprocessing import Process, Value, Array
def f(n, a):
n.value = 3.1415927
for i in range(len(a)):
a[i] = -a[i]
if __name__ == '__main__':
num = Value('d', 0.0)
arr = Array('i', range(10))
p = Process(target=f, args=(num, arr))
p.start()
p.join()
print(num.value) # 3.1415927
print(arr[:]) # [0, -1, -2, -3, -4, -5, -6, -7, -8, -9]
server process
server process用于进程之间共享状态。也是基于数据结构的内存共享方式。相比于使用shared memory objects,使用server process manager更灵活,因为manager对象提供更多类型,不过速度也更慢。
manager对象对应一个本地或远程的服务进程,调用manager.dict()则令服务进程创建dict对象并返回这个共享对象的代理,然后协作的进程可以通过代理访问此共享对象。
server process支持的共享对象类型:list, dict, Namespace, Lock, RLock, Semaphore, BoundedSemaphore, Condition, Event, Barrier, Queue, Value and Array。
from multiprocessing import Process, Manager
def f(d, l):
d[1] = '1'
d['2'] = 2
d[0.25] = None
l.reverse()
if __name__ == '__main__':
with Manager() as manager:
d = manager.dict()
l = manager.list(range(10))
p = Process(target=f, args=(d, l))
p.start()
p.join()
print(d) # {0.25: None, 1: '1', '2': 2}
print(l) # [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
2.2.4 进程同步关系(加锁,释放锁)
from multiprocessing import Process, Lock
def f(l, i):
l.acquire()
try:
print('hello world', i)
finally:
l.release()
if __name__ == '__main__':
lock = Lock()
for num in range(10):
Process(target=f, args=(lock, num)).start()
2.3 subprocess
通常用于启动外部进程。
import subprocess
def main():
print("启动外部进程")
subprocess.run(["ls", "-l"])
print("外部进程执行完毕")
if __name__ == "__main__":
main()
3 参考
https://www.geeksforgeeks.org/python-program-with-concurrency/
https://python-parallel-programmning-cookbook.readthedocs.io/zh-cn/latest/chapter1/index.html
IPC: https://www.geeksforgeeks.org/python-subprocess-module/
1411



被折叠的 条评论
为什么被折叠?



