GIL

  1. 小历史

    Guido van Rossum(吉多·范罗苏姆)创建python时就只考虑到单核CPU,解决多线程之间数据完整性和状态同步的最简单方法自然就是加锁, 于是有了GIL这把超级大锁。因为cpython解析只允许拥有GIL全局解析器锁才能运行程序,这样就保证了保证同一个时刻只允许一个线程可以使用cpu。由于大量的程序开发者接收了这套机制,现在代码量越来越多,已经不容易通过c代码去解决这个问题。

  2. 并行与并发

    并行:多个CPU同时执行多个任务(多进程)
    并发:一个CPU交替处理多个任务(多线程),还是有两个程序,但是只有一个CPU,会交替处理这两个程序,而不是同时执行,只不过因为CPU执行的速度过快,而会使得人们感到是在“同时”执行,执行的先后取决于各个程序对于时间片资源的争夺

  3. GIL:Global Interperter Lock(全局解释器锁)

    1. Cpython解释器的内存管理并不是线程安全的

    2. 保护多线程情况下对Python对象的访问,Cpython使用简单的锁机制避免多个线程同时执行字节码(即只有一个线程占用CPU)

      image

  4. 影响:

    1. 限制了程序的多核运行,同一时间只能有一个线程给你执行字节码(即只有一个线程占用CPU)
      1. CPU密集程序难以利用多核优势(大部分时间花在计算上)
    2. IO期间会释放GIL,对IO密集程序影响不大 (大部分时间花在网络传输上)
  5. 如何规避影响:

    区分是CPU密集还是IO密集
    1. CPU密集可以使用多进程+进程池或者cython扩展(使用多进程完成多线程的任务)
    2. IO密集使用多线程/协程

  6. 为什么有了GIL还要关注线程安全?
    因为Python 还有 check interval 这样的抢占机制
    比如,运行如下代码:

import threading
n = 0
def foo():
    global n
    n += 1
threads = []
for i in range(100):
    t = threading.Thread(target=foo)
    threads.append(t)
for t in threads:
    t.start()
for t in threads:
    t.join()
print(n)

执行此代码会发现,其大部分时候会打印 100,但有时也会打印 99 或者 98,原因在于 n+=1 这一句代码让线程并不安全。如果去翻译 foo 这个函数的字节码就会发现,它实际上是由下面四行字节码组成:

>>> import dis
>>> dis.dis(foo)
LOAD_GLOBAL              0 (n)
LOAD_CONST               1 (1)
INPLACE_ADD
STORE_GLOBAL             0 (n)

而这四行字节码中间都是有可能被打断的!所以,千万别以为有了 GIL 程序就不会产生线程问题,我们仍然需要注意线程安全。

  1. python什么操作是原子的?

    1. 一个操作若是一个字节码指令完成的就是原子的,原子的就是线程安全的(不算LOAD和RETURN)

    2. 使用dis来分析字节码❓

      import dis
      
      def update_list(l):
          l[0] = 1
          
      dis.dis(update_list)
      """
      2           	0 LOAD_CONST               1 (1)	#2是行号
                    	2 LOAD_GLOBAL              0 (l)
                    	4 LOAD_CONST               2 (0)
                    	6 STORE_SUBSCR    			# 单字节码操作,线程安全
                    	8 LOAD_CONST               0 (None)
                   	10 RETURN_VALUE
      """
      
    3. 一般使用加互斥锁的方式保证线程安全,但对性能有一定影响

      import threading
      lock = threading.Lock()
      
      n=[0]
      def foo():
          with lock:	# 加锁,执行完后释放锁
              n[0] +=1
              n[0] +=1
              
      threads = []
      for i in range(5000):
          t = threading.Thread(target=foo)
          threads.append(t)
          
      for t in threads:
          t.start()
          
      print(n)
      
  2. 如何剖析程序性能

    使用各种profile工具(内置或第三方)
    1. 二八定律,大部分时间耗时在少量代码上
    2. 内置的profile/cprofile等工具
    3. 使用pyflame、flameprof的火焰图工具

  3. 服务端性能优化措施

    Web应用一般语言不会成为瓶颈
    1. 数据结果与算法优化
    2. 数据库层:索引优化慢查询消除,批量操作减少IO,NoSQL
    - 慢查询日志:记录响应时间超过阈值的语句,long_query_time,默认为10秒
    3. 网络IO:批量操作,pipeline操作减少IO
    - 用pipeline,避免频繁跟redis服务端交互,大量减少网络io
    -pipline+hmset

    import redis
    
    #创建连接池获取连接
    pool = redis.ConnectionPool(host='wykd', port=6379,password='123456', decode_responses=True)
    rp1 = redis.Redis(connection_pool=pool)
    
    #创建管道,可以选择开启或关闭事务,这里的事务与Redis事务一样是弱事务型
    pipe = rp1.pipeline(transaction=True)
    #在管道中添加命令
    device.info = {
    	'machineId':machineId_str,
    	'machineNum':machineId_str,
    	'factoryId':factory
    }
    pipe.hmset(device_info_key,device_info)
    
    #执行pipeline里的脚本
    pipe.execute()   
    
    1. 缓存:使用内存数据库redis/memcached,处理高并发的请求
    2. 异步:asyncio,celery
    3. 并发:gevent/多线程

问题

  1. 什么时候会释放GIL锁?

    1. 遇到像 I/O操作这种会有时间空闲情况造成CPU闲置的情况会释放GIL
    2. 会有一个专门ticks进行计数 一旦ticks数值达到100 这个时候释放GIL锁,线程之间开始竞争GIL锁(说明:ticks这个数值可以进行设置来延长或者缩减获得GIL锁的线程使用cpu的时间)
  2. 互斥锁和GIL锁的关系?

    GIL锁 :保证同一时刻只有一个线程能使用到cpu
    互斥锁:多线程时,保证修改共享数据时是有序的,不会产生数据修改混乱

    简单地说就是GIL锁是使用CPU的锁,互斥锁是修改数据的锁

    首先假设只有一个进程,这个进程中有两个线程 Thread1,Thread2, 要修改共享的数据date, 并且有互斥锁

    执行以下步骤

    1. 多线程运行,假设Thread1获得GIL可以使用cpu,这时Thread1获得互斥锁lock,Thread1可以改date数据(但并
      没有开始修改数据)
    2. Thread1线程在修改date数据前发生了 i/o操作或者ticks计数满100 (注意就是没有运行到修改data数据),这个
      时候 Thread1 让出了GIL,GIL锁可以被竞争
    3. Thread1 和 Thread2 开始竞争 GIL (注意:如果Thread1是因为 i/o 阻塞 让出的GIL Thread2必定拿到GIL,如果
      Thread1是因为ticks计数满100让出GIL 这个时候 Thread1 和 Thread2 公平竞争)
    4. 假设 Thread2正好获得了GIL, 运行代码去修改共享数据date,由于Thread1有互斥锁lock,所以Thread2无法更改共享数据
      date,这时Thread2让出GIL锁 , GIL锁再次发生竞争
    5. 假设Thread1又抢到GIL,由于其有互斥锁Lock所以其可以继续修改共享数据data,当Thread1修改完数据释放互斥锁lock,
      Thread2在获得GIL与lock后才可对data进行修改
posted @ 2021-09-09 10:55  注入灵魂  阅读(135)  评论(0)    收藏  举报