python GIL

CPython 中的 GIL:影响与应对方法

Python 官方文档


global interpreter lock:
The mechanism used by the CPython interpreter to assure that only one thread executes Python bytecode at a time. This simplifies the CPython implementation by making the object model (including critical built-in types such as dict) implicitly safe against concurrent access. Locking the entire interpreter makes it easier for the interpreter to be multi-threaded, at the expense of much of the parallelism afforded by multi-processor machines.

However, some extension modules, either standard or third-party, are designed so as to release the GIL when doing computationally intensive tasks such as compression or hashing. Also, the GIL is always released when doing I/O.

Past efforts to create a “free-threaded” interpreter (one which locks shared data at a much finer granularity) have not been successful because performance suffered in the common single-processor case. It is believed that overcoming this performance issue would make the implementation much more complicated and therefore costlier to maintain.


全局解释程序锁:
全局解释程序锁(GIL)是 CPython 解释器的一个特性,确保一次只有一个线程可以执行 Python 字节码。GIL 的存在简化了 CPython 的实现,通过隐式地为对象模型提供并发访问安全性,甚至包括关键的内置类型,如字典。虽然全局解释程序锁会限制多线程环境中并行执行的能力,但通过将同步复杂性集中到解释器级别,它使得解释器更容易支持多线程。然而,这也导致多核处理器提供的并行性大部分被消耗。
不过,一些扩展模块(无论是标准的还是第三方的)被设计为在执行计算密集型任务(如压缩或散列)时会释放 GIL。此外,在执行 I/O 操作时,GIL 总是会被释放。
过去创建“自由线程”解释器(以更细的粒度锁定共享数据的解释器)的努力没有成功,因为在常见的单处理器情况下性能受到影响。据信,克服该性能问题将使实现复杂得多,并且因此维护成本更高。


不同 Python 解释器的特点及 GIL 影响

Python 生态系统中有多种 Python 解释器实现,每个实现都有其独特的特点:

  • CPython:作为 Python 的标准解释器,使用 C 语言开发,对 Python 语言特性的支持最全面,广泛被使用。
  • Jython:基于 Java 开发,能够将 Python 代码编译为 Java 字节码,运行在 JVM 上,且可以调用 Java 库。
  • IronPython:使用 C# 开发,运行在 .NET 平台上,可以调用 C# 库和 .NET 框架。
  • PyPy:使用 Python 开发,通过 JIT 技术提高执行效率,与 CPython 兼容性较好。
  • Cython:并非独立的解释器,而是一种能够将 Python 扩展为可编译成 C 语言的语法扩展,可以生成 C 语言代码以提高效率,需要编译后才能在 CPython 中使用。
  • 还有其他实现如 Stackless Python、MicroPython 等。
  • CPython 作为使用 C 语言开发的标准解释器,存在全局解释器锁(GIL)的问题,从而导致多线程无法充分利用多核 CPU。

  • Jython 和 IronPython:由于基于 Java 和 .NET 运行时,不存在 GIL 问题,可以在多核 CPU 上充分发挥优势。

  • PyPy:虽然 PyPy 也有 GIL,但其使用 JIT 编译技术,部分优化了 GIL 带来的问题。

  • Cython:Cython 本身并非独立的解释器,但它可以通过编译成 C 语言的扩展来提高 Python 解释器的效率,不受 GIL 影响。


为什么 CPython 中存在 GIL?

CPython 中存在全局解释器锁(GIL)的主要原因是其历史和设计初衷。

历史背景: Python 最初在单核 CPU 时代诞生,其设计初衷并未考虑多线程并发执行的场景。在此背景下,设计师并没有着重解决多线程竞争问题。

引用计数内存管理机制: CPython 主要使用引用计数内存管理机制。然而,这种机制并不是线程安全的。具体而言,引用计数机制在多线程环境中容易出现问题,多个线程同时对对象引用计数进行增减操作可能引发竞争条件和线程安全问题。

为了避免这些问题,CPython 在执行 Python 字节码时引入了 GIL。这个机制确保同一时刻只有一个线程可以执行 Python 字节码,从而简化了内存管理。然而,这也限制了在多核 CPU 下实现真正的并发执行。

尽管 GIL 在某些情况下降低了多线程编程的便利性,但它在某些方面也有好处,例如在 I/O 密集型任务中释放 GIL,允许其他线程执行。

