Part A. 基础知识及不同并行方式的对比
一、基础知识
1、线程是最小的执行单元,进程由至少一个线程组成。
2、调度进程和线程,是完全由操作系统决定的。
程序自己不能决定什么时候执行,执行多长时间。
3、并行数目不可乱设置。
并行是有上限的。无论是多进程还是多线程,数目一多,效率不提反降。这是因为环境切换是有要消耗资源的。
二、概念的对比
1、多进程(启动多个自身进程、启动另外的进程、进程通信、分布式进程-即多机器);
2、多线程(产生多线程、锁):GIL全局锁导致它只能单核,所以多线程并发不可行。也就不用怎么学它都行。
3、异步IO(协程、一些封装好的库或函数):考虑到CPU和IO之间巨大的速度差异,一个任务在执行的过程中大部分时间都在等待IO操作。只使用单线程执行,却能跳过IO的等待去执行其它代码,避免切换环境导致的消耗。它是多任务编程的一个主要趋势。
三、选择何种并行模式
1、计算密集型
(1)描述:大量消耗(单核)CPU资源。要最高效地利用CPU,计算密集型任务同时进行的数量应当等于CPU的核心数,避免切换任务。
(2)编程语言的选择:C语言。代码运行效率至关重要,不要增加CPU的负担。
(3)应用场景例子【后续自行补充】:计算圆周率、对视频进行高清解码。
(4)对于python,我个人认为是使用多进程
。
2、IO密集型
(1)描述:CPU利用率低,大部分时间在等待IO操作完成。异步IO
专门专对这个问题。
(2)编程语言的选择:python。可以更快完成代码,不需要优化CPU负载。
(3)应用场景例子:Web应用。
(4)对于python,我个人认为是使用多线程
,或者异步IO
。
Part B. Python语言
主要学习内容:【重点:异步IO+多进程。计算密度高要多线程时,可用C语言扩展机制ctypes。后者待学习。】
一、多进程。multiprocessing库
1、Process类。用于创建一个进程对象。
两个核心函数:
start()方法启动进程。
join()方法等待子进程结束后再继续往下运行,用于进程间的同步。
多个进程:
如果想创建多个进程,就增加新的变量p2,多次调用start、join函数。推荐使用 Pool类直接实现。
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() # join()方法可以等待子进程结束后再继续往下运行,通常用于进程间的同步。
print('Child process end.')
2、Pool类。进程池,用于创建大量的子进程。
核心函数:
Pool():初始化进行池。参数是里面能同时执行的进程有多少个,默认大小是CPU的核数。
apply_async():创建一个新进程去执行某函数。
close():调用join()之前必须先调用close(),调用之后就不能继续添加新的Process了。
获取进程ID:
os.getpid() 获取当前进行的ID
os.getppid() 获取父进程的ID
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,)) # 注意一定要保留i后面的逗号。原因暂时未知,否则不能调用 long_time_task 函数。
print('Waiting for all subprocesses done...')
p.close()
p.join()
print('All subprocesses done.')
请注意输出的结果,task 0,1,2,3是立刻执行的,而task 4要等待前面某个task完成后才执行,这是因为Pool的默认大小在我的电脑上是4,因此,最多同时执行4个进程。硬件制约导致的。
3、subprocess类。很多时候,子进程并不是自身,而是一个外部进程。
本小结除了参考的,还需要找更多的资料才行。下面是简单摘录参考的。
我们创建了子进程后,还需要控制子进程的输入和输出。下面的例子演示了如何在Python代码中运行命令nslookup www.python.org,这和命令行直接运行的效果是一样的:
import subprocess
print('$ nslookup www.python.org')
r = subprocess.call(['nslookup', 'www.python.org'])
print('Exit code:', r)
4、进程间通信
Process之间肯定是需要通信的,操作系统提供了很多机制来实现进程间的通信。Python的multiprocessing模块包装了底层的机制,提供了Queue、Pipes等多种方式来交换数据。
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进程里是死循环,无法等待其结束,只能强行终止。这里也能看出不是全部的新进程都是调join来等待其结束。
pr.terminate()
5、分布式进程-多机器进程 managers类
在Thread和Process中,应当优选Process,因为Process更稳定,一个进程挂了相互不影响,而线程一个挂了可能会导致整个进程瘫痪。而且,Process可以分布到多台机器上,而Thread最多只能分布到同一台机器的多个CPU上。
Python的multiprocessing模块不但支持多进程,其中managers子模块还支持把多进程分布到多台机器上。一个服务进程可以作为调度者,将任务分布到其他多个进程中,依靠网络通信。由于managers模块封装很好,不必了解网络通信的细节,就可以很容易地编写分布式多进程程序。
import random, time, queue
from multiprocessing.managers import BaseManager
# 发送任务的队列:
task_queue = queue.Queue()
# 接收结果的队列:
result_queue = queue.Queue()
# 从BaseManager继承的QueueManager:
class QueueManager(BaseManager):
pass
# 把两个Queue都注册到网络上, callable参数关联了Queue对象:
QueueManager.register('get_task_queue', callable=lambda: task_queue)
QueueManager.register('get_result_queue', callable=lambda: result_queue)
# 绑定端口5000, 设置验证码'abc':
manager = QueueManager(address=('', 5000), authkey=b'abc')
# 启动Queue:
manager.start()
# 获得通过网络访问的Queue对象:
task = manager.get_task_queue()
result = manager.get_result_queue()
# 放几个任务进去:
for i in range(10):
n = random.randint(0, 10000)
print('Put task %d...' % n)
task.put(n)
# 从result队列读取结果:
print('Try get results...')
for i in range(10):
r = result.get(timeout=10)
print('Result: %s' % r)
# 关闭:
manager.shutdown()
print('master exit.')
请注意,当我们在一台机器上写多进程程序时,创建的Queue可以直接拿来用,但是,在分布式多进程环境下,添加任务到Queue不可以直接对原始的task_queue进行操作,那样就绕过了QueueManager的封装,必须通过manager.get_task_queue()获得的Queue接口添加。
任务进程要通过网络连接到服务进程,所以要指定服务进程的IP。
然后,在另一台机器上启动任务进程(本机上启动也可以):
import time, sys, queue
from multiprocessing.managers import BaseManager
# 创建类似的QueueManager:
class QueueManager(BaseManager):
pass
# 由于这个QueueManager只从网络上获取Queue,所以注册时只提供名字:
QueueManager.register('get_task_queue')
QueueManager.register('get_result_queue')
# 连接到服务器,也就是运行task_master.py的机器:
server_addr = '127.0.0.1'
print('Connect to server %s...' % server_addr)
# 端口和验证码注意保持与task_master.py设置的完全一致:
m = QueueManager(address=(server_addr, 5000), authkey=b'abc')
# 从网络连接:
m.connect()
# 获取Queue的对象:
task = m.get_task_queue()
result = m.get_result_queue()
# 从task队列取任务,并把结果写入result队列:
for i in range(10):
try:
n = task.get(timeout=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.')
6、一些拓展的知识点
只需要了解概念就好。
(1)setDaemon 设置守护进程/线程 参考
它的作用与join相反,我通常用join。作用:主线程结束,子线程尽管没结束也会终止。
二、多线程
参考1-里面讲的还是比较好的,除了Queue 手工还加了锁是没必要的
多线程和多进程最大的不同在于,多进程中,同一个变量,各自有一份拷贝存在于每个进程中,互不影响,而多线程中,所有变量都由所有线程共享。
0、 multiprocessing.dummy
multiprocessing.dummy 模块与 multiprocessing 模块的区别: dummy 模块是多线程,而 multiprocessing 是多进程。
遇到要加速 for 循环的时候,可以试试它。将会把list 里面的元素,逐个放到map映射到函数中。
参考1、参考2
from multiprocessing.dummy import Pool as ThreadPool
num_of_threads = 4
pool = ThreadPool(num_of_threads)
ious = pool.map(run_function, one_list)
1、 threading库
核心函数:
Thread() 创建线程。参数 name,代表线程的名字。
start()、join():上文已解释。
获取线程名称:
主线程实例的名字叫MainThread,子线程的名字在创建时指定,默认是会自动生成名字。获取名字时用:threading.current_thread().name
import time, threading
# 新线程执行的代码:
def loop():
print('thread %s is running...' % threading.current_thread().name)
n = 0
while n < 5:
n = n + 1
print('thread %s >>> %s' % (threading.current_thread().name, n))
time.sleep(1)
print('thread %s ended.' % threading.current_thread().name)
print('thread %s is running...' % threading.current_thread().name)
t = threading.Thread(target=loop, name='LoopThread') # 这里也有args参数,如果想传参就要 args=(thread_idx,) 它也是要有逗号结尾的,这因为必须大于等于两个参数,所以传单个参数时候要加逗号。多进程传参也是这原因导致的。【否则多线程会报错,多进程会自动不执行。】
t.start()
t.join()
print('thread %s ended.' % threading.current_thread().name)
2、锁
核心函数:
threading.Lock() 创建一个锁。
acquire() 申请获取某个锁。
release() 释放某个锁。
# 需要锁的原因
balance = balance + n 例如改行就要拆更两个语句:
1、计算balance + n,存入临时变量中,即 “临时变量” = balance + n
2、将临时变量的值赋给balance,即 balance = “临时变量”
创建一个锁就是通过threading.Lock()来实现。由于锁只有一个,无论多少线程,同一时刻最多只有一个线程持有该锁,所以,不会造成修改的冲突。
balance = 0
lock = threading.Lock()
def run_thread(n):
for i in range(100000):
# 先要获取锁:
lock.acquire()
try:
# 放心地改吧:
change_it(n)
finally:
# 改完了一定要释放锁:
lock.release()
由于可以存在多个锁,不同的线程持有不同的锁,并试图获取对方持有的锁时,可能会造成死锁。
因为Python的线程虽然是真正的线程,但解释器执行代码时,有一个GIL锁:Global Interpreter Lock,任何Python线程执行前,必须先获得GIL锁,然后,每执行100条字节码,解释器就自动释放GIL锁,让别的线程有机会执行。这个GIL全局锁实际上把所有线程的执行代码都给上了锁,所以,多线程在Python中只能交替执行,即使100个线程跑在100核CPU上,也只能用到1个核。只好多进程提高效率。
3、ThreadLocal类。创建了线程间不会共享(影响)的实例。
ThreadLocal 作用:
有全局变量的优势,能避免局部变量在函数调用的时候,传递起来太麻烦。有局部变量的优势,同名变量间不相互影响,能避免全局变量修改要加锁的问题。总之,兼顾了两者优势。
核心函数:
threading.local() 创建实例。然后不同线程往该实例读取、存放数值,其中同名变量间是相互不影响的。
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()
全局变量local_school是一个ThreadLocal对象,每个Thread对它都可以读写student属性,但互不影响。你可以把local_school看成全局变量,但每个属性如local_school.student都是线程的局部变量,可以任意读写而互不干扰,也不用管理锁的问题,ThreadLocal内部会处理。
4、一些相关辅助
(1)内部就已经实现锁的结构体Queue队列等结构体 参考1
注意:个人认为他还手工加多了锁,这是不对的。
三、异步IO,消息模型
- 在IO操作的过程中,当前线程被挂起,而其他需要CPU执行的代码就无法被当前线程执行了。
- 除了多线程多进程,另一种更好的解决IO问题的方法是异步IO。它是单线程执行,没有切换线程的开销。当代码需要执行一个耗时的IO操作时,它只发出IO指令,并不等待IO结果,然后就去执行其他代码了。一段时间后,当IO返回结果时,再通知CPU进行处理。
1、协程
- 子程序:或者称为函数。子程序调用(函数调用):总是一个入口,一次返回,调用顺序是明确的。一个线程就是执行一个子程序。
- 协程,又称微线程,纤程。英文名Coroutine。协程的调用:执行过程中,在子程序内部可中断(不是函数调用,有点类似CPU的中断),然后转而执行别的子程序,在适当的时候再返回来接着执行。
- 作用:因为协程是一个线程执行,那怎么利用多核CPU呢?最简单的方法是多进程+协程。
Python对协程的支持是通过generator生成器实现的。代码见参考,往后我实践中深入学习过了,再摘录一些精华的。
2、进一步封装的库或函数
- asyncio:是Python 3.4版本引入的标准库,直接内置了对异步IO的支持。
- async和await:为了简化并更好地标识异步IO,从Python 3.5开始引入了新的语法async和await,可以让coroutine的代码更简洁易读。
- aiohttp:asyncio实现了TCP、UDP、SSL等协议,aiohttp则是基于asyncio实现的HTTP框架。