42. 导入锁

导入锁是防止不安全并发导入的同步机制。在 CPython 中,导入不仅仅是名称查找。他们可能会创建模块对象、改变sys.modules,执行任意Python代码,初始化扩展模块,更新包属性,编译源文件,读取字节码缓存,以及运行包初始化代码。

如果没有锁定,两个线程可以同时导入同一模块并观察到不一致的模块状态。

导入锁的存在是因为导入是执行,而执行会改变共享的运行时状态。

42.1 为什么导入需要锁定

考虑这个模块:```python id="ez8v8a"

cache.py

print("initializing cache")

items = {}

def get(key): return items[key] 现在考虑两个线程同时执行此操作:python id="2pklaf" import cache 如果没有同步,两个线程可能:text id="oy5qmp" create a module object insert or overwrite sys.modules["cache"] execute cache.py initialize items twice observe a partially initialized module bind different module objects


## 42.2 导入改变全局运行时状态

导入涉及共享状态。

重要的共享结构包括:```text id="yxk2bl"
sys.modules
sys.meta_path
sys.path
sys.path_hooks
sys.path_importer_cache
parent package attributes
module dictionaries
bytecode cache files
extension module state
```最重要的是`sys.modules````python id="ul20gj"
import sys

print(type(sys.modules))

sys.modules是一个普通的字典,但它是模块标识的核心。如果并发导入损坏了模块插入或替换,则运行时可以观察到重复的模块或不完整的模块。

42.3 简单心智模型

带锁定的简化导入如下所示:```python id="d8ycx5" def import_module(name): lock = get_import_lock_for(name)

with lock:
    if name in sys.modules:
        return sys.modules[name]

    spec = find_spec(name)
    module = module_from_spec(spec)
    sys.modules[name] = module

    try:
        spec.loader.exec_module(module)
    except Exception:
        del sys.modules[name]
        raise

    return module

真正的实现案例较多,但中心思想是:text id="dsb489" for a given module name, only one thread should execute that module's initialization at a time


现代 CPython 在导入机制中使用特定于模块的导入锁。目标是避免不必要地序列化所有导入,同时仍然防止两个线程同时初始化同一模块。

从概念上讲:```text id="r8nrgm"
import a        locks module name "a"
import b        locks module name "b"
import a again  waits for "a" if another thread is initializing it
```这比单个全局导入锁提供了更多的并发性,同时保证了每个模块身份的安全。

锁键是完全限定的模块名称:```text id="ev5p7h"
json
json.decoder
email.message
package.submodule
```每个名称都有自己的导入同步边界。

## 42.5 导入锁与 GIL

全局解释器锁和导入锁解决不同的问题。

|机制|目的|
|---|---|
|吉尔 |保护解释器执行和许多内部对象操作 |
|进口锁|防止不安全的并发模块初始化 |

GIL 并没有消除导入锁定的需要。

模块导入可以执行文件 I/O、执行任意 Python 代码、释放本机代码中的 GIL 或等待其他操作。当第一个导入仍在进行时,另一个线程可以尝试相同的导入。

导入锁保护更高级别的不变量:```text id="u3ib58"
one module name should not be initialized concurrently by multiple threads
```## 42.6 导入是可重入的

进口可以引发更多进口。

例子:```python id="4hqnm0"
# app.py
import config
import server
# server.py
import logging
import socket
```输入`app`进口`server`,并导入`server`导入其他模块

导入锁定机制必须支持嵌套导入

从概念上讲:```text id="k2va9e"
import app
    lock app
    execute app.py
        import server
            lock server
            execute server.py
                import socket
                    lock socket
                    execute socket.py
