python之路-进程、线程、协程篇
进程与线程
什么是进程(process)?
一个程序的执行实例称为进程。一个进程有一个虚拟地址空间、可执行代码、对系统对象的打开句柄、一个安全上下文、一个惟一的进程标识符、环境变量、一个优先级、最小和最大工作集大小,以及至少一个执行线程。每个进程都由一个线程开始,通常称为主线程,但是可以从它的任何线程中创建额外的线程。
程序并不能单独运行,只有将程序装载到内存中,系统为它分配资源才能运行,而这种执行的程序就称之为进程。程序和进程的区别就在于:程序是指令的集合,它是进程运行的静态描述文本;进程是程序的一次执行活动,属于动态概念。
在多道编程中,我们允许多个程序同时加载到内存中,在操作系统的调度下,可以实现并发地执行。这是这样的设计,大大提高了CPU的利用率。进程的出现让每个用户感觉到自己独享CPU,因此,进程就是为了在CPU上实现多道编程而提出的。
有了进程为什么还要线程?
进程有很多优点,它提供了多道编程,让我们感觉我们每个人都拥有自己的CPU和其他资源,可以提高计算机的利用率。既然进程这么优秀,为什么还要线程呢?其实,仔细观察就会发现进程还是有很多缺陷的,主要体现在两点上:
-
进程只能在一个时间干一件事,如果想同时干两件事或多件事,进程就无能为力了。
-
进程在执行的过程中如果阻塞,例如等待输入,整个进程就会挂起,即使进程中有些工作不依赖于输入的数据,也将无法执行。
例如,我们在使用qq聊天, qq做为一个独立进程如果同一时间只能干一件事,那他如何实现在同一时刻 即能监听键盘输入、又能监听其它人给你发的消息、同时还能把别人发的消息显示在屏幕上呢?你会说,操作系统不是有分时么?但分时是指在不同进程间的分时, 即操作系统处理一会你的qq任务,又切换到word文档任务上了,每个cpu时间片分给你的qq程序时,你的qq还是只能同时干一件事呀。
再直白一点, 一个操作系统就像是一个工厂,工厂里面有很多个生产车间,不同的车间生产不同的产品,每个车间就相当于一个进程,且你的工厂又穷,供电不足,同一时间只能给一个车间供电,为了能让所有车间都能同时生产,你的工厂的电工只能给不同的车间分时供电,但是轮到你的qq车间时,发现只有一个干活的工人,结果生产效率极低,为了解决这个问题,应该怎么办呢?。。。。没错,你肯定想到了,就是多加几个工人,让几个人工人并行工作,这每个工人,就是线程!
什么是线程(thread)?
线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
线程是执行上下文,它是CPU需要执行一连串指令所需的所有信息。
假设你正在读一本书,你现在想休息一下,但是你想要回到你所停留的地方,重新开始阅读。实现这个目标的一种方法是记下页码、行号和单词编号。因此,你阅读一本书的执行上下文是这三个数字。
如果你有一个室友,她用的是同样的方法,她可以在你不使用的时候拿起书,然后从她停止的地方继续读下去。然后你可以把它拿回来,然后从你所在的地方重新开始。
进程与线程的区别:
1.进程的内存是独立的,线程共享创建它的进程的地址空间。
2.同一个进程的线程之间可以直接通信,两个进程必须使用进程间通信(中间代理)来与同级进程通信。
3.一个线程可以操作和控制同一个进程的其它线程, 进程只能操作子进程
4.对主线程的更改(取消、优先级更改等)可能会影响流程的其他线程的行为;对父进程的更改不会影响子进程。
5.新线程很容易创建;新进程需要复制父进程。
进程与线程相同点:
1.进程和线程都是一个时间段的描述,是CPU工作时间段的描述。
2.进程和线程都有ID/寄存器组、状态和优先权、信息块,创建后都可更改自己的属性,都可与父进程共享资源、都无法直接访问其他无关进程或线程的资源。
Python threading模块
线程有2种调用方式,如下:
直接调用
import threading
import time
def run(n): #定义每个线程要运行的函数
print("task",n)
time.sleep(2)
#多线程并发:速度快
t1 = threading.Thread(target=run,args=("t1",)) #生成一个线程实例
t2 = threading.Thread(target=run,args=("t2",)) #生成另一个线程实例
t1.start() #启动线程
t2.start() #启动另一个线程
#单线程:速度慢
run("t1")
run("t2")
继承式调用
#-*- coding:utf-8 -*-
#Author:ZhiWenwei
import threading
import time
class MyThread(threading.Thread): #继承父类threading.Thread
def __init__(self,num):
super(MyThread,self).__init__()
self.num = num
def run(self):#把要执行的代码写到run函数里面 线程在创建后会直接运行run函数
print("running on number:%s" %self.num)
time.sleep(3)
if __name__ == '__main__':
t1 = MyThread(1)
t2 = MyThread(2)
t1.start()
t2.start()
多线程并行与串行:
并行:同一时间主线程与子线程一起执行,两者互不影响,速度较快。
串行:从头到尾按照顺序执行,速度较慢。
import threading
import time
def run(n): #定义每个线程要运行的函数
print("task",n)
time.sleep(2)
print("task done",n)
start_time = time.time()
#主线程和子线程是并行的,不是串行,两者互不影响
for i in range(5):
t1 = threading.Thread(target=run, args=("%s"%i,))
t1.start()
#t1.join() #=wait(),相当于变成串行
print("---all threads has finished---")
print("cost",time.time() - start_time)
执行结果:
task 0 task 1 task 2 task 3 task 4 ---all threads has finished--- cost 0.0020034313201904297 task done 0 task done 1 task done 3 task done 2 task done 4
算出并行执行结果:
import threading
import time
def run(n): #定义每个线程要运行的函数
print("task",n)
time.sleep(3)
print("task done",n)
start_time = time.time()
#主线程和子线程是并行的,不是串行,两者互不影响
t_list = []
for i in range(50):
t = threading.Thread(target=run, args=("%s"%i,))
t.start()
t_list.append(t)
for i in t_list:
t.join()
print("---all threads has finished---")
print("cost",time.time() - start_time)
守护线程
如果你设置一个线程为守护线程,就表示你在说这个线程是不重要的,在进程退出的时候,不用等待这个线程退出。 如果你的主线程在退出的时候,不用等待那些子线程完成,那就设置这些线程的daemon属性。即,在线程开始(thread.start())之前,调用setDeamon()函数,设定线程的daemon标志。(thread.setDaemon(True))就表示这个线程“不重要”。如果你想等待子线程完成再退出,那就什么都不用做。,或者显示地调用thread.setDaemon(False),设置daemon的值为false。新的子线程会继承父线程的daemon标志。整个Python会在所有的非守护线程退出后才会结束,即进程中没有非守护线程存在的时候才结束。
例子验证:
import time
import threading
def _demon():
for i in range(1,10):
print(i)
time.sleep(1)
t1 = threading.Thread(target=_demon)
t1.setDaemon(True)
t1.start()
print("main thread end")
得到执行结果:
1
main thread end
线程锁
一个进程下可以启动多个线程,多个线程共享父进程的内存空间,也就意味着每个线程可以访问同一份数据,此时,如果2个线程同时要修改同一份数据,会出现什么状况?
import time
import threading
def addNum():
global num #在每个线程中都获取这个全局变量
print('--get num:',num )
time.sleep(1)
num -=1 #对此公共变量进行-1操作
num = 100 #设定一个共享变量
thread_list = []
for i in range(100):
t = threading.Thread(target=addNum)
t.start()
thread_list.append(t)
for t in thread_list: #等待所有线程执行完毕
t.join()
print('final num:', num )
正常来讲,这个num结果应该是0, 但在python 2.7上多运行几次,会发现,最后打印出来的num结果不总是0,为什么每次运行的结果不一样呢? 哈,很简单,假设你有A,B两个线程,此时都 要对num 进行减1操作, 由于2个线程是并发同时运行的,所以2个线程很有可能同时拿走了num=100这个初始变量交给cpu去运算,当A线程去处完的结果是99,但此时B线程运算完的结果也是99,两个线程同时CPU运算的结果再赋值给num变量后,结果就都是99。那怎么办呢? 很简单,每个线程在要修改公共数据时,为了避免自己在还没改完的时候别人也来修改此数据,可以给这个数据加一把锁, 这样其它线程想修改此数据时就必须等待你修改完毕并把锁释放掉后才能再访问此数据。
*注:不要在3.x上运行,不知为什么,3.x上的结果总是正确的,可能是自动加了锁。
加锁版本:
import time
import threading
def addNum():
global num #在每个线程中都获取这个全局变量
print('--get num:',num )
time.sleep(1)
lock.acquire() #修改数据前加锁
num -=1 #对此公共变量进行-1操作
lock.release() #修改后释放
num = 100 #设定一个共享变量
thread_list = []
lock = threading.Lock() #生成全局锁
for i in range(100):
t = threading.Thread(target=addNum)
t.start()
thread_list.append(t)
for t in thread_list: #等待所有线程执行完毕
t.join()
print('final num:', num )
RLock(递归锁)
说白了就是在一个大锁中还要再包含子锁。
import threading,time
def run1():
print("grab the first part data")
lock.acquire()
global num
num +=1
lock.release()
return num
def run2():
print("grab the second part data")
lock.acquire()
global num2
num2+=1
lock.release()
return num2
def run3():
lock.acquire()
res = run1()
print('--------between run1 and run2-----')
res2 = run2()
lock.release()
print(res,res2)
if __name__ == '__main__':
num,num2 = 0,0
lock = threading.RLock()
for i in range(10):
t = threading.Thread(target=run3)
t.start()
while threading.active_count() != 1:
print(threading.active_count())
else:
print('----all threads done---')
print(num,num2)
semaphore(信号量)
互斥锁 同时只允许一个线程更改数据,而Semaphore是同时允许一定数量的线程更改数据 ,比如厕所有3个坑,那最多只允许3个人上厕所,后面的人只能等里面有人出来了才能再进去。
import threading,time
def run(n):
semaphore.acquire()
time.sleep(1)
print("run the thread:%s"%n)
semaphore.release()
if __name__ == '__main__':
num = 0
semaphore = threading.BoundedSemaphore(5)
for i in range(1,5):
t = threading.Thread(target=run,args=(i,))
t.start()
while threading.active_count() != 1:
pass
else:
print("---all threads done")
print(num)
执行结果:
run the thread:3 run the thread:4 run the thread:1 run the thread:2 ---all threads done 0
协程threading.Event()函数
python线程的事件用于主线程控制其他线程的执行,事件主要提供了三个方法wait()、clear()、set()
事件处理的机制:全局定义了一个“Flag”,如果“Flag”值为 False,那么当程序执行 event.wait 方法时就会阻塞,如果“Flag”值为True,那么执行event.wait 方法时便不再阻塞。
- clear:将“Flag”设置为False
- set:将“Flag”设置为True
用 threading.Event 实现线程间通信,使用threading.Event可以使一个线程等待其他线程的通知,我们把这个Event传递到线程对象中,
Event默认内置了一个标志,初始值为False。一旦该线程通过wait()方法进入等待状态,直到另一个线程调用该Event的set()方法将内置标志设置为True时,该Event会通知所有等待状态的线程恢复运行。
通过Event来实现两个或多个线程间的交互,下面是一个红绿灯的例子,即起动一个线程做交通指挥灯,生成几个线程做车辆,车辆行驶按红灯停,绿灯行的规则。
import threading
import time
event = threading.Event()
def lighter():
count = 1
event.set() #先设置绿灯
while True:
if 5 < count <=10:#改成红灯
event.clear() #清标志位
print("\033[41;1mred light is on...\033[0m")
elif count > 10:
event.set() #变绿灯
count = 0
else:
print("\033[42;1mgreen light is on...\033[0m")
time.sleep(1)
count +=1
def car(name):
while True:
if event.is_set(): #判断,True代表绿灯
print("[%s] running..."%name)
time.sleep(1)
else:
print(" [%s]sees red light,waiting..."%name)
event.wait()
print("\033[34;1m[%s]green light is on start going....\033[34;0m")
light = threading.Thread(target=lighter,)
light.start()
car1 = threading.Thread(target=car,args=("Tesla",))
car1.start()
queue队列
queue模块实现了多生产者,多消费者的队列。当 要求信息必须在多线程间安全交换,这个模块在 线程编程时非常有用 。Queue模块实现了所有要求的锁机制。 说了半天就是Queue模块主要是多线程,保证线程安全使用的。这个类实现了三种类型的queue,区别仅仅在于进去和取出的位置。在一个FIFO(First In,First Out)队列中,先加先取。在一个LIFO(Last In First Out)的队列中,最后加的先出来(操作起来跟stack一样)。priority队列,有序保存,优先级最低的先出来。
queue模块的内容
1. class queue.Queue(maxsize = 0)
构造一个FIFO队列,maxsize可以限制队列的大小。如果队列的大小达到了队列的上限,就会加锁,加入就会阻塞,直到队列的内容被消费掉。maxsize的值小于等于0,那么队列的尺寸就是无限制的
2. class queue.LifoQueue(maxsize = 0)
构造一个Lifo队列
3. class PriorityQueue(maxsize = 0)
优先级最低的先出去,优先级最低的一般使用sorted(list(entries))[0])。典型加入的元素是一个元祖(优先级, 数据)
Queue模块中的常用方法:
- Queue.qsize() 返回队列的大小
- Queue.empty() 如果队列为空,返回True,反之False
- Queue.full() 如果队列满了,返回True,反之False
- Queue.full 与 maxsize 大小对应
- Queue.get([block[, timeout]])获取队列,timeout等待时间
- Queue.get_nowait() 相当Queue.get(False)
- Queue.put(item) 写入队列,timeout等待时间
- Queue.put_nowait(item) 相当Queue.put(item, False)
- Queue.task_done() 在完成一项工作之后,Queue.task_done()函数向任务已经完成的队列发送一个信号
- Queue.join() 实际上意味着等到队列为空,再执行别的操作
实例:
import threading
import queue
q = queue.Queue()
def producer():
for i in range(10):
q.put("骨头 %s" % i)
print("开始等待所有的骨头被取走...")
q.join()
print("所有的骨头被取完了...")
def consumer(n):
while q.qsize() > 0:
print("%s 取到" % n, q.get())
q.task_done() # 告知这个任务执行完了
t = threading.Thread(target=producer, )
t.start()
c1 = consumer("李闯")
多进程multiprocessing
python中的多线程其实并不是真正的多线程,如果想要充分地使用多核CPU的资源,在python中大部分情况需要使用多进程。Python提供了非常好用的多进程包multiprocessing,只需要定义一个函数,Python会完成其他所有事情。借助这个包,可以轻松完成从单进程到并发执行的转换。multiprocessing支持子进程、通信和共享数据、执行不同形式的同步,提供了Process、Queue、Pipe、Lock等组件。
# from multiprocessing import Process
import multiprocessing
import threading
import time,os
def func_f(name):
time.sleep(2)
print('hello',name,os.getpid())
def thread_run():
print("the id thread",threading.get_ident())
if __name__ == '__main__':
for i in range(10):
p = multiprocessing.Process(target=func_f,args=('zww %s'%i,)) #启动十个进程
p.start()
t = threading.Thread(target=thread_run,) #线程
t.start()
#p.join()
每一个子进程都由父进程启动:
#-*- coding:utf-8 -*-
#Author:ZhiWenwei
from multiprocessing import Process
import os
#每一个子进程都由父进程启动
def info(title):
print(title)
print('module name:', __name__)
print('parent process:', os.getppid())
print('process id:', os.getpid())
def f(name):
info('\033[31;1mfunction f\033[0m')
print('hello', name)
if __name__ == '__main__':
info('\033[32;1mmain process line\033[0m')
p = Process(target=f, args=('bob',))
p.start()
p.join()
执行结果:
main process line module name: __main__ parent process: 7664 process id: 11260 function f module name: __mp_main__ parent process: 11260 process id: 2032 hello bob
进程间通讯
不同进程间内存是不共享的,要想实现两个进程间的数据交换,可以用以下方法:
Queue()
from multiprocessing import Process,Queue
def f(q):
q.put([10000,None,'hello!!!!'])
if __name__ == '__main__':
q = Queue()
p = Process(target=f,args=(q,))
p.start()
print(q.get())
p.join()
Pipe()
from multiprocessing import Process,Pipe
def f(conn):
conn.send([9000,None,'I am fine!'])
if __name__ == '__main__':
parent_conn,child_cpnn = Pipe()
p = Process(target=f,args=(child_cpnn,))
p.start()
print(parent_conn.recv())
p.join()
Managers()
from multiprocessing import Process, Manager
import os
def f(d, l):
d[os.getpid()] = os.getpid()
l.append(os.getpid())
print(l)
if __name__ == '__main__':
with Manager() as manager:
d = manager.dict() #生成一个字典,可在多个进程间共享和传递
l = manager.list(range(5)) #生成一个列表。可在多个进程间共享和传递
p_list = []
for i in range(10):
p = Process(target=f, args=(d, l))
p.start()
p_list.append(p)
for res in p_list:
res.join()
print(d)
print(l)
进程同步
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()
进程池
进程池内部维护一个进程序列,当使用时,则去进程池中获取一个进程,如果进程池序列中没有可供使用的进进程,那么程序就会等待,直到进程池中有可用进程为止。
进程池中有两个方法:
- apply
- apply_async
from multiprocessing import Process, Pool
import time
def Foo(i):
time.sleep(2)
return i + 100
def Bar(arg):
print('-->exec done:', arg)
if __name__ == '__main__':
pool = Pool(processes=5) #允许同时放入五个进程
for i in range(10):
pool.apply_async(func=Foo, args=(i,), callback=Bar)
# pool.apply(func=Foo, args=(i,))
print('end')
pool.close()
pool.join() # 进程池中进程执行完毕后再关闭,如果注释,那么程序直接关闭。
协程
协程,又称微线程,纤程。英文名Coroutine。一句话说明什么是线程:协程是一种用户态的轻量级线程。
协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈。因此:
协程能保留上一次调用时的状态(即所有局部状态的一个特定组合),每次过程重入时,就相当于进入上一次调用的状态,换种说法:进入上一次离开时所处逻辑流的位置。
协程的好处:
- 无需线程上下文切换的开销
- 无需原子操作锁定及同步的开销
- "原子操作(atomic operation)是不需要synchronized",所谓原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何 context switch (切换到另一个线程)。原子操作可以是一个步骤,也可以是多个操作步骤,但是其顺序是不可以被打乱,或者切割掉只执行部分。视作整体是原子性的核心。
- 方便切换控制流,简化编程模型
- 高并发+高扩展性+低成本:一个CPU支持上万的协程都不是问题。所以很适合用于高并发处理。
缺点:
- 无法利用多核资源:协程的本质是个单线程,它不能同时将 单个CPU 的多个核用上,协程需要和进程配合才能运行在多CPU上.当然我们日常所编写的绝大部分应用都没有这个必要,除非是cpu密集型应用。
- 进行阻塞(Blocking)操作(如IO时)会阻塞掉整个程序
gevent
gevent是python的一个并发框架,以微线程greenlet为核心,使用了epoll事件监听机制以及诸多其他优化而变得高效.而且其中有个monkey类,
将现有基于Python线程直接转化为greenlet(类似于打patch).他和线程框架性能比高大概4倍(看下图,是gevent和paste的对比):