CPython 使用 C 扩展库: CPython 广泛使用 C 语言扩展库来提升性能。在多线程环境下,C 扩展库需要自己实现线程同步,这可能增加复杂性和错误的可能性。

降低 GIL 影响的方法:

降低 GIL 影响的策略包括但不限于以下方法:

  1. 利用多进程充分利用多核 CPU,从而避免 GIL 的限制。
  2. 使用异步编程来优化 I/O 密集型应用的并发性。
  3. 使用 C 扩展库或 Cython 优化计算密集型代码的执行效率。
  4. 利用 multiprocessing 模块提供的进程池实现多进程并发。
  5. 考虑使用无 GIL 的解释器,如 Jython、IronPython。
  6. 使用并行计算框架如 Dask、Joblib 支持并行计算。
  7. 优化算法和数据结构以减少竞争锁的使用。
  8. 分离计算密集型代码和 I/O 操作,充分利用 GIL 的释放。
  9. 对于 I/O 密集型任务,使用多线程优化而非计算密集型任务。
  10. 合理设置线程数量,避免过多线程竞争。
  11. 使用原生线程或 asyncio 等作为并发手段。
  12. 在适当的情况下使用共享内存和消息传递,减少锁的竞争。
  

Python迭代器与生成器

Python 迭代器和生成器

Python 中的迭代器和生成器是用于处理可迭代对象的强大工具。它们有一些共同点,但也有一些重要的区别。

迭代器(Iterators)

  • 迭代器是可迭代对象的一个子集。
  • 它是一个可以记住遍历位置的对象。
  • 迭代器需要实现 __iter__()__next__() 方法。
  • 使用 iter() 方法可以将列表、元组、集合、字符串等可迭代对象转换为迭代器。
  • 通过调用 next() 方法逐个访问元素。
  • 不能通过索引来访问生成器的元素。
  • 当没有更多元素可迭代时,next() 方法会引发 StopIteration 异常。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 创建一个可迭代对象(列表)
my_list = [1, 2, 3, 4, 5]

# 使用iter()方法将可迭代对象转换为迭代器
my_iterator = iter(my_list)

# 使用next()方法逐个访问迭代器的元素
try:
while True:
element = next(my_iterator)
print(element)
except StopIteration:
pass

在这个示例中,将列表my_list转换为迭代器my_iterator,然后使用next()方法逐个访问迭代器的元素。当没有更多的元素可迭代时,next()方法会引发StopIteration异常,我们通过异常处理来结束循环。

生成器(Generators)

  • 生成器是一种特殊的迭代器。
  • 使用 yield 关键字替代 return 返回值,将函数变成生成器对象。
  • 生成器不会一次性生成所有值,而是按需生成每个值,这可以节省内存。
  • 生成器只能被迭代一次,一旦耗尽,需要重新创建或重新初始化才能再次迭代。
  • 不能通过索引来访问生成器的元素。

生成器是生成元素的,迭代器是访问集合元素的一种方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 创建一个生成器函数
def my_generator():
yield 1
yield 2
yield 3

# 使用生成器函数创建生成器对象
gen = my_generator()

# 迭代生成器并按需生成值
for value in gen:
print(value)

# 尝试再次迭代,会发现生成器已经耗尽
for value in gen:
print(value) # 不会打印任何内容,因为生成器已经耗尽

迭代器用于按需访问集合元素,而生成器用于按需生成元素

  