```这是正常的

当第一个模块仍在初始化时必须允许正在导入一个模块的线程导入另一个模块

## 42.7 同一模块的递归导入

同一模块的递归导入可以通过循环导入进行。```python id="z153co"
# a.py
import b
x = 1
# b.py
import a
y = 2
```什么时候`a`进口`b`, `b`进口`a`,第二次导入`a`等待本身不应该陷入僵局

这是导入锁定必须仔细跟踪所有权和递归的原因之一

导入系统通过返回部分初始化的模块来处理这个问题`sys.modules`在适当的时候

这可以防止无限递归和自死锁但它会暴露部分初始化的状态

## 42.8 部分初始化的模块

在导入过程中CPython 将模块放入`sys.modules`在执行其代码之前

这支持循环导入

为了:```python id="4jdauu"
# a.py
import b
value = 1
```:```python id="ayh1bo"
# b.py
import a
print(a.value)
```该模块`a`存在于`sys.modules``value`被分配所以`b`可以导入`a`, `a.value`可能还不存在

导入锁可防止两个并发初始化它不会使不完整的模块变得完整

不同的问题

|问题 |导入锁有帮助吗 |
|---|---|
|两个线程初始化同一个模块|是的 |
|循环导入观察不完整模块|没有 |
|模块阴影 |没有 |
|坏的`sys.path`订单|没有 |
|顶级副作用|只防止并发重复 |

锁提供了线程安全性它不修复依赖结构

## 42.9 导入锁获取指令

导入形成依赖链一个模块在导入另一个模块时可以持有自己的导入锁

例子:```text id="m3czjg"
Thread 1:
    lock a
    import b
    wait for lock b

Thread 2:
    lock b
    import a
    wait for lock a
```这是一个经典的死锁形状

CPython 的导入机制包括围绕模块锁的死锁检测当它检测到循环等待时它可以避免永远挂起而是处理部分初始化的模块路径

重要的设计点是导入锁必须处理可重入和循环依赖图

## 42.10 线程导入示例

此示例启动多个导入同一模块的线程。```python id="2bqtk9"
# slowmod.py
import time

print("slowmod start")
time.sleep(2)
value = 42
print("slowmod end")
# main.py
import threading

def worker():
    import slowmod
    print(slowmod.value)

threads = [threading.Thread(target=worker) for _ in range(5)]

for t in threads:
    t.start()

for t in threads:
    t.join()
```预期行为:```text id="mq93uj"
slowmod start
slowmod end
42
42
42
42
42
```模块主体执行一次其他线程等待导入完成然后使用缓存的模块

## 42.11 同时导入不同的模块

每个模块的锁允许在依赖关系允许的情况下同时导入不同的模块

主题 1:```python id="jg44xg"
import alpha
```话题2:```python id="vyvgaf"
import beta
```如果`alpha``beta`是独立的它们的导入不需要阻塞在同一个模块锁上

然而他们可能仍然会争论:```text id="4h21u8"
the GIL
file-system operations
path importer caches
extension module initialization
shared package parents
custom finder state
```导入并发是可能的但导入仍然是一个复杂的全局操作

## 42.12 父包和子模块锁

对于:```python id="bbvi06"
import package.submodule
```导入系统必须加载`package`第一的

从概念上讲:```text id="9mmvrw"
lock package
initialize package
lock package.submodule
initialize package.submodule
bind package.submodule attribute
```如果多个线程导入同一包的不同子项:```python id="rp9s87"
import package.alpha
import package.beta
```他们可以共享父包初始化步骤

一旦父级初始化子级就可以由它们自己的模块锁来处理具体取决于包路径和查找器行为

## 42.13 全球进口锁定`_imp`CPython 通过以下方式公开低级导入锁函数`_imp`模块。```python id="8j3c5t"
import _imp

