进程与线程
单核的CPU在一个时间片中只能执行一个程序,各个程序之间抢夺CPU资源
进程:进程就是一个程序在数据集上的一次动态执行过程,进程是资源分配的基本单位。程序运行时,系统会创建一个进程,并为进程分配资源。
线程:复杂代码块的执行,是程序执行的最小单位,也是进程中一个执行路径,每个线程都有自己的任务代码
进程与线程的关系
- 线程不可独立存在,需要依赖进程
- 一个进程可以有多个线程,多个线程共享进程的资源;多个进程间资源是独立的(CPU切换一个线程花费的开销比切换进程小,同时创建一个线程的开销也比进程小)
- 每个进程有独立的资源,当一个进程崩溃时,对其他进程没有影响–进程的稳定性高;但任何一个线程挂掉可能会造成进程崩溃,因为多个线程共享一个进程资源
- 由于GIL锁的缘故,python 中线程实际上是并发运行(即便有多个cpu,线程会在其中一个cpu来回切换,只占用一个cpu资源),而进程才是真正的并行(同时执行多个任务,占用多个cpu资源)
- IO密集型使用多线程(在等待时切换),计算密集型使用多进程(充分利用多核CPU)
并行与并发
- 并行:是指两个或者多个事件在同一时刻发生
- python中的进程属于并行
- 当一个CPU执行一个进程时,另一个CPU可以执行另一个进程
- 两个进程互不抢占CPU资源,能充分利用计算机资源,效率最高
- 并发:并发是指两个或多个事件在同一时间间隔发生
- python中的线程属于并发。
- 不管计算机有多少个CPU,不管你开了多少个线程,同一时间多个任务会在其中一个CPU来回切换,只占用一个CPU,效率并不高;
1、进程
1.1、进程的状态
任务数(进程数)往往大于cpu的核数,即一定有一些任务正在执行,而另外一些任务在等待cpu进行执行,因此导致了有了不同的状态
- 就绪态:运行的条件符合,正在等在空闲cpu执行
- 执行态:cpu正在执行其功能
- 等待态(阻塞):等待某些条件满足,例如一个程序sleep了,此时就处于等待态(再比如一些需要花费时间来等待来自用户、文件、数据库、网络等的输入输出的任务)
- 死亡
1.2、创建进程
创建进程的类:Process([group [, target [, name [, args [, kwargs]]]]])
- target表示调用对象
- args表示调用对象的位置参数元组
- kwargs表示调用对象的字典
- name为别名
- group实质上不使用。
进程的方法:
- is_alive()
- join([timeout])
join()方法可以等待子进程结束后再继续往下运行,通常用于进程间的同步 - run()
- start()
Process以start()启动某个进程 - terminate()。
import multiprocessing
import time
def worker(interval):
n = 5
while n > 0:
print("The time is {0}".format(time.ctime()))
time.sleep(interval)
n -= 1
if __name__ == "__main__":
p = multiprocessing.Process(target = worker, args = (3,))
p.start()
print "p.pid:", p.pid
print "p.name:", p.name
print "p.is_alive:", p.is_alive()
1.3进程池
如果想要启动大量的子进程,可以用进程池的方式批量创建子进程
-
主进程或者主线程会等待子进程或者子线程执行完毕后结束
-
创建进程池后,主进程不会等待进程池中的代码执行完
-
可以使用进程池的.join()方法等待子进程执行完
- p = Pool() 创建进程池,默认创建4个,如果想要更多,加上参数即可:p = Pool(8)
- p.close() 调用close()方法后,就不能再创建进程了
- p.join() 会等待所有子进程执行完毕
from multiprocessing import Pool
if __name__=='__main__':
print 'Parent process %s.' % os.getpid()
p = Pool() #创建进程池,默认创建4个
for i in range(5):
p.apply_async(func_name, args=(i,))
print 'Waiting for all subprocesses done...'
p.close() # 调用close方法后,就不能再创建进程了
p.join() #会等待所有子进程执行完毕
print 'All subprocesses done.'
- 进程 0,1,2,3是立刻执行的
- task 4要等待前面某个task完成后才执行
- 进程池Pool的默认大小是4,最多同时执行4个进程,Pool的默认大小是CPU的核数
1.4进程间通讯
进程间的通讯可以通过socket、文件读写、Queue队列完成
队列:队列类似于一条管道,元素先进先出。队列都是在内存中操作:进程退出,队列清空(队列是一个阻塞的形态)
以下,创建两个子进程,一个往队列里写数据,一个从队列里读数据:
from multiprocessing import Process, Queue
import os, time, random
# 写数据进程执行的代码:
def write(q):
for value in ['A', 'B', 'C']:
print 'Put %s to queue...' % value
q.put(value)
time.sleep(random.random())
# 读数据进程执行的代码:
def read(q):
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()
2、线程
2.1、线程的实现方式
绝大多数情况下,我们只需要使用threading这个高级模块。
实现方式有两种:
(1)函数式
启动一个线程就是把一个函数传入并创建Thread实例,然后调用start()开始执行
import time, threading
# 新线程执行的代码:
def loop():
print('thread %s is running...')
print('thread %s is running...' )
t = threading.Thread(target=loop, name='LoopThread')
t.start() # 启动一个线程
t.join() # 等待所有线程执行完毕
print('thread %s ended')
2)类式
import threading,time
# myThread类继承了线程类threading.Thread,可以重写run方法
class myThread(threading.Thread):
def __init__(self, name):
super().__init__()
self.name = name
# 重写run,run方法中执行需要线程执行的代码,即print_time方法
def run(self):
self.name = threading.Thread.getName(self)
print_time(self.name)
print("线程名:"+self.name)
def print_time(name):
while counter:
time.sleep(delay)
print("{}: {}".format(name, time.ctime(time.time())))
counter -= 1
#创建线程
thread1 = myThread("THREAD-1", 2, 3)
thread2 = myThread("THREAD-2", 5, 2)
print("THREAD-1 is alive?", thread1.is_alive())
thread1.start()
thread2.start()
print("THREAD-1 is alive?", thread1.is_alive())
thread1.join()
thread2.join()
print("THREAD-1 is alive?", thread1.is_alive())
- 多线程比单线程抓取数据的时候快
- 任何进程默认会启动一个线程-主线程MainThread,主线程启动新的线程(子线程的名字在创建时指)
- 主线程等子线程执行结束后结束
- 多线程共享全局变量有抢占资源的问题,可采用同步的互斥锁或者轮询的方式来解决
2.1、线程的通讯
线程发送数据最安全的方式是使用 queue 库中的队列了
创建一个队列,线程通过使用 put() 和 get() 操作来向队列中添加或者删除元素。例如:
from queue import Queue
from threading import Thread
# 线程产生数据
def producer(out_q):
while True:
out_q.put(data)
# 线程消费数据
def consumer(in_q):
while True:
data = in_q.get()
q = Queue()
t1 = Thread(target=consumer, args=(q,))
t2 = Thread(target=producer, args=(q,))
t1.start()
t2.start()
Queue 对象已经包含了必要的锁,所以你可以通过它在多个线程间多安全地共享数据。当使用队列时,协调生产者和消费者的关闭问题可能会有一些麻烦。一个通用的解决方法是在队列中放置一个特殊的值,当消费者读到这个值的时候,终止执行。
生产者和消费者模型:
- 生产者就是生产数据的线程,消费者就是消费数据的线程
- 如果生产者处理速度很快,而消费者处理速度很慢,那么生产者就必须等待消费者处理完,才能继续生产数据;如果消费者的处理能力大于生产者,那么消费者就必须等待生产者
- 阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力,解决生产者与消费者的强耦合
2.2、线程共享全局变量问题
同步是“协同步骤”,不是“一起”
多进程中,对于同一变量,各自有一份拷贝,互不影响
多线程中,所有变量由所有线程共享,当同时修改某一个共享数据的时候,需要进行同步控制,否则多线程会把共享数据搞乱。
2.2.1、线程锁
- 线程同步能保证多个线程安全访问竞争资源。最简单的同步机制是引入互斥锁(互斥锁保证了每次只有一个线程进行写入操作,从而保证了多线程先数据的准确性)。
- 线程锁
创建锁 :mt = threading.Lock()
锁定 :mt.acquire()
释放 :mt.release()
来看看多个线程同时操作一个变量怎么把内容给改乱了:
import time, threading
# 假定这是你的银行存款:
balance = 0
def change_it(n):
# 先存后取,结果应该为0:
global balance
balance = balance + n
balance = balance - n
def run_thread(n):
for i in range(100000):
change_it(n)
t1 = threading.Thread(target=run_thread, args=(5,))
t2 = threading.Thread(target=run_thread, args=(8,))
t1.start()
t2.start()
t1.join()
t2.join()
print(balance)
高级语言的一条语句在CPU执行时是若干条语句,即使一个简单的计算:
balance = balance + n
也分两步:
计算balance + n,存入临时变量中;
将临时变量的值赋给balance。
也就是可以看成:
x = balance + n
balance = x
但是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
究其原因,是因为修改balance需要多条语句,而执行这几条语句时,线程可能中断,从而导致多个线程把同一个对象的内容改乱了。
如果我们要确保balance计算正确,就要给修改数据的方法change_it()上一把锁,当某个线程开始执行修改数据change_it()时,该线程因为获得了锁,因此其他线程不能同时执行change_it(),只能等待,直到锁被释放后,获得该锁以后才能改。
由于锁只有一个,无论多少线程,同一时刻最多只有一个线程持有该锁,所以,不会造成修改的冲突。
balance = 0
lock = threading.Lock()
def run_thread(n):
for i in range(100000):
# 先要获取锁:
lock.acquire()
try:
change_it(n)
finally: # finally的代码,不管有没有报错,都会执行
# 改完了一定要释放锁:
lock.release()
2.2.2、轮询方式
第二个线程一开始并没有执行累加g_num的操作,而是先进行一个死循环(因为判断条件g_flag不满足),在这个循环中不断的"询问"g_flag的值是否不等于1,一但g_flag不等于1,即test1()结束后便开始干"正事"
from threading import Thread
g_num = 0
g_flag = 1 # 增加一个标识全局变量
def test1():
global g_num
global g_flag
if g_flag == 1:
for i in range(1000000):
g_num += 1
g_flag = 0
print('---test1 g_num is %d---' % g_num)
def test2():
global g_num
# 轮询
while True:
if g_flag != 1: # 一旦test1()执行完,即g_flag = 0时,test2()开始执行累加g_num操作
for i in range(1000000):
g_num += 1
break
print('---test2 g_num is %d---' % g_num)
t1 = Thread(target=test1)
t1.start()
t2 = Thread(target=test2)
t2.start()
print('-----g_num: %d-----' % g_num)
2.3、死锁
- t1的代码在等待mutexB解锁的时候t2在等待mutexA解锁
- t1必须先执行完mutexB锁中的代码执行完才能释放mutexA,t2必须先执行完mutexA锁中的代码执行完才能释放mutexB
- 导致两个线程一直等待下去形成死锁,会浪费CPU资源.
解决死锁的办法:
- 设置超时时间 mutexA.acquire(2)
- 也可以从算法上避免死锁
import threading
import time
class MyThread1(threading.Thread):
def run(self):
if mutexA.acquire():
print(self.name + '---do1---up---')
time.sleep(1)
if mutexB.acquire():
print(self.name + '---do1---down---')
mutexB.release()
mutexA.release()
class MyThread2(threading.Thread):
def run(self):
if mutexB.acquire():
print(self.name + '---do2---up---')
time.sleep(1)
if mutexA.acquire():
print(self.name + '---do2---down---')
mutexA.release()
mutexB.release()
if __name__ == '__main__':
mutexA = threading.Lock()
mutexB = threading.Lock()
t1 = MyThread1()
t2 = MyThread2()
t1.start()
t2.start()
运行结果(卡在了这两句,未结束):
Thread-1---do1---up---
Thread-2---do2---up---
2.4、线程在多核CPU中执行
- GIL锁(全局解释锁)保证了前面说的,由于线程共享数据导致的多线程共同操作一个变量,使变量内容改乱的问题
- 但由于GIL是给所有线程强制加了锁,而且只有一个线程执行完100条字节码后(只允许一个线程控制python解释器),解释器才会自动释放GIL锁,导致多个线程无法并行运行,即使有100个线程跑在100个CPU上,也只能用到1个核
- GIL锁成为计算密集型(CPU-bound)和多线程任务的性能瓶颈
例子:
import threading, multiprocessing
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%,也就是仅使用了一核。
2.3、GIL对多线程Python程序的影响
计算密集型任务是那些促使CPU达到极限的任务。这其中包括了进行数学计算的程序,如矩阵相乘、搜索、图像处理等。
I/O密集型任务是一些需要花费时间来等待来自用户、文件、数据库、网络等的输入输出的任务。I/O密集型任务有时需要等待非常久直到他们从数据源获取到他们所需要的内容为止。这是因为在准备好输入输出之前数据源本身需要先进行自身处理。举例来说,一个用户考虑在输入提示中输入什么或者在其自己进程中运行的数据库查询。
GIL对I/O密集型任务多线程程序的性能没有太大的影响,因为在等待I/O时锁可以在多线程之间共享
举一个简单的计算密集型程序例子:
使用单线程和使用双线程实现密集型任务完成时间相差无几。在多线程版本中GIL阻止了计算密集型任务(利用线程进行部分图像处理)线程的并行执行。
因此:
- 即使开启两个线程,但由于锁而变成了单线程任务
- 即使开启两个线程,执行时间并没有比单线程少,原因是锁带来的获取和释放的时间
import time
form threading import Thread
COUNT = 50000000
def count_down(n):
while n > 0:
n -= 1
start = time().time
count_down(COUNT)
end = time().time
print("time taking in seconds-", end - start)
在4核系统上运行得到以下输出:
"time taking in seconds- 6.2000023859845"
使用两个线程并行处理来完成倒计时:
import time
form threading import Thread
COUNT = 50000000
def count_down(n):
while n > 0:
n -= 1
t1 = Thread(target = count_down,args=(COUNT//2,))
t2 = Thread(target = count_down,args=(COUNT//2,))
start = time().time
t1.start()
t2.start()
t1.join()
t2.join()
end = time().time
print("time taking in seconds-", end - start)
再次运行后的结果:
"time taking in seconds- 6.4900023859845"
2.4、如何处理Python中的GIL
在遇到计算密集型任务时,可以使用多进程的方法来避免GIL锁带来的问题
- 进程都有自己的python解释器和内存空间,GIL不会成为问题
- 进程可以充分的利用CPU核数来实现并发任务
- python拥有一个multiprocessing模块,可以轻松创建多进程
代码如下:
import time
from multiprocessing import Process
COUNT = 50000000
def count_down(n):
while n > 0:
n -= 1
p1 = Process(target = count_down,args=(COUNT//2,))
p2 = Process(target = count_down,args=(COUNT//2,))
start = time().time
p1.start()
p2.start()
p1.join()
p2.join()
end = time().time
print("time taking in seconds-", end - start)
或者使用进程池创建:
import time
from multiprocessing import Pool
COUNT = 50000000
def count_down(n):
while n > 0:
n -= 1
pool = Pool(2) # 创建两个进程
start = time().time
r1 = pool.apply_async(count_down,[COUNT//2])
r2 = pool.apply_async(count_down,[COUNT//2])
pool.close()
pool.join()
end = time().time
print("time taking in seconds-", end - start)
执行结果如喜爱:
"time taking in seconds- 4.0600023859845"
总结:
- 多进程相比于多线程版本,性能有所提升
- 但多进程的时间并没有下降到但进程的一半,这是因为进程管理有自己的开销
- GIL锁只存在与传统的python实现方法Cpython中
3、协程
3.1、迭代器
可迭代对象:
- 迭代器—生成器
- 序列(字符串、列表、元祖)
- 字典
可迭代对象与迭代器:
- 可迭代对象包含迭代器
- 如果一个对象拥有__iter__方法,就是可迭代对象;如果一个对象拥有__iter__方法和next方法,就是迭代器
1)iter()
该方法返回的是当前对象的迭代器类的实例,有以下两种写法。
- 用于可迭代对象类的写法,返回该可迭代对象的迭代器类的实例
- 用于迭代器类的写法,直接返回self(即自己本身),表示自身即是自己的迭代器。
2)next()
返回迭代的每一步,实现该方法时注意要最后超出边界要抛出StopIteration异常。
class MyList(object): # 定义可迭代对象类
def __init__(self, num):
self.data = num # 上边界
def __iter__(self):
return MyListIterator(self.data) # 返回该可迭代对象的迭代器类的实例
class MyListIterator(object): # 定义迭代器类,是MyList可迭代对象的迭代器类
def __init__(self, data):
self.data = data # 上边界
self.now = 0 # 当前迭代值,初始为0
def __iter__(self):
return self # 返回该对象的迭代器类的实例;因为自己就是迭代器,所以返回self
def next(self): # 迭代器类必须实现的方法
while self.now < self.data:
self.now += 1
return self.now - 1 # 返回当前迭代值
raise StopIteration # 超出上边界,抛出异常
my_list = MyList(5) # 得到一个可迭代对象
print type(my_list) # 返回该对象的类型
my_list_iter = iter(my_list) # 得到该对象的迭代器实例,iter函数在下面会详细解释
print type(my_list_iter)
for i in my_list: # 迭代
print i
iter()是直接调用该对象的__iter__()方法,并把__iter__()的返回结果作为自己的返回值,故该用法常被称为“创建迭代器
3.2、生成器
在Python中,这种一边循环一边计算的机制,称为生成器(Generator),这样就不必创建完整的list,从而节省大量的空间
使用列表生成式变成generator
>>> g = (x * x for x in range(10))
>>> g
<generator object <genexpr> at 0x104feab40>
g是一个generator,g保存的是一个算法。直到range()中的数据循环完,计算结束,得到list
打印出generator的每一个元素
方法一:使用next()方法
>>> g.next()
0
>>> g.next()
1
>>> g.next()
4
方法二:使用for循环,因为generator是一个可迭代对象
>>> g = (x * x for x in range(10))
>>> for n in g:
... print n
...
0
1
使用用函数来实现generator
def fib(max):
n, a, b = 0, 0, 1
while n < max:
yield b
a, b = b, a + b
n = n + 1
这就是定义generator的另一种方法。如果一个函数定义中包含yield关键字,那么这个函数就不再是一个普通函数,而是一个generator:
>>> fib(6)
<generator object fib at 0x104feaaa0>
- 定义一个函数,来说明生成器的生成规则
- generator的函数,在每次调用next()的时候执行,遇到yield语句返回,再次执行时从上次返回的yield语句处继续执行。
yield关键字有两个作用:
(1)保存当前运行状态,然后暂停执行,即将生成器函数挂起
(2)将yield关键字后面表达式的值作为返回值返回,此时可理解为起到return作用
总结:
- generator是非常强大的工具,在Python中,可以简单地把列表生成式改成generator,也可以通过函数实现复杂逻辑的generator。
- generator的工作原理,是在for循环的过程中不断计算出下一个元素,并在适当的条件结束for循环。
- 对于函数改成的generator来说,遇到return语句或者执行到函数体最后一行语句,就是结束generator的指令,for循环随之结束。
3.3、协程
协程切换任务占用资源很少,效率高
协程的作用:在执行A函数的时候,可以随时中断,去执行B函数,然后中断继续执行A函数(可以自动切换),单着一过程并不是函数调用(没有调用语句),过程很像多线程,然而协程只有一个线程在执行
协程的优势
协程可以很完美的处理IO密集型的问题,处理cpu密集型可以结合多进程+多线程的方式。 IO密集型和CPU密集型
- 执行效率高,因为子程序切换函数,而不是线程,没有线程切换的开销,由程序自身控制切换。于多线程相比,线程数量越多,切换开销越大,协程的优势越明显
- 不需要锁的机制,只有一个线程,也不存在同时写变量冲突,在控制共享资源时也不需要加锁
协程实现
- 使用yield实现多任务,就是协程
def test1():
while True:
print("--1--")
yield
def test2():
while True:
print("--2--")
yield
def main():
t1 = test1()
t2 = test2()
while True:
next(t1)
next(t2)
if __name__ == "__main__":
main()
- 为了更好的使用协程来完成多任务,可使用greenlet模块来实现(使用比较少)
安装:sudo pip3 install greenlet
from greenlet import greenlet
def test1():
while True:
print("--1--")
gr2.switch()
def test2():
while True:
print("--2--")
gr1.switch()
gr1 = greenlet(test1)
gr2 = greenlet(test2)
gr2.switch()
- 使用gevent实现协程(常使用方法)
安装sudo pip3 install gevent
gevent不同于yield和greenlet,它有个优点,遇到延时会自动切换
使用同步的方式写异步IO代码:
# coding:utf8
import requests
import gevent
from gevent import monkey
monkey.patch_all() # 用于将标准库中大部分阻塞式调用修改为协作式运行
def fetch(url):
print("get: {}".format(url))
response = requests.get(url).content
print("{}: {}".format(url, len(response)))
if __name__ == "__main__":
gevent.joinall([
gevent.spawn(fetch, "https://stackoverflow.com/"),
gevent.spawn(fetch, "https://www.douban.com"),
gevent.spawn(fetch, "https://www.github.com")
])
1. gevent.spawn()方法会创建一个新的greenlet协程对象,并运行
2. gevent.joinall()方法的参数是一个协程对象列表,它会等待所有的协程都执行完毕后再退出
3. 程序中有耗时时,将程序中用到的耗时操作的代码自动换成gevent.sleep()
4. 批量添加任务
4、galaxy项目的应用
galaxy项目使用的flask框架,uWSGI Web服务。
uWSGI 启动一个单一的进程和一个单一的线程。可以用 --processes 选项添加更多的进程,或者使用 --threads 选项添加更多的线程 ,也可以两者同时使用:
uwsgi --http :9090 --wsgi-file foobar.py --master --processes 4 --threads 2
galaxy项目在.ini配置文件中配置进程与线程:
[uwsgi]
……
processes = 4
threads = 2
……
同一时刻,可以同时处理4个并行任务,充分利用CPU资源。由于设置了2个线程,每个进程中,两个线程并发执行。