目录
一、进程和线程的比较
1. 进程的开销比线程大很多
2 . 进程之间的数据是隔离的,但是,线程之间的数据不隔离
3 .多个进程之间的线程数据不共享----->进程下的线程通信(IPC)------->队列
二、GIL全局解释器锁
Python代码的执行由Python解释器来控制。Python在设计之初就考虑到要在主循环中,同时只有一个线程在执行。虽然 Python 解释器中可以“运行”多个线程,但在任意时刻只有一个线程在解释器中运行。
对Python解释器的访问由全局解释器锁(GIL)来控制,正是这个锁能保证同一时刻只有一个线
程在运行。
2.1 背景信息
1. Python代码运行在解释器上,由解释器来执行或者解释
2. Python解释器的种类:
1、CPython 2、IPython 3、PyPy 4、Jython 5、IronPython
3. 当前市场使用的最多(95%)的解释器就是CPython解释器
4. GIL全局解释器锁是存在于CPython中
5. 结论是同一时刻只有一个线程在执行? 想避免的问题是,出现多个线程抢夺资源的情况
比如:现在起一个线程,来回收垃圾数据,回收a=1这个变量,另外一个线程也要使用这个变量a,当垃圾回收线程还没没有把变量a回收完毕,另一个线程就来抢夺这个变量a使用。
怎么避免的这个问题,那就是在Python这门语言设计之处,就直接在解释器上添加了一把锁,这把锁就是为了让统一时刻只有一个线程在执行,言外之意就是哪个线程想执行,就必须先拿到这把锁(GIL), 只有等到这个线程把GIL锁释放掉,别的线程才能拿到,然后具备了执行权限。
结论:GIL锁就是保证在统一时刻只有一个线程执行,所有的线程必须拿到GIL锁才有执行权限
2.2 总结
- Python有GIL锁的原因,同一个进程下多个线程实际上同一时刻,只有一个线程在执行
- 只有在Python上开进程用的多,其他语言一般不开多进程,只开多线程就够了
- CPython解释器开多线程不能利用多核优势,只有开多进程才能利用多核优势,其他语言不存在这个问题
- 8核cpu电脑,充分利用起我这个8核,至少起8个线程,8条线程全是计算---->计算机CPU使用率是100%,
- 如果不存在GIL锁,一个进程下,开启8个线程,它就能够充分利用CPU资源,跑满CPU
- CPython解释器中好多代码,模块都是基于GIL锁机制写起来的,改不了了--->我们不能有8个核,但我现在只能用1核,---->开启多进程 ---> 每个进程下开启的线程,可以被多个CPU调度执行
- CPython解释器:IO密集型使用多线程,计算密集型使用多进程
IO密集型,遇到IO操作会切换CPU,假设你开了8个线程,8个线程都有IO操作----->IO操作不消耗CPU---->一段时间内看上去,其实8个线程都执行了, 选多线程好一些
计算密集型,消耗CPU,如果开了8个线程,第一个线程会一直占着CPU,而不会调度到其他线程执行,其他7个线程根本没执行,所以我们开8个进程,每个进程有一个线程,8个进程下的线程会被8个CPU执行,从而效率高
补充:计算密集型选多进程好一些,在其他语言中,都是选择多线程,而不选择多进程。
三、互斥锁
在多线程的情况下,同时执行一个数据,会发生数据错乱的问题
n = 20
from threading import Lock
import time
def task(lock):
lock.acquire()
global n
temp = n
time.sleep(0.5)
n = temp - 1
lock.release()
"""拿时间换空间,空间换时间 时间复杂度"""
from threading import Thread
if __name__ == '__main__':
tt = []
lock = Lock()
for i in range(10):
t = Thread(target=task, args=(lock,))
t.start()
tt.append(t)
for j in tt:
j.join()
print("主", n) # 主 10
补充:
面试题:既然有了GIL锁,为什么还要互斥锁? (多线程下)
比如:我起了2个线程,来执行a=a+1,a一开始是0
1. 第一个线程来了,拿到a=0,开始执行a=a+1,这个时候结果a就是1了
2. 第一个线程得到的结果1还没有赋值回去给a,这个时候,第二个线程来了,拿到的a是0,继续执行
a=a+1结果还是1
3. 加了互斥锁,就能够解决多线程下操作同一个数据,发生错乱的问题
四、线程队列
4.1 为什么线程中还有使用队列?
同一个进程下多个线程数据是共享的
为什么先同一个进程下还会去使用队列呢
因为队列是管道 + 锁
所以用队列还是为了保证数据的安全
4.2 先进先出
'''
queue.Queue 的缺点是它的实现涉及到多个锁和条件变量,
因此可能会影响性能和内存效率。
'''
import queue
queue.Queue()
q = queue.Queue() # 无限大
q.put('first')
q.put('second')
q.put('third')
print(q.get())
print(q.get())
print(q.get())
'''
结果(先进先出):
first
second
third
'''
4.3 后进先出
import queue
'Lifo:last in first out'
q = queue.LifoQueue()
q.put('first')
q.put('second')
q.put('third')
print(q.get())
print(q.get())
print(q.get())
'''
结果(后进先出):
third
second
first
'''
4.4 优先级队列
import queue
q = queue.PriorityQueue()
'put进入一个元组,元组的第一个元素是优先级(通常是数字,也可以是非数字之间的比较),
数字越小优先级越高'
q.put((20, 'a'))
q.put((10, 'b'))
q.put((30, 'c'))
print(q.get())
print(q.get())
print(q.get())
'''
结果(数字越小优先级越高,优先级高的优先出队):
(10, 'b')
(20, 'a')
(30, 'c')
'''
五、进程池和线程池的使用
池:池子、容器类型,可以盛放多个元素
进程池:提前定义好一个池子,然后,往这个池子里面添加进程,以后,只需要往这个进程池里面丢任务就行了,然后,由这个进程池里面的任意一个进程来执行任务
线程池:提前定义好一个池子,然后,往这个池子里面添加线程,以后,只需要往这个线程池里面丢任务就行了,然后,由这个线程池里面的任意一个线程来执行任务
5.1 基本方法
submit(fn, *args, **kwargs):异步提交任务
map(func, *iterables, timeout=None, chunksize=1):取代for循环submit的操作
shutdown(wait=True):相当于进程池的pool.close()+pool.join()操作
- wait=True,等待池内所有任务执行完毕回收完资源后才继续
- wait=False,立即返回,并不会等待池内的任务执行完毕
- 但不管wait参数为何值,整个程序都会等到所有任务执行完毕
- submit和map必须在shutdown之前
result(timeout=None):取得结果
add_done_callback(fn):回调函数
done():判断某一个线程是否完成
cancle():取消某个任务
5.2 使用
def task(n, m):
return n + m
def task1():
return {'username': 'kevin', 'password': 123}
"""开进程池"""
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
def callback(res):
print(res) # Future at 0x1ed5a5e5610 state=finished returned int>
print(res.result()) # 3
def callback1(res):
print(res) # <Future at 0x2ad4ae50bb0 state=finished returned dict>
print(res.result()) # {'username': 'kevin', 'password': 123}
print(res.result().get('username')) # kevin
if __name__ == '__main__':
pool = ThreadPoolExecutor(3) # 定义一个进程池,里面有3个进程
# 2. 往池子里面丢任务
pool.submit(task, m=1, n=2).add_done_callback(callback)
pool.submit(task1).add_done_callback(callback1)
pool.shutdown() # join + close
print(123) # 123
5.3 多线程爬取网页
import requests
def get_page(url):
res = requests.get(url)
name = url.rsplit('/')[-1]+'.html'
return {'name':name,'text':res.content}
def call_back(fut):
print(fut.result()['name'])
with open(fut.result()['name'],'wb') as f:
f.write(fut.result()['text'])
if __name__ == '__main__':
pool = ThreadPoolExecutor(2)
urls = ['http://www.baidu.com','http://www.cnblogs.com',
'http://www.taobao.com']
for url in urls:
pool.submit(get_page,url).add_done_callback(call_back)
六、协程
6.1 协程基础
之前我们学习了线程、进程的概念,了解了在操作系统中进程是资源分配的基本单位,线程是CPU调度的最小单位。按道理来说我们已经算是把CPU的利用率提高很多了。但是我们知道无论是创建多进程还是创建多线程来解决问题,都要消耗一定的时间来创建进程、创建线程、以及管理他们之间的切换。
随着我们对于效率的追求不断提高,基于单线程来实现并发又成为一个新的课题,即只用一个主线程(很明显可利用的CPU只有一个)情况下实现并发。这样就可以节省创建线进程所消耗的时间。
为此我们需要先回顾下并发的本质:切换+保存状态。以前的并发的切换其实是进程或者线程在切换。CPU正在运行一个任务,会在两种情况下切走去执行其他的任务(切换由操作系统强制控制),一种情况是该任务发生了阻塞,另外一种情况是该任务计算的时间过长。
协程:是单线程下的并发,又称微线程,纤程。英文名Coroutine。一句话说明什么是协程:协程是一种用户态的轻量级线程,即协程是由我们自己控制调度的,在操作系统中实际不存在的。
对比操作系统控制线程的切换,用户在单线程内控制协程的切换。
优点如下:
- 协程是最节省资源的,进程是最消耗资源的,其次是线程
- 单线程内就可以实现并发的效果,最大限度地利用CPU
缺点如下:
- 协程的本质是单线程下,无法利用多核,可以是一个程序开启多个进程,每个进程内开启多个线程,每个线程内开启协程
- 协程指的是单个线程,因而一旦协程出现阻塞,将会阻塞整个线程
6.2 协程之gevent模块
6.2.1 猴子补丁的功能(一切皆对象)
拥有在模块运行时替换的功能,例如: 一个函数对象赋值给另外一个函数对象(把函数原本的执行的功能给替换了)。
class Monkey():
def play(self):
print('猴子在玩')
class Dog():
def play(self):
print('狗子在玩')
m=Monkey()
m.play()
m.play = Dog().play
m.play()
6.2.2 monkey patch的应用场景
这里有一个比较实用的例子,很多用到import json, 后来发现ujson性能更高,如果觉得把每个文件的import json改成import ujson as json成本较高, 或者说想测试一下ujson替换是否符合预期, 只需要在入口加上:
import json
import ujson
def monkey_patch_json():
json.__name__ = 'ujson'
json.dumps = ujson.dumps
json.loads = ujson.loads
monkey_patch_json()
aa = json.dumps({'name':'lqz','age':19})
print(aa)
6.2.3 示例1(遇到io自动切)
import gevent
import time
def eat(name):
print('%s eat 1' % name)
gevent.sleep(2)
print('%s eat 2' % name)
def play(name):
print('%s play 1' % name)
gevent.sleep(1)
print('%s play 2' % name)
# eat('kevin')
# play('jerry')
start_time = time.time()
g1 = gevent.spawn(eat, 'lqz')
g2 = gevent.spawn(play, name='lqz')
g1.join()
g2.join() # 相当于执行这些任务完毕
# 或者gevent.joinall([g1,g2])
print('主', time.time() - start_time)
结果如下:
'''
lqz eat 1
lqz play 1
lqz play 2
lqz eat 2
主 2.0305111408233643
'''
6.2.4 示例2
'''
上例gevent.sleep(2)模拟的是gevent可以识别的io阻塞,
而time.sleep(2)或其他的阻塞,gevent是不能直接识别的需要用下面一行代码,打补丁,就可以识别了
from gevent import monkey;monkey.patch_all()必须放到被打补丁者的前面,如time,socket模块之前
或者我们干脆记忆成:要用gevent,需要将from gevent import monkey;monkey.patch_all()放到文件的开头
'''
from gevent import monkey; monkey.patch_all()
import gevent
import time
def eat():
print('eat food 1')
time.sleep(2)
print('eat food 2')
def play():
print('play 1')
time.sleep(1)
print('play 2')
start_time = time.time()
g1 = gevent.spawn(eat)
g2 = gevent.spawn(play)
g1.join()
g2.join()
# gevent.joinall([g1,g2])
print('主', time.time() - start_time)
结果如下:
'''
eat food 1
play 1
play 2
eat food 2
主 2.0193047523498535
'''
6.3 协程实现高并发
服务端
from gevent import monkey;
monkey.patch_all()
import gevent
from socket import socket
# from multiprocessing import Process
from threading import Thread
def talk(conn):
while True:
try:
data = conn.recv(1024)
if len(data) == 0: break
print(data)
conn.send(data.upper())
except Exception as e:
print(e)
conn.close()
def server(ip, port):
server = socket()
server.bind((ip, port))
server.listen(5)
while True:
conn, addr = server.accept()
# t=Process(target=talk,args=(conn,))
# t=Thread(target=talk,args=(conn,))
# t.start()
gevent.spawn(talk, conn)
if __name__ == '__main__':
g1 = gevent.spawn(server, '127.0.0.1', 8080)
g1.join()
客户端
import socket
from threading import current_thread, Thread
def socket_client():
cli = socket.socket()
cli.connect(('127.0.0.1', 8080))
while True:
ss = '%s say hello' % current_thread().getName()
cli.send(ss.encode('utf-8'))
data = cli.recv(1024)
print(data)
for i in range(50000):
t = Thread(target=socket_client)
t.start()