print(_imp.lock_held())
````_imp`模块包括以下功能:```text id="iqvyee"
acquire_lock
release_lock
lock_held
```这些是低级实现接口普通 Python 代码不应使用它们进行应用程序同步

它们的存在是为了进口机械和兼容性

不正确地使用它们可能会使过程陷入僵局或干扰导入系统

## 42.14 导入锁和自定义导入器

自定义查找器和加载器必须假设它们可以在导入同步下调用

发现者应该尽可能避免缓慢阻塞或可重入的行为

装载机的`exec_module`方法执行模块代码它可能会导入其他模块

装载机形状示例:```python id="viqfze"
class Loader:
    def create_module(self, spec):
        return None

    def exec_module(self, module):
        module.value = 42
```如果`exec_module`导入其他模块它参与相同的锁定图

定制进口商应避免在不一致的订单中获取不相关的锁否则他们可能会在 CPython 自己的导入锁逻辑之外造成死锁

## 42.15 导入锁定和`sys.modules`该锁保护模块初始化周围的关键部分。

重要操作包括:```text id="hu7f15"
checking sys.modules
creating the module
inserting the module
executing module code
removing module on failure
returning the initialized module
```最敏感的时刻是执行前的插入。```text id="fzh60s"
sys.modules[name] = module
exec_module(module)
```此顺序支持循环导入但这意味着其他代码可以在执行完成之前观察该模块

该锁确保导入相同名称的其他线程等待完成而不是再次执行它

## 42.16 导入失败及锁释放

如果导入失败必须释放锁

例子:```python id="3sf7se"
# broken.py
raise RuntimeError("boom")
try:
    import broken
except RuntimeError:
    pass
```失败后另一个线程不应该永远被阻塞

进口系统必须:```text id="rhkz4v"
release the module lock
clean up sys.modules when appropriate
propagate the exception
allow future import attempts
```稍后导入可以重试:```python id="36ur7b"
import broken
```并将再次执行该模块除非特殊机器故意留下失败的模块对象

## 42.17 本机扩展模块

扩展模块使导入锁定变得复杂

扩展模块可以:```text id="bc8bib"
run native initialization code
allocate process-global state
register types
import other modules
release the GIL
use static C variables
interact with subinterpreters
```导入锁可以防止同一扩展模块名称的并发初始化但扩展作者仍然需要仔细编写初始化代码

现代多阶段初始化有助于扩展模块存储每个模块对象的状态而不仅仅是在 C 全局变量中

## 42.18 子解释器

子解释器增加了另一个维度

每个解释器对于许多模块都有自己的模块字典状态但本机扩展模块可能仍然具有进程全局 C 状态除非另有设计

导入锁定必须相对于解释器状态和扩展状态来考虑

对于扩展作者来说这意味着:```text id="l90pr8"
avoid mutable process-global module state
prefer per-module state
support multi-phase initialization
consider subinterpreter isolation
```导入锁可以防止并发导入竞争但它不会自动使扩展模块状态在解释器之间隔离

## 42.19 导入锁定并重新加载`importlib.reload(module)`重新执行模块代码。```python id="q3ox9f"
import importlib
import config

importlib.reload(config)
```重新加载必须与导入状态协调因为它会改变现有的模块字典

在重新加载期间其他代码可能仍保留对以下内容的引用:```text id="8a8ikh"
the module object
old functions
old classes
old constants
old imported names
```导入锁可以防止模块名称同时发生冲突的重新加载/导入操作但重新加载在语义上仍然很棘手

重新加载不会更新所有外部引用

## 42.20 导入锁和模块状态可见性

导入锁控制导入操作它不会使模块全局变量具有事务性

如果模块代码在导入期间改变全局状态:```python id="uwbmge"
registry = []

registry.append("phase 1")
registry.append("phase 2")
ready = True
```涉及循环导入的代码可能会遵守:```text id="spya66"
registry exists
registry contains only phase 1
ready does not exist yet
```导入锁可以防止并发重复执行但部分初始化的模块通过循环导入仍然可见

## 42.21 避免导入时间竞争

应用程序代码应尽量减少导入时的突变

更喜欢这个:```python id="1kciks"
# service.py

class Service:
    ...

def create_service(config):
    return Service(config)
