pow 函数使用 ThreadPoolExecutor 阻塞所有线程

pow function blocking all threads with ThreadPoolExecutor

尽管有关 ThreadPoolExecutor 的第一个示例使用了 pow 函数 (https://docs.python.org/3/library/concurrent.futures.html),但使用 pow 似乎会在调用时异常阻塞所有线程。请参阅下面的代码。

from concurrent.futures import ThreadPoolExecutor
import time

executor = ThreadPoolExecutor(max_workers=16)

def blocking():
    i = 0
    while True:
        time.sleep(1)
        i+=1
        print("block",i)
        if i>3:
            print("Starting pow....")
            break
    block= pow(363,100000000000000)
    return True

def non_blocking():
    i = 0
    while True:
        time.sleep(1)
        i+=1
        print("non",i)

    return True

f1 = executor.submit(blocking)
f2 = executor.submit(non_blocking)

我期望输出:

block 1
non 1
block 2
non 2
block 3
non 3
block 4
Starting pow....
non 4
non 5
non 6
non 7
non 8

但程序在 "starting pow...." 后停止 运行 给出结果:

block 1
non 1
block 2
non 2
block 3
non 3
block 4
Starting pow....
pow(363,100000000000000)

如果计算量很大。它需要时间来处理并给出结果

简单计算:

>>> import time
>>> def test(power):
...     start = time.time()
...     pow(363, power)
...     return time.time() - start
...
>>> for power in range(1,11):
...     print(test(pow(10, power)))
...
6.198883056640625e-06
2.1457672119140625e-06
3.0517578125e-05
0.0009307861328125
0.0421295166015625
1.7541508674621582 #This is a (363, 1000000)

时间呈指数增长

您的 python 实现(可能是 CPython)必须具有 Global Interpreter Lock (GIL)

pow 是一个原生的 Python 函数,它在被调用时不会释放 GIL,在 运行 和 运行 时有效地阻止所有执行而

如果您想要 non-blocking,请改用 ProcessPoolExecutor。一个单独的进程意味着 2 个独立的 GIL 并且没有阻塞。它具有完全相同的界面。它也有更多的限制,因为它要求参数是 picklable(没有像线程那样方便的共享内存)

请注意,此计算可能永远不会结束或以 "out of memory error" 结束:因为它是具有 big 幂值的整数之间的幂,所以它不会溢出浮点数可以,但计算和计算,每次都会创建越来越大的整数

这个数字的估计位数大致是:

>>> math.log10(363)*100000000000000
255990662503611.25

10^14 位数字超过了当前任何计算机的内存处理能力,而且 CPU-wise.

这是因为 Python 的全局解释器锁 (GIL)。

如果您查看函数的字节码:

import time

def blocking():
    i = 0
    while True:
        time.sleep(1)
        i+=1
        print("block",i)
        if i>3:
            print("Starting pow....")
            break
    block= pow(363,100000000000000)
    return True

import dis

dis.dis(blocking)

看起来像这样:

  4           0 LOAD_CONST               1 (0)
              2 STORE_FAST               0 (i)

  5           4 SETUP_LOOP              50 (to 56)

  6     >>    6 LOAD_GLOBAL              0 (time)
              8 LOAD_METHOD              1 (sleep)
             10 LOAD_CONST               2 (1)
             12 CALL_METHOD              1
             14 POP_TOP

  7          16 LOAD_FAST                0 (i)
             18 LOAD_CONST               2 (1)
             20 INPLACE_ADD
             22 STORE_FAST               0 (i)

  8          24 LOAD_GLOBAL              2 (print)
             26 LOAD_CONST               3 ('block')
             28 LOAD_FAST                0 (i)
             30 CALL_FUNCTION            2
             32 POP_TOP

  9          34 LOAD_FAST                0 (i)
             36 LOAD_CONST               4 (3)
             38 COMPARE_OP               4 (>)
             40 POP_JUMP_IF_FALSE        6

 10          42 LOAD_GLOBAL              2 (print)
             44 LOAD_CONST               5 ('Starting pow....')
             46 CALL_FUNCTION            1
             48 POP_TOP

 11          50 BREAK_LOOP
             52 JUMP_ABSOLUTE            6
             54 POP_BLOCK

 12     >>   56 LOAD_GLOBAL              3 (pow)
             58 LOAD_CONST               6 (363)
             60 LOAD_CONST               7 (100000000000000)
             62 CALL_FUNCTION            2
             64 STORE_FAST               1 (block)

 13          66 LOAD_CONST               8 (True)
             68 RETURN_VALUE

您会注意到 pow 调用作为单个字节码指令发生在 62。 GIL 只能在字节码之间传递,因此由于 pow(363,100000000000000) 需要很长时间才能执行,其他线程在执行期间没有机会 运行。