一、概述
线程:是操作系统能够进行运算调度的最小单位。它包含在进程之中,是进程实际运作单位。一条线程指的是进程中一个单一顺序的控制流。一个进程可以并发多个线程,每条线程执行不同任务。一个线程是一堆执行指令,可以资源共享。
进程:是一堆资源的整合,管理它里面的线程。如果执行相同的东西,进程和线程之间没有谁快谁慢的说法。线程里面可以资源共享,进程就不行,进程要通过队列等方式。
区别:
1. 线程可以资源共享,进程不行。
2. 线程之间可以通信,进程不行。
3. 线程容易被创建,但是进程就不行,进程消耗较大。
4. 主线程可以影响子线程,进程之间,完全独立。
并发和并行
并发:逻辑上同时发生。例如一个处理器同时处理多个任务,但是在此过程中会抢占CPU,给人一种同时发生的感觉,实际上有先后顺序。
并行:实际上同时发生。例如两个处理器同时处理两个任务,不抢占CPU,真正意义上的两件或多件事一起干。
GIL锁
GIL是全局解释器锁,是cpython里面的一个bug,一次只能进去一个线程,但是可以多进程。如果想要充分利用多核CPU,可以用multprocessing(多进程的一个模块),协程是一个很好的解决方案。
其他知识
python解释器:cpython、pipy、gpython
I/O密集型任务或函数:可以利用休息时间,去干别的,此类任务可以用多线程。
CPU/计算 密集型任务或函数:因为里面东西都要跑,如果只有一个CPU,会出现抢占资源的问题,此类任务可以用协程解决,但是资源和内存都会消耗很大,最好用C来实现。
二、线程
1. 通过threading库来实现多线程
import threading
import time
begin = time.time()
def first():
print('这是第一个人物')
def second(n):
print('这是第二个任务')
print('%s' % n)
t1 = threading.Thread(target=first) # first后面加不加括号都一样,如果函数有参数,一定不能加括号
t2 = threading.Thread(target=second,args=(1,)) # 如果函数有参数,可以加args,参数要以元组形式加
t1.start() # 用start来启动线程
t2.start()
t1.join() # 如果 t1 不执行完,就不往下执行,起阻塞作用
t2.join()
end = time.time()
print(end - begin)
2. 守护线程(t1.setDaemon(True))
如果将一个线程设置为守护线程,那么当主线程执行完,会强制结束守护线程。
import time
import threading
def game(n):
print('%s---开始游戏' % n)
time.sleep(3)
print('游戏结束')
def movie(m):
print('%s---电影开始' % m)
time.sleep(5)
print('电影结束')
t1 = threading.Thread(target=game,args=('荒野大嫖客',))
t2 = threading.Thread(target=movie,args=('黑客帝国',))
threads = []
threads.append(t1)
threads.append(t2)
if __name__ == '__main__':
t1.setDaemon(True) # 主线程结束,守护线程跟着结束
for t in threads:
t.start()
# t.join() # 这里的t是默认继承 t.start() 中最后的t
print('end=======')
在守护线程当中,只会等执行时间长的,上面例子当中,t1时间短,所以在等待t2的时候,t1会执行完,因此结果没变化。如果将t2设置为守护线程,t1结束后,会直接结束t2,不会执行t2后面的内容。
3. 同步锁
如果同时执行100个线程,同时去取num这一个资源,这样很容易出错,有可能第一个线程执行到一半就停下了,然后就开始执行第二个线程。要解决这种问题,就要通过上锁的方式来解决。当开启锁时,只允许当前线程执行,直到这个锁被释放,才会把CPU给下一个线程。
a = threading.Lock() # 把锁赋值给一个对象
a.acquire() # 开启锁
# 这里是中间内容
a.release() # 关闭锁
import threading
import time
def addnum():
global num
# num -= 1 # 拆分成下面的写法,并且中间加点东西才能看出效果
a.acquire() # 开启锁
temp = num
# print(456) # 中间加这个,或者下面的 time.sleep 都行,建议加这个
time.sleep(0.001)
num = temp-1
print('hello')
a.release() # 关闭锁
a = threading.Lock()
num = 100
threads = []
for i in range(100):
t = threading.Thread(target=addnum)
t.start()
threads.append(t)
for t in threads: # 等100个线程跑完,再去执行主线程
t.join()
print('finally--num:',num)
4. 死锁与递归锁
以下这种情况,就会出现死锁的情况
import threading,time
class myThread(threading.Thread):
def doA(self):
lock.acquire()
print(self.name,"gotlockA",time.ctime())
time.sleep(3)
lock.acquire()
print(self.name,"gotlockB",time.ctime())
lock.release()
lock.release()
def doB(self):
lock.acquire()
print(self.name,"gotlockB",time.ctime())
time.sleep(2)
lock.acquire()
print(self.name,"gotlockA",time.ctime())
lock.release()
lock.release()
def run(self): # 这玩意默认执行,特殊方法
self.doA()
self.doB()
if __name__=="__main__":
# lockA=threading.Lock()
# lockB=threading.Lock()
lock = threading.RLock() # 这个是解决办法
threads=[]
for i in range(5):
threads.append(myThread()) # 调用类中的run方法,这个叫做集成式调用
for t in threads:
t.start()
for t in threads:
t.join()
死锁现象就是我们创建了两把锁,当线程一执行完doA时,回去执行doB,然后线程二又会去doA,然后lockA和lockB就乱套了,都需要对方的锁,所以就会卡在那里。
Rlock时递归锁,它内部有个累加器,创建一次acquire,内部就会+1,可以无限加,release一次,就会释放相应的锁,这样就不会出现死锁现象。它的解释顺序相当于:
进学校门---进教室门---出教室门---出学校门
5. 信号量(Semaphore)
它相当于是设置了房间,不管有多少个线程,房间有几个位置,每次就只能进去几个,等里面的出来后,其他线程才能继续进去。
import threading
import time
class mythread(threading.Thread):
def run(self):
if semaphore.acquire():
print(self.name) # 这里的self.name是调用对象名
time.sleep(3)
semaphore.release()
semaphore = threading.BoundedSemaphore(2) # 设置两个房间,每次只能同时进两个线程
# semaphore = threading.Semaphore(5) # 效果和上面这个一样,不过有区别
thread = []
for i in range(20):
thread.append(mythread())
for t in thread:
t.start()
BoundedSemaphore与Semaphore的区别
前者在调用release()的时候,会校验一下当前信号量的值,是否会大于初始值。
假如只定义了5个信号量,释放了5次后,如果继续调用release,前者会抛出异常,后者只会返回None
6. 条件变量同步
wait():条件不满足时调用,线程会释放锁,并进入等待阻塞。
notify():条件创造后调用,通知等待池激活一个线程。
notifyAll():条件创造后调用,通知等待池激活所有线程。
import threading
import time
from random import randint
class producer(threading.Thread):
global baozi
def run(self):
while True:
b = randint(1,100)
print(self.name, ':做了这个包子--', str(b))
if conn.acquire():
baozi.append(b)
conn.notify() # 通知消费者开始吃
conn.release()
time.sleep(3)
class consumer(threading.Thread):
global baozi
def run(self):
while True:
conn.acquire()
if len(baozi) == 0:
conn.wait()
print('消费者吃掉了',baozi[0])
del baozi[0]
conn.release()
time.sleep(0.25)
baozi = []
thread = []
conn = threading.Condition()
for i in range(2):
thread.append(producer())
thread.append(consumer())
for t in thread:
t.start()
for t in thread:
t.join()
可以认为Condition对象维护了一个锁(Lock/RLock)和一个waiting池。线程通过acquire获得Condition对象,当调用wait方法时,线程会释放Condition内部的锁并进入blocked状态,同时在waiting池中记录这个线程。当调用notify方法时,Condition对象会从waiting池中挑选一个线程,通知其调用acquire方法尝试取到锁。
Condition对象的构造函数可以接受一个Lock/RLock对象作为参数,如果没有指定,则Condition对象会在内部自行创建一个RLock。
除了notify方法外,Condition对象还提供了notifyAll方法,可以通知waiting池中的所有线程尝试acquire内部锁。
由于上述机制,处于waiting状态的线程只能通过notify方法唤醒,所以notifyAll的作用在于防止有的线程永远处于沉默状态。
7. 同步条件Event
event.isSet():返回event的状态值;
event.wait():如果 event.isSet()==False将阻塞线程;
event.set(): 设置event的状态值为True,所有阻塞池的线程激活进入就绪状态, 等待操作系统调度;
event.clear():恢复event的状态值为False。
import threading
import time
class boss(threading.Thread):
def run(self):
print('今晚大家要加班!!!!')
event.set() or event.isSet()
time.sleep(5)
print('可以下班回家了')
event.set() or event.isSet()
class worker(threading.Thread):
def run(self):
event.wait()
# lock.acquire() # 加这个的原因时看着输出同一行,很不爽
print('我***,****')
# lock.release()
time.sleep(0.5)
event.clear()
event.wait()
time.sleep(0.5)
# lock.acquire()
print('终于下班了')
# lock.release()
event = threading.Event()
lock = threading.RLock()
thread = []
for i in range(3):
thread.append(worker())
thread.append(boss())
for t in thread:
t.start()
# time.sleep(0.01) # 加这个的原因时看着输出同一行,很不爽
for t in thread:
t.join()
8. 多线程利器 queue
它时一种数据结构,用来储存数据的。它定义了三种队列模式:
1.Queue(maxsize):FIFO队列模式(first in first out),先入先出,缺省参数为0,无穷大
2.lifoQueue(maxsize):LIFO队列模式(last in first out),后入先出
3.PriorityQueue(maxsize):优先级队列模式,不常用,使用它时,项目应是(priority,data)的形式
import queue
q = queue.Queue(3) # 括号里可以设置放入数据的多少,这种方式创建的时线程队列
# 如果不设长度,默认无限长
q = queue.Queue(maxsize=3) # 与上等价
q.put(item, block=True, timeout=None) # 将Item放入队列
# block : Ture 队列已满,full方法会异常
# False 队列未满,立即使用
# timeout表示阻塞队列时长
q.put(2)
q.put(2)
q.put(2)
q.put(2)
q.get(block=True, timeout=None) # 从对列中移除并返回一个数据。当队列为空值,将一直等待。
print(q.get())
print(q.get())
print(q.get())
print(q.get())
# 如果存4个,取3个,什么都不打印,程序会一直处于等待状态,不会停止
# 如果存3个,取4个,打印3个,程序会一直等待放入第四个,不会停止
q.full() # 当队列任务已满时,返回True,否则返回False。
q.empty() # 队列为空返回True,否则返回False。
q.qsize() # 返回队列的大小
三、进程
由于GIL的存在,python中的多线程其实并不是真正的多线程,如果想要充分地使用多核CPU的资源,在python中大部分情况需要使用多进程。Python提供了非常好用的多进程包multiprocessing,只需要定义一个函数,Python会完成其他所有事情。借助这个包,可以轻松完成从单进程到并发执行的转换。
multiprocessing支持子进程、通信和共享数据、执行不同形式的同步,提供了Process、Queue、Pipe、Lock等组件。
multiprocessing包是Python中的多进程管理包。与threading.Thread类似,它可以利用multiprocessing.Process对象来创建一个进程。该进程可以运行在Python程序内部编写的函数。
该Process对象与Thread对象的用法相同,也有start(), run(), join()的方法。
此外multiprocessing包中也有Lock/Event/Semaphore/Condition类 (这些对象可以像多线程那样,通过参数传递给各个进程),用同步进程,其用法与threading包中的同名类一致。所以,multiprocessing的很大一部份与threading使用同一套API,只不过换到了多进程的情境。
在UNIX平台上,当某个进程终结之后,该进程需要被其父进程调用wait,否则进程成为僵尸进程(Zombie)。所以,有必要对每个Process对象调用join()方法 (实际上等同于wait)。对于多线程来说,由于只有一个进程,所以不存在此必要性。
multiprocessing提供了threading包中没有的IPC(比如Pipe和Queue),效率上更高。应优先考虑Pipe和Queue,避免使用Lock/Event/Semaphore/Condition等同步方式 (因为它们占据的不是用户进程的资源)。
多进程应该避免共享资源。在多线程中,我们可以比较容易地共享资源,比如使用全局变量或者传递参数。在多进程情况下,由于每个进程有自己独立的内存空间,以上方法并不合适。此时我们可以通过共享内存和Manager的方法来共享资源。但这样做提高了程序的复杂度,并因为同步的需要而降低了程序的效率。
创建进程的方式一
from multiprocessing import Process
import time
def f(name):
time.sleep(1)
print('hello',name,time.ctime())
if __name__ == '__main__':
list = []
for i in range(3):
p = Process(target=f,args=('NFTS',))
list.append(p)
p.start()
for i in list:
p.join()
print('运行结束')
Process.PID中保存有PID,如果进程还没有start(),则PID为None。
Windows系统下,需要注意的是要想启动一个子进程,必须加上那句if __name__ == "main",进程相关的要写在这句下面,不加会报错。
创建进程的方式二:类式调用
from multiprocessing import Process
import time
class myprocess(Process):
def __init__(self):
super(myprocess,self).__init__()
def run(self):
time.sleep(1)
print('hello',self.name,time.ctime())
if __name__ == '__main__':
list = []
for i in range(3):
p = myprocess()
p.start()
list.append(p)
for i in list:
i.join()
print('运行结束')
查看进程的ID号
from multiprocessing import Process
import os
import time
def info(title):
print(title)
print('module name:', __name__) # __name__ 就等于__main__
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')
time.sleep(100)
p = Process(target=info, args=('bob',))
p.start()
p.join()
进程间通信(用Queue来实现)
from multiprocessing import Process,Queue
def f(q,n):
q.put([n,'ppap'])
if __name__ == '__main__':
q = Queue()
list = []
for i in range(3):
p = Process(target=f,args=(q,i))
list.append(p)
p.start()
print(q.get())
print(q.get())
print(q.get())
for i in list:
i.join()
Pipe管道通信
from multiprocessing import Process,Pipe
def f(conn):
conn.send(123)
conn.close()
if __name__ == '__main__':
prant_conn,son_conn = Pipe()
list = []
p = Process(target=f,args=(son_conn,))
p.start()
print(prant_conn.recv())
p.join()
Manger
from multiprocessing import Process, Manager
def f(d, l,n):
d[n] = '1'
d['2'] = 2
d[0.25] = None
l.append(n)
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,i))
p.start()
p_list.append(p)
for res in p_list:
res.join()
print(d)
print(l)
四、协程
协程又称为微线程、纤程,英文名Coroutine。它是一种用户态的轻量级线程,本质是一个单线程,需要和进程配合才能在CPU上运行。
特点:
1. 必须在只有一个单线程里实现并发
2. 修改共享数据不需加锁
3. 用户程序里自己保存多个控制流的上下文栈
4. 一个协程遇到IO操作自动切换到其它协程
可以用yield和gevent来实现,gevent是一个三方库。
import gevent
def a():
print('程序aaaaaaa开始-----------')
gevent.sleep(1)
print('程序aaaaaaaaaa结束')
def b():
print('程序bbbbbbbbbbbb开始-----')
gevent.sleep(2)
print('程序bbbbbbbbbb结束')
gevent.joinall([
gevent.spawn(a),
gevent.spawn(b)
])
下面例子中,用greenlet来实现,它是一个用C实现的协程模块。
from greenlet import greenlet
def test1():
print(12)
gr2.switch()
print(34)
gr2.switch()
def test2():
print(56)
gr1.switch()
print(78)
gr1 = greenlet(test1)
gr2 = greenlet(test2)
gr1.switch()
下面是一个综合比较案例
from gevent import monkey
import gevent
from urllib.request import urlopen
import time
monkey.patch_all()
def f(url):
print('网址:%s'%url)
resp = urlopen(url)
data = resp.read()
with open('校花网.html','wb') as f:
f.write(data)
print('结束')
start = time.time()
# 普通执行,花费:0.4717395305633545
# l = ['https://www.tupianzj.com/meinv/mm/jurumeinv/','https://mc.163.com/',
# 'http://www.gaosan.com/gaokao/76265.html']
# for url in l:
# f(url)
# 协程执行,花费:0.2602534294128418
gevent.joinall([
gevent.spawn(f,'https://www.tupianzj.com/meinv/mm/jurumeinv/'),
gevent.spawn(f,'https://mc.163.com/'),
gevent.spawn(f,'http://www.gaosan.com/gaokao/76265.html')
])
end = time.time()
print('花费了:%s'%(end-start))
gevent库中的monkey.patch,它是一个监听I/O阻塞的补丁,它可以提高程序的执行效率,当遇到I/O阻塞,会及时切换执行。