GIL全局解释器锁、死锁、信号量、event以及线程队列 目  录 TCP实现并发的两种方法 GIL全局解释器锁 GIL与普通的互斥锁 死锁现象、递归锁 信号量 event事件 线程队列 补充知识: 一、TCP实现并发的两种方法 二、GIL全局解释器锁 面试重点 三、GIL与普通的互斥锁 四、死锁现象和递归锁 五、信号量 六、event事件 七、线程队列

补充知识:

什么是服务端或服务端应满足什么要求?

"""
服务端
    1.要有固定的IP和PORT
    2.24小时不间断提供服务
    3.能够支持并发
"""

一、TCP实现并发的两种方法

TCP实现并发有两种方法:

1、利用socketserver模块

2、利用多线程方法

代码演示如下:

方法1:socketserver模块

import socketserver


class MyServer(socketserver.BaseRequestHandler):
    def handle(self):
        # print('来啦 老弟')
        while True:
            data = self.request.recv(1024)
            print(self.client_address)  # 客户端地址
            print(data.decode('utf-8'))
            self.request.send(data.upper())


if __name__ == '__main__':
    """只要有客户端连接  会自动交给自定义类中的handle方法去处理"""
    server = socketserver.ThreadingTCPServer(('127.0.0.1',8080),MyServer)  # 创建一个基于TCP的对象
    server.serve_forever()  # 启动该服务对象
服务端
import socket

client = socket.socket()
client.connect(('127.0.0.1',8080))