同步和异步
区别:
1.同步就是发生调用时,一定等待结果返回,整个调用才结束。
2.异步就是发生调用后,立即返回,不等待结果返回。被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用。
import gevent
def foo():
print('Running in foo')
gevent.sleep(0) #让当前的greenlet睡眠N秒,这0标识控制其它协程而不会让其它进程睡眠
print('Explicit context switch to foo again')
def bar():
print('Explicit context to bar')
gevent.sleep(0)
print('Implicit context switch back to bar')
gevent.joinall([ #gevent.Greenlet实例,直到这个greenlet完成或者超时
gevent.spawn(foo), #spawn可以实现一个grennlet实例并且加到队列并且启动,效果类似于gevent.Greenlet(foo).start()
gevent.spawn(bar),
])

协程gevent并发爬网页
from urllib import request
import gevent
import time
from gevent import monkey
monkey.patch_all()#把当前程序所有的io操作做上单独标记
def f(url):
print('get:%s'%url)
resp = request.urlopen(url)
data = resp.read()
f = open("url.html","wb")
f.write(data)
f.close()
print('%d bytes received from %s'%(len(data),data))
urls = ['http://www.cnblogs.com/wenwei-blog/p/8890436.html',
'https://www.google.la/',
'https://www.python.org',
'https://github.com/',
'http://edu.51cto.com/',
]
time_start = time.time()
for url in urls:
f(url)
print("同步cost",time.time() - time_start)
async_time_start = time.time()
gevent.joinall([
gevent.spawn(f,'http://www.cnblogs.com/wenwei-blog/p/8890436.html'),
gevent.spawn(f,'https://www.google.la/'),
gevent.spawn(f,'https://www.python.org'),
gevent.spawn(f,'https://github.com/'),
gevent.spawn(f,'http://edu.51cto.com/'),
])
print("异步cost",time.time() - async_time_start)
浙公网安备 33010602011771号