• 线程之全局解释器锁加一些了解知识点


    ---恢复内容开始---

    1.GIL全局解释器锁

    2.GIL与普通的互斥锁

    3.死锁

    4.信号量

    5.event事件

    6.线程q队列

    7.补充 基于TCP使用线程实现高并发

    一.GIL全局解释器锁

    GIL jaosn总结

     1     TCP服务端实现并发
     2         1.将不同的功能尽量拆分成不同的函数
     3             拆分出来的功能可以被多个地方使用
     4         
     5         1.将连接循环和通信循环拆分成不同的函数
     6         2.将通信循环做成多线程
     7         
     8 
     9     GIL(全局解释器锁)
    10     在CPython解释器才有GIL的概念,不是python的特点
    11     GIL也是一把互斥锁
    12         将并发变成串行 牺牲了效率但是提高了数据的安全
    13         ps:
    14             1.针对不同的数据 应该使用不同的锁去处理
    15             2.自己不要轻易的处理锁的问题 哪怕你知道acquire和release
    16             当业务逻辑稍微复杂的一点情况下 极容易造成死锁
    17     CPython中的GIL的存在是因为python的内存管理不是线程安全的
    18     
    19     内存管理
    20         引用计数:值与变量的绑定关系的个数
    21         标记清除:当内存快要满的时候 会自动停止程序的运行 检测所有的变量与值的绑定关系
    22                 给没有绑定关系的值打上标记,最后一次性清除
    23         分代回收:(垃圾回收机制也是需要消耗资源的,而正常一个程序的运行内部会使用到很多变量与值
    24         并且有一部分类似于常量,减少垃圾回收消耗的时间,应该对变量与值的绑定关系做一个分类
    25         )        新生代(5S)》》》青春代(10s)》》》老年代(20s)
    26                 垃圾回收机制扫描一定次数发现关系还在,会将该对关系移至下一代
    27                 随着代数的递增 扫描频率是降低的
    28     
    29 
    30     同一个进程下的多个线程能否同时运行
    31     GIL类似于是加在解释器上面的一把锁
    View Code

    什么是GIL:首先来看看官方的解释

      在CPython中,这个全局解释器锁,也称之为GIL,是一个互斥锁,

    防止多个线程在同一个时间执行Python字节码,这个锁是非常正要的,

    因为CPython的内存管理非线程安全的,很多其他的特性依赖于GIL,

    所以即使他影响了程序的效率也无法将其直接去除

    总结:在CPython中,GIL会把线程的并行变成串行,导致效率降低

    PS:需要知道的是,解释器并不只有CPython,还有PyPy,JPython等等。

    GIL也仅存在于CPython中,这并不是python这门语言的问题,而是CPython解释器的问题

    2.GIL带来的问题:

    首先必须明确执行一个py文件,分为三个步骤

    1. 从硬盘加载Python解释器到内存

    2. 从硬盘加载py文件到内存

    3. 解释器解析py文件内容,交给CPU执行

    其次需要明确的是每当执行一个py文件,就会立即启动一个python解释器,

    当执行test.py时其内存结构如下:

    GIL,叫做全局解释器锁,加到了解释器上,并且是一把互斥锁,那么这把锁对应用程序到底有什么影响?
    
    这就需要知道解释器的作用,以及解释器与应用程序代码之间的关系
    
    py文件中的内容本质都是字符串,只有在被解释器解释时,才具备语法意义,解释器会将py代码翻译为当前系统支持的指令交给系统执行。
    
    

    开启子线程时,给子线程指定了一个target表示该子线程要处理的任务即要执行的代码。代码要执行则必须交由解释器,即多个线程之间就需要共享解释器,为了避免共享带来的数据竞争问题,于是就给解释器加上了互斥锁!
    
    由于互斥锁的特性,程序串行,保证数据安全,降低执行效率,GIL将使得程序整体效率降低!

    3.那么为什么需要GIL锁:

    GIL与GC
    
    在使用Python中进行编程时,程序员无需参与内存的管理工作,这是因为Python有自带的内存管理机制,简称GC。那么GC与GIL有什么关联?
    
    要搞清楚这个问题,需先了解GC的工作原理,Python中内存管理使用的是引用计数,每个数会被加上一个整型的计数器,表示这个数据被引用的次数,当这个整数变为0时则表示该数据已经没有人使用,成了垃圾数据。
    
    当内存占用达到某个阈值时,GC会将其他线程挂起,然后执行垃圾清理操作,垃圾清理也是一串代码,也就需要一条线程来执行。
    
    示例代码:

    from threading import Thread
    def task():
    a = 10
    print(a)

    
    

    # 开启三个子线程执行task函数
    Thread(target=task).start()
    Thread(target=task).start()
    Thread(target=task).start()

     

    上述代码的内存结构如下:


    通过上图可以看出,GC与其他线程都在竞争解释器的执行权,而CPU何时切换,以及切换到哪个线程都是无法预支的,这样一来就造成了竞争问题,假设线程1正在定义变量a=10,而定义变量第一步会先到到内存中申请空间把10存进去,第二步将10的内存地址与变量名a进行绑定,如果在执行完第一步后,CPU切换到了GC线程,GC线程发现10的地址引用计数为0则将其当成垃圾进行了清理,等CPU再次切换到线程1时,刚刚保存的数据10已经被清理掉了,导致无法正常定义变量。

    当然其他一些涉及到内存的操作同样可能产生问题问题,为了避免GC与其他线程竞争解释器带来的问题,CPython简单粗暴的给解释器加了互斥锁,如下图所示:


    有了GIL后,多个线程不可能在同一时间使用解释器,从而保证了解释器的数据安全

    GIL的加锁时机:在调用解释器时立即加锁

    解锁时机:
      当前线程遇到了IO时释放

      当前线程执行时间超过设定值时释放

    GIL锁有优点也有缺点:
    优点:

      保证了数据的安全

    缺点:
      互斥锁的特性使得多线程无法并行

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

    案例:计算密集型

     1 from multiprocessing import Process
     2 from threading import Thread
     3 import os,time
     4 def work():
     5     res=0
     6     for i in range(100000000):
     7         res*=i
     8 
     9 
    10 if __name__ == '__main__':
    11     l=[]
    12     print(os.cpu_count())  # 本机为6核
    13     start=time.time()
    14     for i in range(6):
    15         # p=Process(target=work) #耗时  4.732933044433594
    16         p=Thread(target=work) #耗时 22.83087730407715
    17         l.append(p)
    18         p.start()
    19     for p in l:
    20         p.join()
    21     stop=time.time()
    22     print('run time is %s' %(stop-start))
    View Code

    案例:IO密集型

     1 from multiprocessing import Process
     2 
     3 import os,time
     4 def work():
     5     time.sleep(2)
     6 
     7 if __name__ == '__main__':
     8     l=[]
     9     print(os.cpu_count()) #本机为6核
    10     start=time.time()
    11     for i in range(4000):
    12         p=Process(target=work) #耗时9.001083612442017s多,大部分时间耗费在创建进程上
    13         # p=Thread(target=work) #耗时2.051966667175293s多
    14         l.append(p)
    15         p.start()
    16     for p in l:
    17         p.join()
    18     stop=time.time()
    19     print('run time is %s' %(stop-start))
    View Code

    总结:

    python的多线程到底有没有用
    需要看情况而定 并且肯定是有用的
    多进程+多线程配合使用

    二.GIL与普通的互斥锁区别

    GIL保护的是解释器级别的数据安全,比如对象的引用计数,垃圾分代数据等等,具体参考垃圾回收机制详解。
    
    对于程序中自己定义的数据则没有任何的保护效果,这一点在没有介绍GIL前我们就已经知道了,所以当程序中出现了共享自定义的数据时就要自己加锁,如下例:
    案例:向一下代码那样,还是需要加互斥所的
     1 from threading import Thread
     2 import time
     3 
     4 n = 100
     5 
     6 
     7 def task():
     8     global n
     9     tmp = n
    10     time.sleep(1)
    11     n = tmp -1
    12 
    13 t_list = []
    14 for i in range(100):
    15     t = Thread(target=task)
    16     t.start()
    17     t_list.append(t)
    18 
    19 for t in t_list:
    20     t.join()
    21 
    22 print(n)
    View Code

    三.死锁

    死锁问题
    当程序出现了不止一把锁,分别被不同的线程持有, 有一个资源 要想使用必须同时具备两把锁
    这时候程序就会进程无限卡死状态 ,这就称之为死锁

     案例:

    案例:

    from threading import 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()
    这样就会产生死锁现象:解释
    首先执行的是func1然后线程1抢到A锁,其他九个线程需要的需要等待A锁被释放,
    线程1不会释放A锁,紧接着就会抢B锁,B锁是没有人抢的,所以直接就可以拿到B锁,
    然后释放B锁,紧接着释放A锁,那么其他九个在等待的线程看到A锁被线程1释放了,
    他们就会立马区抢,那么线程1已经区执行func2了,因为其他九个线程都在func1哪里抢,
    线程1直接就可以抢到B锁,然后线程1会进入阻塞状态1秒,但是线程1还持有者B锁,
    func1里面的线程也开始抢B锁了,1秒阻塞状态过去,线程1也需要抢A锁了,
    但是A锁被func1里面的线程持有,这样就会产生我要你的A锁,你要我的B锁,
    但是都给不了,就会卡住

    补充知识点:

    Rlock 称之为递归锁或者可重入锁

    与Lock唯一的区别: Rlock同一线程可以多次执行acquire 但是执行几次acquire就应该对应release几次 如果一个线程已经执行过acquire 其他线程将无法执行acquire

     案例:

     1 from threading import Thread,Lock,current_thread,RLock
     2 import time
     3 """
     4 Rlock可以被第一个抢到锁的人连续的acquire和release
     5 每acquire一次锁身上的计数加1
     6 每release一次锁身上的计数减1
     7 只要锁的计数不为0 其他人都不能抢
     8 
     9 """
    10 # mutexA = Lock()
    11 # mutexB = Lock()
    12 mutexA = mutexB = RLock()  # A B现在是同一把锁
    13 
    14 
    15 class MyThread(Thread):
    16     def run(self):  # 创建线程自动触发run方法 run方法内调用func1 func2相当于也是自动触发
    17         self.func1()
    18         self.func2()
    19 
    20     def func1(self):
    21         mutexA.acquire()
    22         print('%s抢到了A锁'%self.name)  # self.name等价于current_thread().name
    23         mutexB.acquire()
    24         print('%s抢到了B锁'%self.name)
    25         mutexB.release()
    26         print('%s释放了B锁'%self.name)
    27         mutexA.release()
    28         print('%s释放了A锁'%self.name)
    29 
    30     def func2(self):
    31         mutexB.acquire()
    32         print('%s抢到了B锁'%self.name)
    33         time.sleep(1)
    34         mutexA.acquire()
    35         print('%s抢到了A锁' % self.name)
    36         mutexA.release()
    37         print('%s释放了A锁' % self.name)
    38         mutexB.release()
    39         print('%s释放了B锁' % self.name)
    40 
    41 for i in range(10):
    42     t = MyThread()
    43     t.start()
    View Code

    四.信号量

    可以现在被锁定的代码 同时可以被多少线程并发访问
    互斥锁: 锁住一个马桶 同时只能有一个
    信号量: 锁住一个公共厕所 同时可以来一堆人


    用途: 仅用于控制并发访问 并不能防止并发修改造成的问题

    案例:

     1 from threading import Semaphore,Thread
     2 import time
     3 import random
     4 
     5 sm = Semaphore(5)  # 造了一个含有五个的坑位的公共厕所
     6 
     7 def task(name):
     8     sm.acquire()
     9     print('%s占了一个坑位'%name)
    10     time.sleep(random.randint(1,3))
    11     sm.release()
    12 
    13 for i in range(40):
    14     t = Thread(target=task,args=(i,))
    15     t.start()
    View Code

    五.event事件

    什么是事件

    事件表示在某个时间发生了某个事情的通知信号,用于线程间协同工作。

    因为不同线程之间是独立运行的状态不可预测,所以一个线程与另一个线程间的数据是不同步的,当一个线程需要利用另一个线程的状态来确定自己的下一步操作时,就必须保持线程间数据的同步,Event就可以实现线程间同步

    可用的一些方法:

    event.isSet():返回event的状态值;
    event.wait():将阻塞线程;知道event的状态为True
    event.set(): 设置event的状态值为True,所有阻塞池的线程激活进入就绪状态, 等待操作系统调度;
    event.clear():恢复event的状态值为False

    案例:

     1 from threading import Event,Thread
     2 import time
     3 
     4 # 先生成一个event对象
     5 e = Event()
     6 
     7 
     8 def light():
     9     print('红灯正亮着')
    10     time.sleep(3)
    11     e.set()  # 发信号
    12     print('绿灯亮了')
    13 
    14 def car(name):
    15     print('%s正在等红灯'%name)
    16     e.wait()  # 等待信号
    17     print('%s加油门飙车了'%name)
    18 
    19 t = Thread(target=light)
    20 t.start()
    21 
    22 for i in range(10):
    23     t = Thread(target=car,args=('伞兵%s'%i,))
    24     t.start()
    View Code

     六.线程q对列

    同一个进程下的多个线程本来就是数据共享 为什么还要用队列
    
    因为队列是管道+锁  使用队列你就不需要自己手动操作锁的问题 
    
    因为锁操作的不好极容易产生死锁现象

    1.Queue 先进先出队列

    与多进程中的Queue使用方式完全相同,区别仅仅是不能被多进程共享。

     案例:

    q =  Queue(3)
    q.put(1)
    q.put(2)
    q.put(3)
    print(q.get(timeout=1))
    print(q.get(timeout=1))
    print(q.get(timeout=1))

    2.LifoQueue 后进先出队列

    该队列可以模拟堆栈,实现先进后出,后进先出

     案例:

    lq = LifoQueue()
    
    lq.put(1)
    lq.put(2)
    lq.put(3)
    
    print(lq.get())
    print(lq.get())
    print(lq.get())

    3.PriorityQueue 优先级队列

    该队列可以为每个元素指定一个优先级,这个优先级可以是数字,字符串或其他类型,但是必须是可以比较大小的类型,取出数据时会按照从小到大的顺序取出

     案例:

    pq = PriorityQueue()
    # 数字优先级
    pq.put((10,"a"))
    pq.put((11,"a"))
    pq.put((-11111,"a"))
    
    print(pq.get())
    print(pq.get())
    print(pq.get())
    # 字符串优先级
    pq.put(("b","a"))
    pq.put(("c","a"))
    pq.put(("a","a"))
    
    print(pq.get())
    print(pq.get())
    print(pq.get())

     补充:如何解决基于TCP的高并发

    server:

     1 import socket
     2 from threading import Thread
     3 
     4 """
     5 服务端
     6     1.要有固定的IP和PORT
     7     2.24小时不间断提供服务
     8     3.能够支持并发
     9 """
    10 
    11 server = socket.socket()
    12 server.bind(('127.0.0.1',8080))
    13 server.listen(5)
    14 
    15 
    16 def talk(conn):
    17     while True:
    18         try:
    19             data = conn.recv(1024)
    20             if len(data) == 0:break
    21             print(data.decode('utf-8'))
    22             conn.send(data.upper())
    23         except ConnectionResetError as e:
    24             print(e)
    25             break
    26     conn.close()
    27 
    28 while True:
    29     conn, addr = server.accept()  # 监听 等待客户端的连接  阻塞态
    30     print(addr)
    31     t = Thread(target=talk,args=(conn,))
    32     t.start()
    View Code

    client:

     1 import socket
     2 
     3 
     4 client = socket.socket()
     5 client.connect(('127.0.0.1',8080))
     6 
     7 while True:
     8     client.send(b'hello')
     9     data = client.recv(1024)
    10     print(data.decode('utf-8'))
    View Code
  • 相关阅读:
    Docker学习笔记
    SpringMVC学习笔记
    机器学习预测2022年考研成绩、考研分数线
    代码随想录贪心算法
    给定两个序列src,dst,src为入栈顺序,判断dst是否为src的一个出栈顺序(c++)
    代码随想录回溯算法
    代码随想录动态规划
    代码随想录二叉树
    Docker安装memcached
    OpenEuler镜像配置
  • 原文地址:https://www.cnblogs.com/zahngyu/p/11353069.html
Copyright © 2020-2023  润新知