while True:
    client.send(b'hello')
    data = client.recv(1024)
    print(data.decode('utf-8')
客户端

方法2:多线程

import socket
from threading import Thread

"""
服务端
    1.要有固定的IP和PORT
    2.24小时不间断提供服务
    3.能够支持并发
"""

server = socket.socket()
server.bind(('127.0.0.1',8080))
server.listen(5)


def talk(conn):
    while True:
        try:
            data = conn.recv(1024)
            if len(data) == 0:break
            print(data.decode('utf-8'))
            conn.send(data.upper())
        except ConnectionResetError as e:
            print(e)
            break
    conn.close()

while True:
    conn, addr = server.accept()  # 监听 等待客户端的连接  阻塞态
    print(addr)
    t = Thread(target=talk,args=(conn,))
    t.start()
服务端
import socket


client = socket.socket()
client.connect(('127.0.0.1',8080))

while True:
    client.send(b'hello')
    data = client.recv(1024)
    print(data.decode('utf-8'))
客户端

二、GIL全局解释器锁

什么是GIL?

GIL的本质也是一把互斥锁:它将并发转成串行虽然降低了运行效率但是保证了数据的安全性。

GIL的作用是什么?

它可以阻止同一进程内多个线程的同时执行(同一进程内的多线程可以实现并发但是不能并行)

注意:

python解释器有很多种,最常见的就是Cpython解释器。

GIL存在的原因就是因为CPython解释器的内存管理不是线程安全的!

由以上理论提出两个问题?

同一进程内的多线程只能实现并发不能实现并行,那么“python的多线程没法利用多核优势,是不是就没什么用了

答:

'''
研究python的多线程是否有用需要分情况讨论
四个任务 计算密集型的  10s
单核情况下
    开线程更省资源
多核情况下
    开进程 10s
    开线程 40s

四个任务 IO密集型的  
单核情况下
    开线程更节省资源
多核情况下
    开线程更节省资源

'''

代码示例:

# 计算密集型
from multiprocessing import Process
from threading import Thread
import os,time
def work():
    res=0
    for i in range(100000000):
        res*=i


if __name__ == '__main__':
    l=[]
    print(os.cpu_count())  # 本机为6核
    start=time.time()
    for i in range(6):
        # p=Process(target=work) #耗时  4.732933044433594
        p=Thread(target=work) #耗时 22.83087730407715
        l.append(p)
        p.start()
    for p in l:
        p.join()
    stop=time.time()
    print('run time is %s' %(stop-start))
# IO密集型
from multiprocessing import Process
from threading import Thread
import threading
import os,time
def work():
    time.sleep(2)


if __name__ == '__main__':
    l=[]
    print(os.cpu_count()) #本机为6核
    start=time.time()
    for i in range(4000):
        p=Process(target=work) #耗时9.001083612442017s多,大部分时间耗费在创建进程上
        # p=Thread(target=work) #耗时2.051966667175293s多
        l.append(p)
        p.start()
    for p in l:
        p.join()
    stop=time.time()
    print('run time is %s' %(stop-start))

总结:

"""
python的多线程到底有没有用
需要看情况而定  并且肯定是有用的


多进程+多线程配合使用
"""

GIL存在的原因就是因为CPython解释器的内存管理不是线程安全的,那么“为什么Cpython解释器的内存管理不是线程安全的

答:

首先我们先明确一下Python解释器的内存管理——

  垃圾回收机制:

  1、引用计数

  2、标记清除

  3、分代回收

然后我们明确一下,垃圾回收机制是扫描到内存中的数据,发现没有变量名与其绑定就会触发回收机制,将该数据清除回收。

明确几个重要观点:

1、python代码只有通过Python解释器才能被识别,运行!

例如:我们开了10个进程,每个进程里面都有多个线程,那么会有多少个Python解释器?

答:10个,每个进程里面都会有一个Python解释器。且每个进程内都会有一个垃圾回收机制,垃圾回收机制也是一段代码,相当于一个线程!!!

为什么线程会不安全?

观点图示如下:

GIL全局解释器锁、死锁、信号量、event以及线程队列
目  录
TCP实现并发的两种方法
GIL全局解释器锁
GIL与普通的互斥锁
死锁现象、递归锁
信号量
event事件
线程队列
补充知识:
一、TCP实现并发的两种方法
二、GIL全局解释器锁
面试重点
三、GIL与普通的互斥锁
四、死锁现象和递归锁
五、信号量
六、event事件
七、线程队列

我们反向思考,如果Python多线程可以利用多核优势,允许同一进程下多个线程同时运行会发生什么事情

线程1刚刚创建一个数据变,如 a = 1,申请一块内存空间,将数值1 丢进去,但是我们还没有创建变量名指向它,如果4个线程同时运行,

这个时候垃圾回收线程起来了,发现 1 没有变量名与其绑定顺手就会将其清除掉!

如何解决线程的不安全性,阻止同一进程内多个线程的同时运行?

答:给Python解释器加“锁”!!

每个线程想要运行内部分代码就要获得Python解释器来翻译代码才能执行,我们给python解释器加锁使得同一时间只有一个线程可以抢到这把锁运行自己的代码,

没有抢到锁的进程只能等待锁被释放,抢到锁之后才能运行!这就保证了统一进程下只有一个线程可以运行,也就避免了同一时间线程1 创建数据 垃圾回收线程清除数据的问题出现!!

这把锁就是全局解释器所 GIL!!!

面试重点

1、GIL 是Python 的特点吗?

答:不是,GIL是CPython 的特点

2、观点:

单进程下多个线程无法利用多核优势是所有解释型语言的通病!

三、GIL与普通的互斥锁

提出问题:

既然进程内部已经有了全局解释器所GIL ,那么在同一进程内开多个线程我们还需不需要加锁?

我们根据代码结合图示来解释为什么加了GIL还会有数据错乱出现?

from threading import Thread
import time

n = 100


def task():
    global n
    tmp = n
    time.sleep(1)  #####
    n = tmp -1

t_list = []
for i in range(100):
    t = Thread(target=task)
    t.start()
    t_list.append(t)

for t in t_list:
    t.join()

print(n)
'''
time.sleep(1)  存在;执行结果 99
time.sleep(1)  不存在;执行结果 0
'''

图示:

GIL全局解释器锁、死锁、信号量、event以及线程队列
目  录
TCP实现并发的两种方法
GIL全局解释器锁
GIL与普通的互斥锁
死锁现象、递归锁
信号量
event事件
线程队列
补充知识:
一、TCP实现并发的两种方法
二、GIL全局解释器锁
面试重点
三、GIL与普通的互斥锁
四、死锁现象和递归锁
五、信号量
六、event事件
七、线程队列

答:虽然在进程内部有GIL存在,保证同一进程下只有一个线程运行,但是如果遇到I/O操作,如time.sleep(1)  GIL锁就会释放,

  这时会有多个线程去抢锁并完成代码操作  n= tem - 1 =99,导致数据错乱!

  如果没有I/O操作,那么抢到锁的线程会一直拿着这把锁执行代码,知道运行结束才会释放锁!

四、死锁现象和递归锁

前提知识:

class Demo(object):
    pass

obj1 = Demo()
obj2 = Demo()
print(id(obj1),id(obj2))
"""
只要类加括号实例化对象
无论传入的参数是否一样生成的对象肯定不一样
单例模式除外


自己千万不要轻易的处理锁的问题  

"""

死锁现象,代码演示如下:

from threading import Thread,Lock,current_thread,RLock
import time

mutexA = Lock()
mutexB = Lock()

class MyThread(Thread):
    def run(self):  # 创建线程自动触发run方法 run方法内调用func1 func2相当于也是自动触发
        self.func1()
        self.func2()

    def func1(self):
        mutexA.acquire()
        print('%s抢到了A锁'%self.name)  # self.name等价于current_thread().name
        mutexB.acquire()
        print('%s抢到了B锁'%self.name)
        mutexB.release()
        print('%s释放了B锁'%self.name)
        mutexA.release()
        print('%s释放了A锁'%self.name)

    def func2(self):
        mutexB.acquire()
        print('%s抢到了B锁'%self.name)
        time.sleep(1)   #  模拟了i/o 操作
        mutexA.acquire()
        print('%s抢到了A锁' % self.name)
        mutexA.release()
        print('%s释放了A锁' % self.name)
        mutexB.release()
        print('%s释放了B锁' % self.name)

for i in range(10):
    t = MyThread()
    t.start()
    
"""
运行结果:
    Thread-1抢到了A锁
    Thread-1抢到了B锁
    Thread-1释放了B锁
    Thread-1释放了A锁
    Thread-1抢到了B锁
    Thread-2抢到了A锁
Thread-1抢到了B锁,等到Thread-1抢A锁的时候,A锁在Thread-2手里,
Thread-2只有抢到B锁才能释放A锁,但是B锁在Thread-1 手里,这样双方都不能执行下一步操作,卡死
这种情况就是死锁现象!
"""

递归锁,代码演示如下:

from threading import Thread,Lock,current_thread,RLock
import time
"""
Rlock可以被第一个抢到锁的人连续的acquire和release
每acquire一次锁身上的计数加1
每release一次锁身上的计数减1
只要锁的计数不为0 其他人都不能抢

"""
# mutexA = Lock()
# mutexB = Lock()
mutexA = mutexB = RLock()  # A B现在是同一把锁


class MyThread(Thread):
    def run(self):  # 创建线程自动触发run方法 run方法内调用func1 func2相当于也是自动触发
        self.func1()
        self.func2()

    def func1(self):
        mutexA.acquire()
        print('%s抢到了A锁'%self.name)  # self.name等价于current_thread().name
        mutexB.acquire()
        print('%s抢到了B锁'%self.name)
        mutexB.release()
        print('%s释放了B锁'%self.name)
        mutexA.release()
        print('%s释放了A锁'%self.name)

    def func2(self):
        mutexB.acquire()
        print('%s抢到了B锁'%self.name)
        time.sleep(1)
        mutexA.acquire()
        print('%s抢到了A锁' % self.name)
        mutexA.release()
        print('%s释放了A锁' % self.name)
        mutexB.release()
        print('%s释放了B锁' % self.name)

for i in range(10):
    t = MyThread()
    t.start()

五、信号量

# 信号量可能在不同的领域中 对应不同的知识点
"""
互斥锁:一个厕所(一个坑位)
信号量:公共厕所(多个坑位)
"""
from threading import Semaphore,Thread
import time
import random


sm = Semaphore(5)  # 造了一个含有五个的坑位的公共厕所

def task(name):
    sm.acquire()
    print('%s占了一个坑位'%name)
    time.sleep(random.randint(1,3))
    sm.release()

for i in range(40):
    t = Thread(target=task,args=(i,))
    t.start()

六、event事件

使用到的方法:

  导入模块:from threading import Thread,Event

  生成事件对象: e = Event()

  使用方法:e.set() 发送信号     

       e.wait()  等待信号,只有接收到发送信号之后才能执行后面代码

from threading import Event,Thread
import time

# 先生成一个event对象
e = Event()


def light():
    print('红灯正亮着')
    time.sleep(3)
    e.set()  # 发信号
    print('绿灯亮了')

def car(name):
    print('%s正在等红灯'%name)
    e.wait()  # 等待信号
    print('%s加油门飙车了'%name)

t = Thread(target=light)
t.start()

for i in range(10):
    t = Thread(target=car,args=('伞兵%s'%i,))
    t.start()

七、线程队列

同一进程下多个线程数据是共享的,为什么还要使用队列?

"""
同一个进程下的多个线程本来就是数据共享 为什么还要用队列

因为队列是管道+锁  使用队列你就不需要自己手动操作锁的问题 

因为锁操作的不好极容易产生死锁现象
"""

普通队列:

import queue
q = queue.Queue()
q.put('hahha')
print(q.get())

后进先出队列(后进先出,先进后出也就是堆栈!!!)

q = queue.LifoQueue()
q.put(1)
q.put(2)
q.put(3)
print(q.get())

优先级队列

q = queue.PriorityQueue()
# 数字越小 优先级越高
q.put((10,'haha'))
q.put((100,'hehehe'))
q.put((0,'xxxx'))
q.put((-10,'yyyy'))
print(q.get())