GIL全局解释器锁

一 : 死锁现象与递归锁

死锁现象: 根本就走不了,因为堵住啦

from threading import Thread
from threading import Lock
import time
lock_A = Lock()
lock_B = Lock()

class MyThread(Thread):
    def run(self):
        self.f1()
        self.f2()
    def f1(self):
        lock_A.acquire()
        print(f"{self.name}拿到了 A 锁")

        lock_B.acquire()
        print(f"{self.name}拿到了 B锁")

        lock_B.release()
        lock_A.release()

    def f2(self):
        lock_B.acquire()
        print(f"{self.name}拿到了 B 锁")

        lock_A.acquire()
        print(f"{self.name}拿到了A锁")

        time.sleep(0.1)

        lock_A.release()
        lock_B.release()


if __name__ == '__main__':
    for i in range(3):
        t = MyThread()
        t.start()

过程分析 : t1 先抢到了 A锁,此时t2 t3 也想抢 A锁,但是没有办法,只能等着.等待 t1 释放 A锁,t1 又抢了 B 锁,此时 t1 有 A B 两把锁,没有释放,t2 t3 继续等待

当 t1 依次释放 BA 锁时,线程 t2 t3 争抢 A锁,t1 争抢 B锁,按照结果分析,t2 抢到了 A 锁,t1 拿到了 B 锁,但是接下来,t1 睡了0.1秒,t1 拥有 B 锁,想要 A 锁,但是 t2 拥有 A 锁,想要 B 锁就矛盾了

这样就造成了死锁的现象

于是引出了下面的递归锁,解决这一个问题

递归锁

应用 : 可以解决死锁问题,业务需要锁时,先要考虑递归锁

from threading import Thread
from threading import RLock
import time
lock_A = lock_B = RLock()

class MyThread(Thread):
    def run(self):
        self.f1()
        self.f2()
    def f1(self):
        lock_A.acquire()
        print(f"{self.name}拿到了 A 锁")

        lock_B.acquire()
        print(f"{self.name}拿到了 B锁")

        lock_B.release()
        lock_A.release()

    def f2(self):
        lock_B.acquire()
        print(f"{self.name}拿到了 B 锁")

        lock_A.acquire()
        print(f"{self.name}拿到了A锁")

        time.sleep(0.1)

        lock_A.release()
        lock_B.release()


if __name__ == '__main__':
    for i in range(3):
        t = MyThread()
        t.start()

**原因分析 : 递归锁有一个计数功能,原数字为 0,上一次锁,计数加一,释放一次锁,计数-1,只要递归锁上面的数字不为 0,其他线程就不能抢锁 **

本质的原因还是在于,只能有一个执行一个线程,只要存在,其他的进不来,坤坤说的

二 : 信号量(不重要)

也是一种锁,控制并发数量

就像是上厕所,有一个坑,就会被下一个占用

from threading import Thread ,Semaphore,current_thread
import time
import random
sem = Semaphore(5)
def task():
    sem.acquire()
    print(f"{current_thread().name}厕所 ing")
    time.sleep(random.randint(1,3))
    sem.release()
if __name__ == '__main__':
    for i in range(20):
        t = Thread(target = task)
        t.start()

三 : GIL 全局解释器锁(重要,好好的看一下)

好多自称大神的说,GIL 锁就是 Python 的知名缺陷,Python 不能多核,并发不行等等其他问题

一个线程的执行流程

image-20190823163631211

这里我口述一下:当你打开一个 py 文件,我们首先在cpython 中打开一个进程空间,进程空间包括 Python 解释器和文件,Python 解释器又包含虚拟机和编译器.

编译器:将 python 语言转化为 C语言能识别的字节码

虚拟机:字节码转成机器码,交由 CPU 去运行

理论上来说:单个进程的多线程可以利用多核(CPU)

​ 但是开发 Cpython 解释器,给解释器加了一把锁

CPython 规定:同一个时刻,只能允许一个线程进解释器

image-20190823164742834

为什么加锁??

  1. 当时都是单核时代,而且 CPU 的价格非常贵
  2. 如果不加全局解释器锁,开发 Cpython 解释器的程序员就会在源码内部各种主动加锁,解锁,非常麻烦,各种死锁现象等等,他为了省事,直接进入解释器时给线程加了一个锁.

优点:保证了 Cpython 解释器的数据资源的安全

缺点:单个进程的多线程不能利用多核

Jython 没有 GIL 锁

pypy 也没有 GIL 锁

提问:现在多核时代,我将 cpython 的 GIL 锁去掉行吗?

不可以,因为 cpython 解释器所有的业务逻辑都是围绕单个线程去实现的,去掉这个 GIL锁,几乎不可能

image-20190823165445155

单个进程的多线程可以并发,但是不能利用多核,不能并发

多个进程可以并发,并行

**IO 密集型 : **单个进程的多线程合适,并发执行

image-20190823170325224

**计算密集型 : **多进程的并行

image-20190823170445041

四 : GIL 与 lock 锁的区别

相同点:

都是同种锁,互斥锁

**不同点 : **

  1. GIL 全局解释器锁,保护解释器内部的资源数据安全
  2. GIL 锁, 上锁 释放无需动手
  3. 自己代码中定义的互斥锁保护资源数据的安全
  4. 自己定义的互斥锁必须自己手动上锁,释放锁

image-20190823171410146

五 : 验证计算密集型 IO 密集型的效率