```对此:```python id="5wtu0a"
# service.py

config = load_config()
service = Service(config)
service.start()
```第二种形式在导入时执行工作它更难测试更难重新加载并且对导入顺序更敏感

导入时的工作通常应限于定义和廉价常量

## 42.22 安全顶级代码

安全的顶层模块代码通常包括:```text id="xbk1bu"
imports
constants
class definitions
function definitions
small table definitions
cheap feature detection
type aliases
```风险较高的顶级代码包括:```text id="ippw6v"
network calls
database connections
thread startup
event loop startup
large file reads
global registration with side effects
process-wide configuration
monkey patching
```导入锁会序列化初始化但昂贵或副作用大的导入仍然会损害启动和并发性

## 42.23 导入锁定和死导入

死导入是等待由于依赖循环或外部锁而无法完成导入的模块的导入

例子:```python id="jt6y2f"
# a.py
import threading
import b

lock = threading.Lock()
# b.py
import a
```简单的循环导入通常通过部分模块可见性来处理但是如果模块代码在导入期间获取外部锁启动线程或等待事件则更容易产生死锁

避免在导入时等待线程或外部锁

## 42.24 导入期间的自定义锁定

这是有风险的:```python id="626g6o"
# registry.py
import threading

lock = threading.Lock()

with lock:
    import plugin
```如果`plugin`进口`registry`并尝试获取相同的锁程序可能会死锁

更好的设计:```python id="9ez7pd"
# registry.py
import threading

lock = threading.Lock()
handlers = {}

def load_plugin(name):
    import importlib
    module = importlib.import_module(name)
    return module

def register(name, handler):
    with lock:
        handlers[name] = handler
```尽可能将应用程序锁保留在导入依赖周期之外

## 42.25 导入锁和插件系统

插件系统通常动态导入模块。```python id="iixpre"
import importlib

def load_plugins(names):
    for name in names:
        importlib.import_module(name)
```如果插件在导入时注册自己则加载会按模块名称序列化但仍会改变共享注册表

更安全的插件设计将导入与注册分开:```python id="d13tr6"
def load_plugin(name):
    module = importlib.import_module(name)
    return module.setup
```然后在受控阶段调用setup:```python id="p8oelv"
for setup in setups:
    setup(registry)
```这使得初始化顺序变得明确

## 42.26 导入锁定和启动性能

导入锁会影响线程程序的启动性能

如果许多线程延迟启动并导入依赖项则多个线程可能会阻塞相同的导入

一个常见的改进是在单线程启动期间导入主要依赖项:```python id="e6dmwr"
def main():
    import logging
    import database
    import server

    server.run()
```这会在工作线程开始之前预先加载初始化

对于服务器程序通常的模式是:```text id="iq8ig9"
configure process
import dependencies
initialize application
start worker threads or event loop
```避免让工作线程独立发现和导入大型依赖图

## 42.27 诊断导入锁定问题

导入锁定或导入周期问题的症状包括:```text id="m5z0iy"
program hangs during import
thread dump shows imports in multiple threads
partially initialized module errors
module attributes missing only during startup
plugin loading deadlocks
reload behaves inconsistently
```有用的调试工具:```python id="1mccj7"
import sys

print(sys.modules.get("module_name"))
import _imp

print(_imp.lock_held())
```对于挂起请使用线程转储。```python id="csvfsw"
import faulthandler

faulthandler.dump_traceback()
```或者从命令行启用故障处理程序:```bash id="7wjpm2"
python -X faulthandler app.py
```## 42.28 导入时序和锁争用

可以通过以下方式检查导入时间:```bash id="gr9wta"
python -X importtime -c "import your_package"
```该报告报告导入期间所花费的时间

它不直接显示锁争用但它有助于识别可能成为争用点的缓慢导入

缓慢的导入通常在发生时值得关注:```text id="7c79oz"
inside worker startup
inside request paths
inside plugin discovery
inside command-line entry points
inside test collection
```## 42.29 导入锁和异步代码

异步代码不会使导入异步

里面的 import 语句`async def`当协程的该部分执行时仍然同步运行。```python id="fg7lak"
async def handler():
    import heavy_module
    return heavy_module.run()
