Is there a more efficient threading lock?

Michael Speer knomenet at gmail.com
Mon Feb 27 01:26:59 EST 2023


https://stackoverflow.com/questions/69993959/python-threads-difference-for-3-10-and-others

https://github.com/python/cpython/commit/4958f5d69dd2bf86866c43491caf72f774ddec97

it's a quirk of implementation. the scheduler currently only checks if it
needs to release the gil after the POP_JUMP_IF_FALSE, POP_JUMP_IF_TRUE,
JUMP_ABSOLUTE, CALL_METHOD, CALL_FUNCTION, CALL_FUNCTION_KW, and
CALL_FUNCTION_EX opcodes.

>>> import code
>>> import dis
>>> dis.dis( code.update_x_times )
 10           0 LOAD_GLOBAL              0 (range)
              2 LOAD_FAST                0 (xx)
              4 CALL_FUNCTION            1
##### GIL CAN RELEASE HERE #####
              6 GET_ITER
        >>    8 FOR_ITER                 6 (to 22)
             10 STORE_FAST               1 (_)
 12          12 LOAD_GLOBAL              1 (vv)
             14 LOAD_CONST               1 (1)
             16 INPLACE_ADD
             18 STORE_GLOBAL             1 (vv)
             20 JUMP_ABSOLUTE            4 (to 8)
##### GIL CAN RELEASE HERE (after JUMP_ABSOLUTE points the instruction
counter back to FOR_ITER, but before the interpreter actually jumps to
FOR_ITER again) #####
 10     >>   22 LOAD_CONST               0 (None)
             24 RETURN_VALUE
>>>

due to this, this section:
 12          12 LOAD_GLOBAL              1 (vv)
             14 LOAD_CONST               1 (1)
             16 INPLACE_ADD
             18 STORE_GLOBAL             1 (vv)

is effectively locked/atomic on post-3.10 interpreters, though this is
neither portable nor guaranteed to stay that way into the future


On Sun, Feb 26, 2023 at 10:19 PM Michael Speer <knomenet at gmail.com> wrote:

> I wanted to provide an example that your claimed atomicity is simply
> wrong, but I found there is something different in the 3.10+ cpython
> implementations.
>
> I've tested the code at the bottom of this message using a few docker
> python images, and it appears there is a difference starting in 3.10.0
>
> python3.8
> EXPECTED 2560000000
> ACTUAL   84533137
> python:3.9
> EXPECTED 2560000000
> ACTUAL   95311773
> python:3.10 (.8)
> EXPECTED 2560000000
> ACTUAL   2560000000
>
> just to see if there was a specific sub-version of 3.10 that added it
> python:3.10.0
> EXPECTED 2560000000
> ACTUAL   2560000000
>
> nope, from the start of 3.10 this is happening
>
> the only difference in the bytecode I see is 3.10 adds SETUP_LOOP and
> POP_BLOCK around the for loop
>
> I don't see anything different in the long c code that I would expect
> would cause this.
>
> AFAICT the inplace add is null for longs and so should revert to the
> long_add that always creates a new integer in x_add
>
> another test
> python:3.11
> EXPECTED 2560000000
> ACTUAL   2560000000
>
> I'm not sure where the difference is at the moment. I didn't see anything
> in the release notes given a quick glance.
>
> I do agree that you shouldn't depend on this unless you find a written
> guarantee of the behavior, as it is likely an implementation quirk of some
> kind
>
> --[code]--
>
> import threading
>
> UPDATES = 10000000
> THREADS = 256
>
> vv = 0
>
> def update_x_times( xx ):
>     for _ in range( xx ):
>         global vv
>         vv += 1
>
> def main():
>     tts = []
>     for _ in range( THREADS ):
>         tts.append( threading.Thread( target = update_x_times, args =
> (UPDATES,) ) )
>
>     for tt in tts:
>         tt.start()
>
>     for tt in tts:
>         tt.join()
>
>     print( 'EXPECTED', UPDATES * THREADS )
>     print( 'ACTUAL  ', vv )
>
> if __name__ == '__main__':
>     main()
>
> On Sun, Feb 26, 2023 at 6:35 PM Jon Ribbens via Python-list <
> python-list at python.org> wrote:
>
>> On 2023-02-26, Barry Scott <barry at barrys-emacs.org> wrote:
>> > On 25/02/2023 23:45, Jon Ribbens via Python-list wrote:
>> >> I think it is the case that x += 1 is atomic but foo.x += 1 is not.
>> >
>> > No that is not true, and has never been true.
>> >
>> >:>>> def x(a):
>> >:...    a += 1
>> >:...
>> >:>>>
>> >:>>> dis.dis(x)
>> >   1           0 RESUME                   0
>> >
>> >   2           2 LOAD_FAST                0 (a)
>> >               4 LOAD_CONST               1 (1)
>> >               6 BINARY_OP               13 (+=)
>> >              10 STORE_FAST               0 (a)
>> >              12 LOAD_CONST               0 (None)
>> >              14 RETURN_VALUE
>> >:>>>
>> >
>> > As you can see there are 4 byte code ops executed.
>> >
>> > Python's eval loop can switch to another thread between any of them.
>> >
>> > Its is not true that the GIL provides atomic operations in python.
>>
>> That's oversimplifying to the point of falsehood (just as the opposite
>> would be too). And: see my other reply in this thread just now - if the
>> GIL isn't making "x += 1" atomic, something else is.
>> --
>> https://mail.python.org/mailman/listinfo/python-list
>>
>


More information about the Python-list mailing list