多进程和多线程
Windows下使用multiprocessing实现多进程。
from multiprocessing import Process
import os
# 子进程的函数
def run_proc(name):
# os.getpid()获取进程号
print('Run child process %s (%s)...' % (name, os.getpid()))
if __name__ == '__main__':
# 父进程即main函数
print('Parent process %s.' % os.getpid())
# 创建子进程
p = Process(target = run_proc, args = ('test',))
# 开启子进程
p.start()
# 等待子进程的结束
p.join()
# End标志父进程结束
print('End')
"""
Parent process 5852.
Run child process test (8536)...
End
"""
Python中有两个多线程库:_thread
与threading
模块,但是建议不要直接使用_thread
,因为_thread
过于接近底层,不方便移植。
import threading
def loop():
# 获取当前线程的name属性
thread_name = threading.current_thread().name
print('Thread %s is running...' % thread_name)
n = 0
# 执行一个计算任务
while n < 5:
n = n + 1
print('Thread %s >>> %d' % (thread_name, n))
print('Thread %s ends.' % thread_name)
# 获取当前线程
thread_name = threading.current_thread().name
print('Thread %s is running...' % thread_name)
# 创建子线程
t = threading.Thread(target = loop, name = 'loopThread')
# 启动子线程
t.start()
# 等待子线程结束
t.join()
# 主线程结束
print('Thread %s ends.' % thread_name)
"""
Thread MainThread is running... # 主线程MainThread
Thread loopThread is running... # 子线程loopThread
Thread loopThread >>> 1
Thread loopThread >>> 2
Thread loopThread >>> 3
Thread loopThread >>> 4
Thread loopThread >>> 5
Thread loopThread ends.
Thread MainThread ends.
"""
多线程往往面临资源的竞争,以存款操作为例:
"""
多线程操作银行存款
"""
import threading
balance = 0
def change_it(n):
"""
操作存款, 先存后取, 0*n=0
"""
global balance
balance = balance + n
balance = balance - n
balance = balance * n
# 理论上, 如果在run_thread中, 确保每次都完整执行完一遍change_it函数, 存款应当是不变的
# 即不论操作多少次, 存款应该是不变的
# 换言之, 如果没有线程的切换, balance最后还是0
def run_thread(n):
for i in range(10000):
change_it(n)
# 创建子线程t1: 每次存取5元, 创建子线程t2: 每次存取8元
t1 = threading.Thread(target = run_thread, args = (5, ))
t2 = threading.Thread(target = run_thread, args = (8, ))
# 开启子线程t1
t1.start()
# 开启子线程t2
t2.start()
# 等待子线程t1结束
t1.join()
# 等待子线程t2结束
t2.join()
# 打印最后的存款, 顺序执行情况下(线程顺序执行, 没有切换)应该是0
# 由于现在是多线程, 可能存在线程的切换, 存款最后可能不再是0
print(balance)
balance就是一个公共资源,由于多线程的切换,可能导致操作资源后出现错误的结果。为了避免这个错误,我们需要在脚本中控制资源的操作,比如加锁,使用run_thread_lock
代替run_thread
:
lock = threading.Lock()
def run_thread_lock(n):
for i in range(10000):
# 先获得锁
lock.acquire()
try:
# 再进行计算修改
change_it(n)
finally:
# 最后释放锁
lock.release()
使用try
和finally
的意义在于:不管哪个线程执行change_it
出现问题,最后都能把锁释放出来留给其他线程。如果没有finally
,很可能某个线程在执行change_it
时出现异常不再执行,而不能进行下一步释放锁,其他线程也得不到锁,就会出现死锁。
为了更方便地保证线程的安全(线程安全就是多线程访问同一资源,不会产生不确定的结果),Python提供了队列结构(使得线程对资源的操作是遵循规则的),队列分为三种:
- FIFO,先进先出;
import queue
import threading
# FIFO
q = queue.Queue()
for i in range(5):
q.put(i)
while not q.empty():
print(q.get())
"""
0
1
2
3
4
"""
- LIFO:后进先出,即堆栈;
# LIFO
q = queue.LifoQueue()
for i in range(5):
q.put(i)
while not q.empty():
print(q.get())
"""
4
3
2
1
0
"""
- Priority Queue:优先队列,数据进入后进行排序,再根据优先级pop;
class Task:
def __init__(self, priority, description):
self.priority = priority # 任务的优先级
self.description = description # 任务的描述信息
# 该魔法方法用于实现对象之间直接比较, 而不需要采用方法调用
def __lt__(self, other):
return self.priority < other.priority
q = queue.PriorityQueue()
# 在元素(Task的实例)put到优先队列时, 自动调用__lt__比较各个实例, 并排序
q.put(Task(1, 'Important task'))
q.put(Task(10, 'Normal task'))
q.put(Task(100, 'Lazy task'))
# 从优先队列中pop元素
def job(q):
while True:
task = q.get()
print('Task: %s\n' % task.description)
q.task_done()
# 创建两个线程同时操作q中的资源
threads = [threading.Thread(target = job, args = (q, )),
threading.Thread(target = job, args = (q, ))]
for t in threads:
# setDaemon(true)来设置线程为“守护线程”, 将一个用户线程设置为守护线程的方式是: 在线程对象创建之前, 调用线程对象的setDaemon方法
# 守护线程也称“服务线程”,在没有用户线程可服务时会自动离开, 守护线程的优先级比较低, 用于为系统中的其它对象和线程提供服务。
t.setDaemon(True)
t.start()
# 等待线程q结束, 即q内元素为空
q.join()
"""
可见, 两个线程能够按照优先级合理执行Task
Task: Important task
Task: Normal task
Task: Lazy task
"""
关于GIL
多线程可以运行在单核上,也可以运行在多核上。一个线程可以某一时间段在一个核心上运行,下一刻在另一个核心上运行。线程是内核调度的最小单位。 一个进程可以有多个线程,它们共同完成某个任务。 线程是被包裹在进程中的,进程提供了线程运行的资源。 进程之间互不影响,一个进程挂掉,并不影响其它进程,然而一个进程内的一个线程出现问题 ,其它线程也无法正常运行。
为了方便开发人员保护公共资源,Python在使用多线程利用多核时,某个线程想要执行,必须先拿到GIL,我们可以把GIL看作是“通行证”,并且在一个python进程中,GIL只有一个,这使得Python的多线程几乎就是单线程。
而上面对于多线程的编程实例,主要目的是让我们了解多线程编程。在Python中,还是使用多进程才能真正提高效率。
我们使用线程局部变量也可以确保线程安全:
import threading
# 线程的局部变量
local_school = threading.local()
def process_student():
std = local_school.student
print('Hello %s (%s)\n' % (std, threading.current_thread().name))
def process_thread(name):
local_school.student = name
process_student()
t1 = threading.Thread(target = process_thread, args = ('Tom', ), name = 'TA')
t2 = threading.Thread(target = process_thread, args = ('Jack', ), name = 'TB')
t1.start()
t2.start()
t1.join()
t2.join()
"""
Hello Tom (TA)
Hello Jack (TB)
"""
我们创建了两个线程,并且分别向两个线程传递不同的参数,两个线程共同操作了线程变量local_school
,如果不定义local_school
是线程局部的,将会使得线程不安全,因为此时local_school
是物理上共用的。然而,由于local_school
是线程局部的,该变量将在每个线程内保持"独立",其实是为两个线程开辟了两个资源。
线程池
线程和进程的上下文切换,创建和销毁都需要成本。线程池可以重用已存在的线程,降低线程创建和销毁造成的消耗:
import time
import threadpool
def long_op(n):
print('%d' % n)
time.sleep(2)
# 创建线程池, 线程池内的线程数量小于单个CPU的核数
pool = threadpool.ThreadPool(2)
# 创建了10个任务
tasks = threadpool.makeRequests(long_op, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
print("len of task: ",len(tasks))
# 把任务扔进线程池
[pool.putRequest(task) for task in tasks]
pool.wait()
"""
中间的空行代表线程池重新创建线程
在同时使用线程池内所有线程后, 就销毁并重建, 因为此时线程池内没有空线程可切换
len of task: 10
12
34
56
78
910
"""
异步IO
假设现在只有一个单核CPU,存在三个线程,一个调度线程S,两个工作线程W1和W2,时间是在三个线程上分配的:
- W1有5个时间长度
XXX'X'X
,其中X'
代表等待外部设备的时间,本身不占用CPU时间; - W2有7个时间长度
YY'Y'Y'YYY
;
在普通的情况下,时间进度可能为SXXYY'SSX'...
,S最开始分配到时间,然后执行W1,又切换到执行W2,此时W2开始访问外部设备,S先询问W2,W2还在等待外部设备,S询问W1的进度,W1不需要等待外部设备,然后执行W1,此时已经轮到W1访问外部设备,然后S将不停询问W1和W2的进度,已确认已经访问完外部设备,才继续执行W1或W2。
可发现,S总是把时间花在询问W1和W2上,这造成了一些CPU时间的浪费。因此,需要异步IO,当代码需要执行一个耗时的IO操作时,它只发出IO指令,并不等待IO结果,然后就去执行其他代码了。一段时间后,当IO返回结果时,再通知CPU进行处理。
异步IO可以提高单个单核CPU上的利用率。
股票信息获取的并发编程
Python都已经有了不同方式的相关封装,我们只需调用它们即可。
直接开辟多线程方式
import requests
import threading
def get_stock(code):
url = 'http://hq.xxx.cn/list=' + code
resp = requests.get(url).text
print('%s\n' % resp)
codes = ['sz000878', 'sh600993', 'sz000002', 'sh600153', 'sz002230', 'sh600658']
threads = [threading.Thread(target = get_stock, args = (code, )) for code in codes]
for t in threads:
t.start()
for t in threads:
t.join()
线程池方式
import requests
import threadpool
def get_stock(code):
url = 'http://hq.xxx.cn/list=' + code
resp = requests.get(url).text
print('%s\n' % resp)
codes = ['sz000878', 'sh600993', 'sz000002', 'sh600153', 'sz002230', 'sh600658']
pool = threadpool.ThreadPool(2)
tasks = threadpool.makeRequests(get_stock, codes)
[pool.putRequest(task) for task in tasks]
pool.wait()
异步IO方式
import aiohttp
import asyncio
# 该装饰器可以使任务在执行时是异步IO的方式
@asyncio.coroutine
def get_stock(code):
url = 'http://hq.xxx.cn/list=' + code
resp = yield from aiohttp.request('GET', url)
body = yield from resp.read()
print(body.decode('gb2312'))
codes = ['sz000878', 'sh600993', 'sz000002', 'sh600153', 'sz002230', 'sh600658']
# 使用异步IO的消息循环机制
tasks = [get_stock(code) for code in codes]
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))
loop.close()