Python 下划线

  • 单个前导下划线:

    用于表示该变量是模块内部使用的,作为约定,告诉其他开发者这不是公共接口的一部分,不建议直接访问

    1
    2
    3
    4
    5
    6
    class MyClass:
    def __init__(self):
    self.public_variable = 42
    self._protected_variable = 'protected data'
    obj = MyClass()
    print(obj._protected_variable) # 输出: protected data
  • 单个末尾下划线

    避免与Python关键字或者内置函数名命名冲突时,可以使用单个末尾下划线来重命名变量,如class_可以避免与class冲突

    class_ = 'test'
    print(class_)  # 输出: test
    ------------------------------
    class= 'test'
        File "<stdin>", line 1
          class= 'test'
             ^
    SyntaxError: invalid syntax
  • 双前导下划线

    名称修饰,保护父类属性的访问,防止子类属性覆盖父类属性,访问方法obj._类名__属性名

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    class Parent:
    def __init__(self):
    self.__x = 10 # 使用双前导下划线命名属性

    class Child(Parent):
    def __init__(self):
    super().__init__()
    self.__x = 20 # 子类定义相同名称的属性

    parent = Parent()
    child = Child()

    print(parent.__dict__) # 输出 {'_Parent__x': 10}
    print(child.__dict__) # 输出 {'_Parent__x': 10, '_Child__x': 20}

    print(parent._Parent__x) # 输出 10
    print(child._Child__x) # 输出 20

  • 双前导和双末尾下划线

    用于标识特殊方法(魔术方法),如 __init__(构造方法)、__str__(字符串表示方法)。

    预定义的特殊名称,如,__name__ 是一个包含当前模块名称的特殊变量。__file__ 包含当前模块的文件路径。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class MyClass:
    def __init__(self):
    self.__count__ = 0

    def __str__(self):
    return f"MyClass instance with count: {self.__count__}"
    if __name__ =='__main__': # 预定义的特殊名称
    obj = MyClass()
    obj.__count__ = 5 # 使用末尾下划线的特殊变量
    print(obj) # 输出 "MyClass instance with count: 5"

  • 单个下划线:

    用来表示无关紧要的变量

    1
    2
    for _ in range(5):
    print('test')

    在交互式解释器中,_指向最后一次执行的表达式结果

    1
    2
    3
    4+8
    7+9
    _ # 输出16
  

Python爬虫 requests访问http网站之443报错(ssl验证)

报错信息:

1
urllib3.exceptions.MaxRetryError: HTTPSConnectionPool(host='ssr4.scrape.center', port=443): Max retries exceeded with url: /page/1 (Caused by SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1007)')))

SSL介绍:

  • SSL(Secure Sockets Layer)验证是在建立HTTPS连接时进行的一种验证过程。SSL验证的目的是确保服务器的身份,并验证通信双方之间的信任关系。以下是SSL验证的基本过程:

  • 客户端请求建立SSL连接:当客户端(通常是Web浏览器)向服务器发送HTTPS连接请求时,它将尝试与服务器建立SSL连接。

  • 服务器发送数字证书:如果服务器支持SSL,它会将包含数字证书的响应发送给客户端。数字证书由经过可信认证的第三方机构(证书颁发机构)签发,并包含了服务器的公钥、服务器的身份信息以及其他相关信息。

  • 客户端验证数字证书:客户端接收到服务器发送的数字证书后,会对证书进行验证。验证过程包括以下几个方面:

    • a. 校验证书的签发机构:客户端会检查证书是否由被客户端信任的可信证书颁发机构签发。

    • b. 检查证书的有效期:客户端会验证证书是否在有效期内,即确认证书尚未过期。

    • c. 验证服务器域名:客户端会核对证书中的服务器域名与客户端请求的域名是否匹配。这可以防止中间人攻击等安全威胁。

    • d. 检查证书的撤销状态:客户端会查询证书撤销列表(CRL)或在线证书状态协议(OCSP)服务器,确认证书是否被吊销。

  • 客户端生成会话密钥:一旦客户端验证通过,它会生成一个临时的会话密钥(也称为”对称密钥”或”会话密钥”),用于该SSL会话的后续加密和解密操作。

  • 安全数据传输:客户端使用服务器的公钥对会话密钥进行加密,并将加密后的会话密钥发送给服务器。服务器使用自己的私钥解密接收到的会话密钥。之后,客户端和服务器之间的通信将使用该会话密钥进行对称加密和解密。

  • 通过SSL验证过程,客户端可以验证服务器的身份,并确保与服务器之间建立了安全的通信通道。这种验证过程可以帮助防止中间人攻击、欺骗和数据篡改等安全威胁。同时,SSL验证还为用户提供了信任和可靠性,因为证书是由可信的第三方机构签发的,代表了服务器的身份和认证信息。
    使用requests.get(verify=False) 关闭ssl验证。
    urllib3.disable_warnings()禁用 urllib3 库中的 SSL 证书验证警告

Python魔术方法

Python常用的魔术方法有

__init__,对象被创建后调用,用于初始化对象的属性。

__new__,对象被创建之前被调用,

__str__,用于打印该对象展示给用户的字符串信息。

__repr__,用于开发调试时,给开发人员看的打印对象的详细信息

__len__,用于获取容器对象的元素个数。

__call__,允许对象像函数一样被调用,可以传递参数。

__iter__,返回一个迭代器,允许对象成为一个可以迭代的容器,常和__next__一起使用,

  

:D 一言句子获取中...