目录
GIL全局解释器锁
python在设计之初就考虑要在主循环中,同时只有一个线程在执行。虽然python解释器中可以有多个线程运行,但是在任意时刻只能有一个线程在解释器中运行
访问python解释器由GIL全局解释器锁来控制,正是GIL全局解释器锁,所以程序中才能保证同一时刻只能有一个线程在运行
背景信息:
1. Python代码运行在python解释器上,由python解释器来执行或解释
2. Python解释器的种类:
1. CPython(用的最多) 2. IPython 3. pypy 4. JPython 5. TronPython
3. GIL全局解释器锁是存在于CPython中
4. 结论是:同一时刻只有一个线程在执行。 避免的问题:出现多个线程抢夺资源的情况
如何解决出现多个线程抢夺资源的情况?
python语言在设计时,就添加了一把锁,这把锁就是为了同一时刻只有一个线程在执行,言外之意就是哪个线程想执行,就必须先拿到这把锁(GIL),只有等到这个线程把GIL锁释放掉,别的进程才能拿到,然后具备执行权限
结论:GIL锁就是保证了同一时刻只有一个线程执行,所有的线程必须拿到GIL锁才有执行权限。
关于GIL锁的结论:
1. python有GIL锁的原因?---保证在同一个进程下多个线程实际上同一时刻,只有一个线程执行。
2. 只有python上开进程用的都,其他语言(其它语言没有GIL锁)一般不开多进程,只开多线程。
3. cpython解释器开多线程不能利用多核优势,而是得利用多进程。其它语言没有这个问题
4. 如果不存在GIL锁,一个进程下,开8个线程,他就能充分利用cpu资源,跑满cpu
5. cpython中很多代码都是基于GIL锁写的,电脑中只能使用一核,开启多进程,每个进程下开启一个线程,这样就可以被cpu充分调用执行
6. CPython解释器:io密集型使用多线程,计算密集型使用多线程
原因:1. io密集型,遇到io操作会切换cpu。假如你开了8个线程,每个线程都有io操作----但是io操作不消耗cpu-----一段时间内看上去8个线程都执行了,因此选多线程
2. 计算密集型:消耗cpu,如果开了8个线程,第一个线程会一直占用cpu,而不会调度到其它线程执行,其它7个线程根本没有执行,所以我们开了8个进程,每一个进程有有一个线程,8个进程下的线程会被8个cpu执行,从而效率高,因此选多进程
互斥锁
问题:在多线程的情况下,同时执行一个数据,会发生数据错乱的问题?
互斥锁作用:在多线程的情况下,同时执行一个数据,不会发生数据错乱的问题
互斥锁的使用:互斥锁的使用和进程锁一样,只是一个在进程中,一个在线程中
补充:
面试题:既然有了GIL锁,为什么还要互斥锁?
原因:线程的执行是非常快的,当第一个线程执行完毕后,还未结果返回,第二个线程就已经执行了,这样就会导致数据错乱,而互斥锁就可以很好的解决多线程下操作同一数据发生错乱的问题
线程队列(线程中使用队列)
线程使用队列直接导入queue模块(import queue),然后调用Queue这个类(queue.Queue)
线程队列的三种特征:
1. 先进先出(queue.Queue)
使用方法:调用queue模块中的Queue类,然后入队和出队
2. 后进先出(queue.LifoQueue)
使用方法:调用queue模块中的LifoQueue类,然后入队和出队
3. 优先级队列:(queue.priorityQueue)
使用方法:调用queue模块中的priorityQueue类,然后入队和出队
queue.Queue的缺点:他涉及到多个锁和条件变量,因此可能会影响性能好内存效率
进程池和线程池
池:就是一个容器,可以存放多个元素
进程池:提前定义好一个池子,然后往池子中添加进程,以后,只需往这个池子中丢任务就行,然后由池子中的任意一个进程来执行任务
线程池:提前定义好一个池子,然后往池子中添加线程,以后,只需往这个池子中丢如任务就行,然后有池子中的任意一个线程来执行任务即可
线程池
from concurrent.futures import ThreadPoolExecutor
pool = ThreadPoolExecutor(5)
括号内不传参数的话,会默认开设当前计算机cpu核数的5倍的线程(看源码即懂),假设参数写5,代表池子里面固定有5个线程,不存在不断创建和销毁的过程。
线程池的使用
进程池
from concurrent.futures import ProcessPoolExecutor
pool = ProcessPoolExecutor(3)进程池大体上跟线程池类似,ProcessPoolExecutor(3),括号内不填的话,会默认创建与“cpu核数”相同数量的进程(看源码即懂),同样的,进程池中的进程是固定工,不会重复创建和销毁。
进程池和线程池在使用形式上是一样的,唯一不同的是:在Windows环境下,进程池要放在main方法里面,否则会报错
进程池的使用
协程理论
进程:资源分配的基本单位
线程:执行的最小单位
协程:单线程下的并发,最节省资源
并发的本质:切换+保存状态
**以前的并发其实线程或进程的切换**
如何切换:
切换是由程序员自己来切,不是操作系统切。本质上就是最大限度的利用cpu资源
如何使用:导入gevent模块,gevent模块不是内置模块因此需要我们下载。
如何下载:在pycharm的终端(Terminal)中输入pip install gevent 然后点回车
协程的优缺点以及特点
对比操作系统控制线程的切换,用户在单线程内控制协程的切换。
优点如下:
- 协程的切换开销更小,属于程序级别的切换,操作系统完全感知不到,因而更加轻量级
- 单线程内就可以实现并发的效果,最大限度地利用cpu
缺点如下:
- 协程的本质是单线程下,无法利用多核,可以是一个程序开启多个进程,每个进程内开启多个线程,每个线程内开启协程
- 协程指的是单个线程,因而一旦协程出现阻塞,将会阻塞整个线程
总结协程特点:
- 必须在只有一个单线程里实现并发
- 修改共享数据不需加锁
- 用户程序里自己保存多个控制流的上下文栈
- 附加:一个协程遇到IO操作自动切换到其它协程(如何实现检测IO,yield、greenlet都无法实现,就用到了gevent模块(select机制))
协程实现高并发
服务端代码
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(5000):
t = Thread(target=socket_client)
t.start()