Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

pow function blocking all threads with ThreadPoolExecutor

Even though the first example about the ThreadPoolExecutor uses the pow function ( https://docs.python.org/3/library/concurrent.futures.html ) it seems that using pow abnormally blocks all threads when called. See code below.

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)

I expected the output:

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

but the program stops running after "starting pow...." giving the result:

block 1
non 1
block 2
non 2
block 3
non 3
block 4
Starting pow....
like image 437
Miojo_Esperto Avatar asked Jul 05 '26 14:07

Miojo_Esperto


2 Answers

Your python implementation (probably CPython) must have a Global Interpreter Lock (GIL)

pow is a native Python function, which doesn't release the GIL when called, efficiently blocking all executions while it's running and it's running quite a while.

If you want non-blocking, use ProcessPoolExecutor instead. A separate process means 2 separate GILs and not blocking. It has the exact same interface. It also has more constraints as it requires parameters to be picklable (no convenient shared memory like with threads)

Note that this computation probably never ends or ends up with "out of memory error": since it's power between integers with a big power value, so it doesn't overflow like floats would do but computes and computes, creating bigger and bigger integers each time

Estimated numbers of digits for this number is roughly:

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

10^14 digits that is more than any current computer can handle memory wise, and also CPU-wise.

like image 133
Jean-François Fabre Avatar answered Jul 07 '26 02:07

Jean-François Fabre


This is because of Python's global interpreter lock (GIL).

If you look at the bytecode for your function:

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)

which looks like this:

  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

You'll notice that the pow call happens at 62 as a single bytecode instruction. The GIL can only be handed off between bytecodes, so since pow(363,100000000000000) takes a long time to execute, the other thread doesn't get a chance to run during it.

like image 31
Josh Karpel Avatar answered Jul 07 '26 04:07

Josh Karpel



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!