IO 密集型

image-20190823171531413

计算密集型

image-20190823171559290

代码方的实现 :

计算密集型: 单个进程的多线程并发 VS 多个进程的并发执行

from threading import Thread
from multiprocessing import Process
import time
import flask

import random
def task():
    count = 1
    for i in range(1,100000):
        count += 1
if __name__ == '__main__':
    ###多进程的并发  并行
    start_time = time.time()
    # l1 = []
    # for i in range(4):
    #     p = Process(target = task)
    #     l1.append(p)
    #     p.start()
    # for i in l1:
    #     i.join()
    # print(f"执行效率:{time.time() - start_time}")  #  执行效率:0.023988962173461914

    # 多线程的并发
    l1 = []
    for i in range(4):
        p = Thread (target = task)
        l1.append(p)
        p.start()
    for p in l1:
        p.join()
    print(f"执行效率:{time.time() - start_time}")   #  执行效率:0.026134014129638672

由上方代码可知,计算密集型采用多进程的并发效率更快

IO 密集型 : 多进程的并发,并行 VS 多线程的并发

多进程的并发执行

def task():
    count = 0
    time.sleep(random.randint(1,3))
    count += 1
if __name__ == '__main__':
    start_time = time.time()
    l1 = []
    for i in range(50):
        p = Process(target = task,)
        l1.append(p)
        p.start()
    for p in l1:
        p.join()
    print(f"执行效率:{time.time() - start_time}")   #执行效率:3.0838990211486816

多线程的并发执行

def task():
    count = 0
    time.sleep(random.randint(1,3))
    count += 1

if __name__ == '__main__':
    start_time = time.time()
    l1 = []
    for i in range(50):
        p = Thread(target = task,)
        l1.append(p)
        p.start()
    for p in l1:
        p.join()
    print(f"执行效率:{time.time() - start_time}")   #执行效率:3.009322166442871

两个对比结果可以得出 对于 IO 密集型来说 单个进程的多线程的效率更好一些

六 : 多进程实现 socket 通信

无论是多线程还是多进程,如果按照上面的写法,来一个客户端请求,我就开一个线程,来一个请求开一个线程

应该是这样 : 你的计算机允许范围内,开启线程进程数量越多越好

服务端

import socket
from threading import Thread
def communicate(conn,addr):
    while 1:
        try:
            from_client_data = conn.recv(1024)
            print(f"来自{addr[1]}的消息:{from_client_data.decode('utf-8')}")
            to_client_data = input(">>>>")
            conn.send(to_client_data.encode('utf-8'))
        except Exception :
            break
        conn.close()
def _accept():
    server = socket.socket()
    server.bind(('127.0.0.1',8848))
    server.listen(5)
    while 1:
        conn,addr = server.accept()
        t = Thread(target = communicate,args = (conn,addr))
        t.start()

if __name__ == '__main__':
    _accept()

客户端

import socket
client = socket.socket()
client.connect(('127.0.0.1',8848))
while 1:
    try:
        to_server_data = input(">>>>").strip()
        client.send(to_server_data.encode('utf-8'))
        from_server_data = client.recv(1024)
        print(f"来自服务端的消息:{from_server_data.decode('utf-8')}")
    except Exception :
        break
client.close()

七 : 进程池,线程池

线程池 : 一个容器,这个容器限制住你开启线程的数量,比如 4 个,第一次肯定只能并发的处理 4 个任务,只要有任务完成,线程就会接下一个任务

又一个以时间换空间的例子

进程池:放置进程的一个容器

from concurrent.futures import ProcessPoolExecutor,ThreadPoolExecutor
import os
import time
import random
def task(n):
    print(f"{os.getpid()}接客")
    time.sleep(random.randint(1,3))
if __name__ == '__main__':
    p = ProcessPoolExecutor()   默认不写,进程池里面的进程数与cpu个数相等
    for i in range(20):
        p.submit(task,i)

线程池 : 当是线程的一个容器,用来限制系统中线程的数量

from concurrent.futures import ProcessPoolExecutor,ThreadPoolExecutor
import os
import time
import random
def task(n):
    print(f"{os.getpid()}接客")  #  线程来说  pid 都是一样的
    time.sleep(random.randint(1,3))
if __name__ == '__main__':
    t = ThreadPoolExecutor()  # 默认不写, cpu个数*5 线程数
    for i in range(20):
        t.submit(task,i)

线程即使开销小,电脑也不能无限的开线程,我们应该对线程和进程的数量做限制,在计算机能承受最大的情况下,尽可能的多开线程和进程

今日总结

递归锁:RLock 同一把锁,引用一次计数加一,释放一次计数减一,只要计数不为 0 ,其他线程进程就抢不到,他能解决死锁问题

信号量:同一时刻,可以设置抢锁的线程,或者进程数量

GIL 锁:全局解释器锁,进入解释器的时候,同一时刻只能有一个线程CPython 解释器录入的

​ 优点: 保证了解释器的资源数据安全

​ 缺点:单进程的多线程不能利用多核优势

GIL 锁与自定义锁的异同:

GIL 锁保证了解释器数据的安全

自定义锁保证了进程线程中的数据安全线程池,进程池:控制开启线程或者 进程的数量

IO 密集型:利用单进程的多线程并发

计算密集型:多进程的并发或者并行

posted @ 2019-08-26 18:20  挂机兄  阅读(114)  评论(0编辑  收藏  举报