【Python 总结笔记】 GIL 锁;迭代器、生成器、协程

一、谈谈什么是 GIL (全局解释器锁)

https://zhuanlan.zhihu.com/p/75780308

1 Python GIL 全局解释性锁

概念:在 CPython 中,GIL 是防止多线程并发执行机器码的互斥锁。 由于 GIL 的存在导致 Python 中的多线程并不是并发执行,而是”交替执行“。 (这导致了 Python 的多线程并未充分利用多核。反而多进程能利用多核。但是多进程切换开销比多线程切换开销大,所以出现了 trade-off 现象)

2 GIL 就一定线程安全吗?

2.1 什么是线程安全?

同一时间只有一个线程操作共享数据。由于 GIL 会自动释放,因此可能会造成线程 B 对共享数据的操作被线程 A 操作覆盖 —— 类比数据库不加行锁导致的第一类更新丢失问题。

2.2 GIL 的粗粒度安全

1)IO 操作一定是线程安全的;
2)非 IO 操作可能是线程不安全的。有 GIL 不意味着一定是线程安全,GIL 只能保证粗粒度安全。对于原子型的计算操作,若操作未执行完毕,不释放 GIL。这样能保证计算操作的线程安全性。 对于多线程操作中不是 IO 密集型,且计算操作不是原子型,对共享变量的读取和写入进行加锁,以保证线程安全。

3 如何避免 GIL 的影响:

1)IO 密集型应用中建议使用多线程 。因为首先 GIL 的存在保证了 IO 操作的线程安全性,就算因为 IO 操作线程释放 GIL,也能切换到其他非 IO 继续执行。而线程切换比进程切换要轻;
2)计算密集型应用中 ,尽量用多进程或者协程来代替多线程。因为若使用多线程,GIL 切换后,线程 2 又获取不了线程 1 对共享变量的访问锁,无法推进,多线程就失去了意义。
备注:对于 非 GIL 机制,是不存在多线程对计算密集型应用不起正向作用的。正是因为 GIL 机制导致 Python 中的多线程并不是并发执行,而是”交替执行“。

4 补充:GIL 会带来什么问题?

可能会导致某些线程迟迟得不到运行。
GIL 的问题是从多任务单核演进到多任务多核导致的问题。
对于单核机器而言,所有线程共享一个全局锁。占有 CPU 的线程就占有这把全局锁。直到阻塞或因时间片轮转而就绪。
对于多核机器而言(以双核为例),若占有 CPU 1 的线程 1 陷入 IO 阻塞而释放 GIL,则占有 CPU 2 的线程 2 必定能拿到 GIL;若线程 1 因为时间片轮转的原因,而释放 GIL,此时线程 1 与 线程 2 共同竞争 GIL,此时 GIL 可能再次被线程 1 获得。

二、迭代器、生成器、协程

参考链接:https://www.cnblogs.com/51try-again/p/11074621.html

1 Python 迭代器

迭代是访问集合元素的一种方式,可以在无需关注对象内部结构的时候,实现对集合元素的遍历。一个迭代器类需要有 2 个方法:_iter_() 与 _next_()
_iter_() 返回一个特殊的迭代器对象, 这个迭代器对象实现了_next_() 方法并通过 StopIteration 异常标识迭代的完成。
_next_() 返回下一个迭代器对象

class MyNumbers:
  def __iter__(self):  # 生成迭代器对象
    self.a = 1
    return self
 
  def __next__(self):  # 返回下一个迭代器对象
    if self.a <= 20:
      x = self.a
      self.a += 1
      return x
    else:
      raise StopIteration

myclass = MyNumbers()
myiter = iter(myclass)
for x in myiter:
  print(x)

2 生成器

使用了 yield 的函数称为 生成器(generator),生成器是一个返回迭代器的函数,只能用于迭代操作。在调用生成器运行的过程中,每次遇到 yield 的函数会暂停并保存当前所有的运行信息,返回 yield 的值,并在下一次执行 next() 方法时从当前位置继续运行。
迭代器是在可迭代对象基础上,加了 next() 方法;生成器是在迭代器的基础上,再实现了 yield 。yield 相当于 return,每次调用遍历的时候,都会在 yield 这里将新的值返回,并在这里阻塞,等待下一次调用(next() 或 generator.send(None))。【这种机制使得生成器节省了内存,实现了异步编程】

def fibonacci(n): # 生成器函数 - 斐波那契
    a, b, counter = 0, 1, 0
    while True:
        if (counter > n): 
            return
        yield a
        a, b = b, a + b
        counter += 1
f = fibonacci(10) # f 是一个迭代器,由生成器返回生成
while True:
    try:
        print (next(f), end=" ")
    except StopIteration:
        sys.exit()
2.1 生成器的执行状态

1)等待开始执行:GEN_CREATED
2)解释器正在执行:GEN_RUNNING
3)在 yield 表达式处暂停:GEN_SUSPENDED
4)执行结束:GEN_CLOSED

3 生成器与协程的区别

基于生成器可实现协程。将 yield 与 iterator.send() 的结合 可以实现协程。
备注:iterator.send() 在生成器暂停的时候向其发送数据。

def jumping_range(N):
    index = 0
    while index < N:
        # 通过send()发送的信息将赋值给jump
        # 若无发送,jump 为 None
        jump = yield index
        if jump is None:
            jump = 1
        index += jump

if __name__ == '__main__':
    itr = jumping_range(5)
    print(next(itr))  # 0
    # 从上一次断点的地方,将 2 发送给 junmp,继续执行到一次断点,此时index = 2
    print(itr.send(2))
    # 在上一次断点的时候,外部函数并未给 jump 赋值,此时 jump 为 None
    # 来到 if 函数,jump = 1
    print(next(itr)) # 3
    print(itr.send(-1)) # 2

yield index 将 index return 给外部调用程序;
jump = yield 接收外部程序 send() 发送的数据

posted @   MasterBean  阅读(155)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· .NET10 - 预览版1新功能体验(一)
点击右上角即可分享
微信分享提示