GIL全局解释器锁
python在设计之初就考虑到了要在主循环之中,同时只有一个线程执行。虽然在python解释器中可以运行多个“线程”,但是在任意时刻其实只有一个线程在运行。
在python解释器的访问由全局解释器(GIL)来控制,正式这个锁来保证同一时刻来运行一个线程
背景信息
1.python代码的运行在python解释器上运行,由解释器来执行或者解释。
2.python解释器有好几种版本:我们最常用的是Cpython,还有pypy,Jython,Ipython,Ironpython
3. 市面上使用最多的就是Cpython解释器
4. GIL全局解释锁是在Cpython中的
5. 同一时刻只有一个线程在执行为了避免出现多个线程抢占资源的情况
如何避免:
那就是在Python这门语言设计之处,就直接在解释器上添加了一把锁,这把锁就是为了让统一时刻只有一个线程在执行,言外之意就是哪个线程想执行,就必须先拿到这把锁(GIL), 只有等到这个线程把GIL锁释放掉,别的线程才能拿到,然后具备了执行权限.
得出结论:GIL锁就是保证在统一时刻只有一个线程执行,所有的线程必须拿到GIL锁才有执行权限
需要我们记忆的有:
1.python有GIL锁的原因,同一个进程下的多个线程实际上同一时刻,只有一个线程在执行
2.只有在python上开进程用的多,其他语言一般不开多进程,只开多线程就够了
解释:1.因为GIL锁的原因,多个进程下的多个线程是只有一个线程在执行,所以在python中就算开启了多个线程,执行的还是一个线程,而别的语言没有GIL锁。
2.在其他语言中开启线程的消耗确是很小
3. cpython解释器开多线程不能利用多核优势,只有开多进程才能利用多核优势,其他语言不存在这个问题
解释:正确,因为在Cpython中多进程可以使用多核的优势,而多线程实际上还是同一时间执行一个线程
4. 8核cpu电脑,充分利用起我这个8核,至少起8个线程,8条线程全是计算--->计算机cpu使用率是100%。
解释:8核cpu电脑,充分利用8核的话,必须是启用八个进程,八个线程不是同一进程下的,所以要想充分利用的话,必须开八个进程。
5. 如果不存在GIL锁,一个进程下,开启8个线程,它就能够充分利用cpu资源,跑满cpu
解释:正确,当GIL锁不存在下,在python中开启八个线程,就爱可以充分利用cpu性能。
6. cpython解释器中好多代码,模块都是基于GIL锁机制写起来的,改不了了---》我们不能有8个核,但我现在只能用1核,----》开启多进程---》每个进程下开启的线程,可以被多个cpu调度执行
.解释:正确,我们目前使用的内置模块,内置函数等其他都是基于GIL锁机制来写的,不可以修改,除非官方宣布取消GIL锁机制。
7. cpython解释器:io密集型使用多线程,计算密集型使用多进程
io密集型,遇到io操作会切换cpu,假设你开了8个线程,8个线程都有io操作---》io操作不消耗cpu---》一段时间内看上去,其实8个线程都执行了, 选多线程好一些
计算密集型,消耗cpu,如果开了8个线程,第一个线程会一直占着cpu,而不会调度到其他线程执行,其他7个线程根本没执行,所以我们开8个进程,每个进程有一个线程,8个进程下的线程会被8个cpu执行,从而效率高
计算密集型使用多进程会更好,在其他语言中,都是使用多线程。
互斥锁
互斥锁:解决多线程下操作同一个数据,发生错乱的问题。
from threading import Thread
n = 10
def task():
global n
n -= 1
if __name__ == '__main__':
ll = []
for i in range(10):
t = Thread(target=task)
t.start()
ll.append(t)
for j in ll:
j.join()
print('主程序:', n) # 打印结果:主程序: 0
当加入一个睡眠时间:
from threading import Thread
import time
n = 10
def task():
global n
tm = n
time.sleep(0.1)
n = tm - 1
if __name__ == '__main__':
ll = []
for i in range(10):
t = Thread(target=task)
t.start()
ll.append(t)
for j in ll:
j.join()
print('主程序:', n) # 打印结果:主程序: 9
结果就变了,是因为在多线程的情况下,同时执行一个数据,会发生数据错乱的问题。
解决:加入锁
from threading import Thread
import time
from threading import Lock
n = 10
"""在多线程的情况下,同时执行一个数据,会发生数据错乱的问题"""
def task(lock):
lock.acquire()
global n
tm = n
time.sleep(0.1)
n = tm - 1
lock.release()
if __name__ == '__main__':
ll = []
lock = Lock()
for i in range(10):
t = Thread(target=task, args=(lock,))
t.start()
ll.append(t)
for j in ll:
j.join()
print('主程序:', n) # 打印结果:主程序: 0
在加入锁之后,每次执行线程时,会等待当前线程,当运行之后把结果赋予给n之后在解除锁后,下回进入下个线程的执行,这样线程就不会出现错乱了。
问:在多线程下为什么有了GIL锁之后还要互斥锁?
例:当前启用了两个线程来执行a = a+1,开始的a值为0
答:执行第一个线程,得到a = 0,开始执行a+1,a开始的值为0,执行结束时,a值为1,当第一个线程执行结果为1还没赋予给a,这时,执行的的第二个线程已经开始执行了,拿到的结果是0,然后继续执行最终得到的结果就是 a=a+1为1
所以还是得需要互斥锁来防止数据的错乱问题。
线程队列
问:为什么线程中还使用队列:
答:因为队列是管道+锁,所以使用队列是为了保护数据安全。
线程队列:先进先出,后进先出,优先级
先进先出
关键词:queue.Queue
"""线程队列"""
from multiprocessing import Queue
queue.Queue()
import 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""""
后进先出
关键词:queue.LifoQueue
"""线程队列"""
from multiprocessing import Queue
import queue
queue.Queue()
q = queue.LifoQueue() # 后进先出
q.put('second')
q.put('third')
print(q.get())
print(q.get())
print(q.get())
"""执行结果,后进先出:third
second
first"""
优先级
关键字:
queue.PriorityQueue
from multiprocessing import Queue
queue.Queue()
"""put 进入一个元组,元组的第一个元素是优先级,通常是数字,但是也可以与非数字直接做比较数字越小,优先级越高"""
q = queue.PriorityQueue() #
q.put((10, 'first'))
q.put((20, 'second'))
q.put((30, 'third'))
print(q.get())
print(q.get())
print(q.get())
"""执行结果,数字越小先出队列,优先级更好,出队列:(10, 'first')
(20, 'second')
(30, 'third')"""
进程池和线程池的使用
什么是池子:它是容器类型,用来存放多个元素
进程池:提前定义一个池,之后往池子里添加进程,之后只需要往池子里丢任务就可以了,池子里其中一个进程来执行这个任务
线程池:提前定义一个池,之后往池子里添加线程,之后只需要往池子里丢任务就可以了,池子里其中一个线程来执行这个任务
开启进程池:
from concurrent.futures import TheeadpoolExecutor,ProcessPoolExecutor
定义进程池
pool = ProcessExecutor()
def task(n, m):
return n + m
def task1():
return {'username': 'kk', 'password': '18'}
"""开进程池"""
from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor
def hd(res):
print(res) # 回调函数
print(res.result())
def hd1(res):
print(res)
print(res.result())
print(res.result().get('username'))
if __name__ == '__main__':
"""定义进程池,里面有三个进程"""
pool = ProcessPoolExecutor(3)
"""往池子里丢任务"""
pool.submit(task, m=1, n=2).add_done_callback(hd)
pool.submit(task1).add_done_callback(hd1)
pool.shutdown() # 代表 join+close
print(123)
"""输出结果:<Future at 0x28a5efe24f0 state=finished returned int>
3
<Future at 0x28a5effdc40 state=finished returned dict>
{'username': 'kk', 'password': '18'}
kk
123
<Future at 0x28a5efe24f0 state=finished returned int>
3
<Future at 0x28a5effdc40 state=finished returned dict>
{'username': 'kk', 'password': '18'}
kk
123
"""
协程
进程:资源分配
线程:执行的最小单位
协程:单线程下的并发
并发:切换+保存状态
在之前的并发切换,其实就是进程与线程直接的切换
最节省资源的是协程,再者是线程,最后是进程
问 如何判断是否有I/O?
答:我们程序员所做的协程不是操作系统l来操作的,而是我们程序员所操作的。本质上是为了最大限度的利用cpu。
协程实现高并发
关键字:gevent
客户端:
import socket
from threading import current_thread,Thread
def socket_client():
cil = socket.socket()
cil.connect(('127.0.0.1',2000))
while True:
k = '%s hello' % current_thread().getName()
cil.send(k.encode('utf-8'))
data = cil.recv(1024)
print(data)
for i in range(3):
t = Thread(target=socket_client,)
t.start()
服务端:
from gevent import monkey
monkey.patch_all()
import gevent
from socket import socket
from threading import Thread
"""定义一个线程"""
def task(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 task1(ip, port):
server = socket()
server.bind((ip, port))
server.listen(5)
while True:
conn, addr = server.accept()
gevent.spawn(task, conn)
if __name__ == '__main__':
g1 = gevent.spawn(task1, '127.0.0.1', 2000)
g1.join()