```第一次致电`handler`到达导入可能会在模块加载时阻止事件循环

对于异步服务器在启动事件循环之前导入大量依赖项或者将昂贵的初始化移至显式异步启动挂钩中

## 42.30 导入锁和多重处理

每个进程都有自己的解释器状态和自己的导入状态

通过多处理子进程单独导入模块

这对于产生新解释器的平台或启动方法更为重要

例如通过生成式进程创建子进程导入主模块

这就是多处理代码需要:```python id="w0j45j"
if __name__ == "__main__":
    main()
```如果没有防护子进程可能会在导入期间重新运行顶级代码

导入锁仅保护一个进程内的导入它不会跨进程同步导入

## 42.31 导入锁和字节码缓存写入

当CPython导入源模块时它可能会读或写`.pyc`文件

否则并发导入可能会围绕字节码缓存生成进行竞争

进口机械对此处理得很仔细尽管如此字节码缓存是一种优化而不是模块标识的来源

身份的来源是:```text id="ka9zk4"
module name in sys.modules
```字节码缓存仅存储已编译的代码对象以便以后更快地导入

## 42.32 导入锁和文件系统更改

导入锁不会使文件系统状态稳定

如果在导入时创建删除或替换文件行为仍然会令人困惑

示例:```text id="50enmw"
deployment replacing package files during process startup
tests generating modules dynamically
plugin files being written while discovery runs
zip archive changed while importing
```使用稳定的部署方法避免在正在运行的进程导入可导入代码时改变可导入代码

## 42.33 导入锁不是应用程序锁

不要使用导入作为同步机制

这是一个糟糕的模式:```python id="xdy9zr"
def initialize_once():
    import initialize_side_effects
```它依赖导入缓存来运行初始化一次

首选显式一次性初始化:```python id="q7vhsy"
_lock = threading.Lock()
_initialized = False

def initialize_once():
    global _initialized

    if _initialized:
        return

    with _lock:
        if _initialized:
            return
        do_initialization()
        _initialized = True
```导入缓存适用于模块应用程序的生命周期应该是明确的

## 42.34 设计规则:导入应该是无聊的

最安全的进口产品很无聊

无聊的导入:```text id="tjyc8p"
defines names
sets constants
imports dependencies
does no external work
finishes quickly
```令人惊讶的进口:```text id="8vcz4t"
starts threads
opens sockets
loads large models
registers global plugins
patches builtins
changes logging globally
reads mutable external config
```导入锁定可能会降低令人惊讶的导入的活跃度它不能让它们变得容易推理

## 42.35 最小导入锁定模型

紧凑型:```text id="iwck6e"
Thread imports module M.
Import system acquires lock for M.
If M is already initialized, return it.
If M is initializing in another thread, wait.
If this thread is already involved in the cycle, use partial module state.
Create and cache M before execution.
Execute M.
On success, mark M initialized and release lock.
On failure, clean up and release lock.
```该模型解释了为什么导入对于正常使用来说足够线程安全为什么循环导入仍然可以暴露不完整的模块以及为什么导入时副作用仍然很危险

## 42.36 要点

导入锁可防止同一模块名称的并发初始化

该锁保护模块加载而不是保护任意模块语义

GIL 和导入锁解决不同的问题

导入是可重入的因为模块可以在执行期间导入其他模块

循环导入是通过将模块插入到`sys.modules`在执行之前这可以公开部分初始化的模块

自定义导入器插件扩展模块子解释器重新加载和线程启动都使导入锁定变得更加重要

良好的应用程序设计可以使导入保持快速确定性并且基本上没有外部副作用