并发编程(三) GIL全局解释器锁和其他知识

GIL全局解释器锁

全局解释器的官方解释

In CPython, the global interpreter lock, or GIL, is a mutex that prevents multiple
native threads from executing Python bytecodes at once. This lock is necessary mainly
because CPython’s memory management is not thread-safe.

ps:python解释器有很多种 最常见的就是Cpython解释器
GIL的存在是因为CPython解释器的内存管理不是线程安全的

垃圾回收机制
1.引用计数
2.标记清除
3.分代回收

并发编程(三) GIL全局解释器锁和其他知识

引用计数:值和变量绑定关系的个数
标记清除:内存快满的时候,扫描内存,没有绑定变量的值会被清除掉
分代回收:频繁扫描是无意义的,因此采取措施降低扫描频率。垃圾回收是会消耗资源的,而程序运行内部会用到变量与值,并且部分是于常量,要减少垃圾回收消耗的时间
GIL本质也是一把互斥锁:将并发变成串行牺牲效率保证数据的安全 
用来阻止同一个进程下的多个线程的同时执行(同一个进程内多个线程无法实现并行但是可以实现并发)

python的多线程没法利用多核优势 是不是就是没有用了?

研究python的多线程是否有用需要分情况讨论

四个任务 计算密集型的

分析:计算密集,无阻塞态,一个线程走完另一个线程启动,因此开4个线程需要40秒
开4个进程,那就是并行,因此只需要10秒

单核情况下
    开线程更省资源
多核情况下
    开进程 10s
    开线程 40s
计算密集型,多核开进程最快
from multiprocessing import Process
from threading import Thread
import os, time


def work():
    res = 0
    for i in range(10000000):
        res *= i


if __name__ == '__main__':
    l = []
    # print(os.cpu_count())  # 本机为6核
    start = time.time()
    for i in range(6):
        p=Process(target=work)     # 耗时 2.8391623497009277
        # p = Thread(target=work)  # 耗时 4.586262226104736
        l.append(p)
        p.start()
    for p in l:
        p.join()
    stop = time.time()
    print('run time is %s' % (stop - start))

四个任务 IO密集型的

分析:阻塞态,开哪个都可以,都是等待,开进程开销还更大,因此开线程

单核情况下
    开线程更节省资源
多核情况下
    开线程更节省资源
# 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()#join的作用:让列表中的线程不要接着往下走!
    stop=time.time()
    print('run time is %s' %(stop-start))

join的作用:让进程等待结束再进行下一个进程(后来总结:让列表中的线程不要接着往下走!)

如何实现TCP服务端实现并发?

思路:while循环开线程

#服务端 
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()  # 监听 等待客户端的连接  阻塞态
  
    t = Thread(target=talk,args=(conn,))
    t.start()
ps:常见问题
GIL是不是python解释器的特点?
GIL是Cpython解释器的特点,python解释器有多个语言编写的版本

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)

之前不明白第二个for循环join什么时候开始运行,在线程sleep的时候,原本是直接到最下面执行print,现在多了两行代码,走到join这里,发现要等待,不能跳着运行

死锁

死锁的原因是一个线程抢两把锁

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)
        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()

现在是线程1跑到func2,抢到了B锁,准备抢A锁,而A锁在线程2的手上产生了死锁

并发编程(三) GIL全局解释器锁和其他知识

创建线程自动触发run方法 run方法内调用func1 func2相当于也是自动触发
self.name等价于current_thread().name。线程号的意思

不要轻易动“锁”有关的事!

解决方法:Rlock递归锁

所有取名递归,就是它可以有一个计数的功能,数值可加可减

将锁替换为Rlock()即可
from threading import Thread,Lock,current_thread,RLock
import time
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
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()#放锁位置
    print('%s走了'%name)
for i in range(40):
    t = Thread(target = task ,args =(i,))
    t.start()
结果:
0占了一个坑位
1占了一个坑位
2占了一个坑位
3占了一个坑位
4占了一个坑位
4走了
0走了
1走了
3走了
2走了

event事件

之前的join方法是让主进程等待子进程

案例演示:等红绿灯

操作步骤:

1先生成一个event对象

2发信号e.set()

3.等待信号e.wait()

from threading import Event,Thread
import time

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

def light():
    print('红灯正亮着')
    time.sleep(1)
    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()

线程q

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

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

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


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())