Skip to content

Does CPython >= 3.10, <=3.12 support a thread-safe increment? #134212

Open
@tvvister

Description

@tvvister

Documentation

There is no information about it in docs. But the code demonstrates that increment works correctly in multithreading context.

import concurrent.futures


def inc():
    global count
    for _ in range(1000):
        count += 1

for threads_count in range(1, 101):
    for _ in range(3):
        count = 0
        with concurrent.futures.ThreadPoolExecutor(max_workers=threads_count) as exe:
            for _ in range(10000):
                exe.submit(inc)

        print(f"{threads_count=}, {count=}")


import dis
dis.dis(inc)
output

threads=1, count=10000000
threads=1, count=10000000
threads=1, count=10000000
threads=2, count=10000000
threads=2, count=10000000
threads=2, count=10000000
threads=3, count=10000000
threads=3, count=10000000
threads=3, count=10000000
threads=4, count=10000000
threads=4, count=10000000
threads=4, count=10000000
threads=5, count=10000000
threads=5, count=10000000
threads=5, count=10000000
threads=6, count=10000000
threads=6, count=10000000
threads=6, count=10000000
threads=7, count=10000000
threads=7, count=10000000
threads=7, count=10000000
threads=8, count=10000000
threads=8, count=10000000
threads=8, count=10000000
threads=9, count=10000000
threads=9, count=10000000
threads=9, count=10000000
threads=10, count=10000000
threads=10, count=10000000
threads=10, count=10000000
threads=11, count=10000000
threads=11, count=10000000
threads=11, count=10000000
threads=12, count=10000000
threads=12, count=10000000
threads=12, count=10000000
threads=13, count=10000000
threads=13, count=10000000
threads=13, count=10000000
threads=14, count=10000000
threads=14, count=10000000
threads=14, count=10000000
threads=15, count=10000000
threads=15, count=10000000
threads=15, count=10000000
threads=16, count=10000000
threads=16, count=10000000
threads=16, count=10000000
threads=17, count=10000000
threads=17, count=10000000
threads=17, count=10000000
threads=18, count=10000000
threads=18, count=10000000
threads=18, count=10000000
threads=19, count=10000000
threads=19, count=10000000
threads=19, count=10000000
threads=20, count=10000000
threads=20, count=10000000
threads=20, count=10000000
threads=21, count=10000000
threads=21, count=10000000
threads=21, count=10000000
threads=22, count=10000000
threads=22, count=10000000
threads=22, count=10000000
threads=23, count=10000000
threads=23, count=10000000
threads=23, count=10000000
threads=24, count=10000000
threads=24, count=10000000
threads=24, count=10000000
threads=25, count=10000000
threads=25, count=10000000
threads=25, count=10000000
threads=26, count=10000000
threads=26, count=10000000
threads=26, count=10000000
threads=27, count=10000000
threads=27, count=10000000
threads=27, count=10000000
threads=28, count=10000000
threads=28, count=10000000
threads=28, count=10000000
threads=29, count=10000000
threads=29, count=10000000
threads=29, count=10000000
threads=30, count=10000000
threads=30, count=10000000
threads=30, count=10000000
threads=31, count=10000000
threads=31, count=10000000
threads=31, count=10000000
threads=32, count=10000000
threads=32, count=10000000
threads=32, count=10000000
threads=33, count=10000000
threads=33, count=10000000
threads=33, count=10000000
threads=34, count=10000000
threads=34, count=10000000
threads=34, count=10000000
threads=35, count=10000000
threads=35, count=10000000
threads=35, count=10000000
threads=36, count=10000000
threads=36, count=10000000
threads=36, count=10000000
threads=37, count=10000000
threads=37, count=10000000
threads=37, count=10000000
threads=38, count=10000000
threads=38, count=10000000
threads=38, count=10000000
threads=39, count=10000000
threads=39, count=10000000
threads=39, count=10000000
threads=40, count=10000000
threads=40, count=10000000
threads=40, count=10000000
threads=41, count=10000000
threads=41, count=10000000
threads=41, count=10000000
threads=42, count=10000000
threads=42, count=10000000
threads=42, count=10000000
threads=43, count=10000000
threads=43, count=10000000
threads=43, count=10000000
threads=44, count=10000000
threads=44, count=10000000
threads=44, count=10000000
threads=45, count=10000000
threads=45, count=10000000
threads=45, count=10000000
threads=46, count=10000000
threads=46, count=10000000
threads=46, count=10000000
threads=47, count=10000000
threads=47, count=10000000
threads=47, count=10000000
threads=48, count=10000000
threads=48, count=10000000
threads=48, count=10000000
threads=49, count=10000000
threads=49, count=10000000
threads=49, count=10000000
threads=50, count=10000000
threads=50, count=10000000
threads=50, count=10000000
threads=51, count=10000000
threads=51, count=10000000
threads=51, count=10000000
threads=52, count=10000000
threads=52, count=10000000
threads=52, count=10000000
threads=53, count=10000000
threads=53, count=10000000
threads=53, count=10000000
threads=54, count=10000000
threads=54, count=10000000
threads=54, count=10000000
threads=55, count=10000000
threads=55, count=10000000
threads=55, count=10000000
threads=56, count=10000000
threads=56, count=10000000
threads=56, count=10000000
threads=57, count=10000000
threads=57, count=10000000
threads=57, count=10000000
threads=58, count=10000000
threads=58, count=10000000
threads=58, count=10000000
threads=59, count=10000000
threads=59, count=10000000
threads=59, count=10000000
threads=60, count=10000000
threads=60, count=10000000
threads=60, count=10000000
threads=61, count=10000000
threads=61, count=10000000
threads=61, count=10000000
threads=62, count=10000000
threads=62, count=10000000
threads=62, count=10000000
threads=63, count=10000000
threads=63, count=10000000
threads=63, count=10000000
threads=64, count=10000000
threads=64, count=10000000
threads=64, count=10000000
threads=65, count=10000000
threads=65, count=10000000
threads=65, count=10000000
threads=66, count=10000000
threads=66, count=10000000
threads=66, count=10000000
threads=67, count=10000000
threads=67, count=10000000
threads=67, count=10000000
threads=68, count=10000000
threads=68, count=10000000
threads=68, count=10000000
threads=69, count=10000000
threads=69, count=10000000
threads=69, count=10000000
threads=70, count=10000000
threads=70, count=10000000
threads=70, count=10000000
threads=71, count=10000000
threads=71, count=10000000
threads=71, count=10000000
threads=72, count=10000000
threads=72, count=10000000
threads=72, count=10000000
threads=73, count=10000000
threads=73, count=10000000
threads=73, count=10000000
threads=74, count=10000000
threads=74, count=10000000
threads=74, count=10000000
threads=75, count=10000000
threads=75, count=10000000
threads=75, count=10000000
threads=76, count=10000000
threads=76, count=10000000
threads=76, count=10000000
threads=77, count=10000000
threads=77, count=10000000
threads=77, count=10000000
threads=78, count=10000000
threads=78, count=10000000
threads=78, count=10000000
threads=79, count=10000000
threads=79, count=10000000
threads=79, count=10000000
threads=80, count=10000000
threads=80, count=10000000
threads=80, count=10000000
threads=81, count=10000000
threads=81, count=10000000
threads=81, count=10000000
threads=82, count=10000000
threads=82, count=10000000
threads=82, count=10000000
threads=83, count=10000000
threads=83, count=10000000
threads=83, count=10000000
threads=84, count=10000000
threads=84, count=10000000
threads=84, count=10000000
threads=85, count=10000000
threads=85, count=10000000
threads=85, count=10000000
threads=86, count=10000000
threads=86, count=10000000
threads=86, count=10000000
threads=87, count=10000000
threads=87, count=10000000
threads=87, count=10000000
threads=88, count=10000000
threads=88, count=10000000
threads=88, count=10000000
threads=89, count=10000000
threads=89, count=10000000
threads=89, count=10000000
threads=90, count=10000000
threads=90, count=10000000
threads=90, count=10000000
threads=91, count=10000000
threads=91, count=10000000
threads=91, count=10000000
threads=92, count=10000000
threads=92, count=10000000
threads=92, count=10000000
threads=93, count=10000000
threads=93, count=10000000
threads=93, count=10000000
threads=94, count=10000000
threads=94, count=10000000
threads=94, count=10000000
threads=95, count=10000000
threads=95, count=10000000
threads=95, count=10000000
threads=96, count=10000000
threads=96, count=10000000
threads=96, count=10000000
threads=97, count=10000000
threads=97, count=10000000
threads=97, count=10000000
threads=98, count=10000000
threads=98, count=10000000
threads=98, count=10000000
threads=99, count=10000000
threads=99, count=10000000
threads=99, count=10000000
threads=100, count=10000000
threads=100, count=10000000
threads=100, count=10000000
11 0 LOAD_GLOBAL 0 (range)
2 LOAD_CONST 1 (1000)
4 CALL_FUNCTION 1
6 GET_ITER
>> 8 FOR_ITER 6 (to 22)
10 STORE_FAST 0 (_)

12 12 LOAD_GLOBAL 1 (count)
14 LOAD_CONST 2 (1)
16 INPLACE_ADD
18 STORE_GLOBAL 1 (count)
20 JUMP_ABSOLUTE 4 (to 8)

11 >> 22 LOAD_CONST 0 (None)
24 RETURN_VALUE

Looks like '+=' isn't an atomic op. But stiil I have a correct result.

Can we just use it as a guarantee or it is just an accent coincidence?

Activity

nineteendo

nineteendo commented on May 19, 2025

@nineteendo
Contributor

That might be because of the global interpreter lock.

tvvister

tvvister commented on May 19, 2025

@tvvister
Author

That might be because of the global interpreter lock.

I ve tried the same with python 3.7.3. Results are different.

threads=1, count=10000000
threads=1, count=10000000
threads=1, count=10000000
threads=2, count=7320515
threads=2, count=6738504
threads=2, count=6988554
threads=3, count=4260950
threads=3, count=5614856
threads=3, count=4990384
threads=4, count=4277691
threads=4, count=4863640
threads=4, count=3803397
5 0 SETUP_LOOP 24 (to 26)
2 LOAD_GLOBAL 0 (range)
4 LOAD_CONST 1 (1000)
6 CALL_FUNCTION 1
8 GET_ITER
>> 10 FOR_ITER 12 (to 24)
12 STORE_FAST

Both times GIL works, I am not sure what it is, new GIL or interpreter behavior. The point is just to understand. Maybe describe it in docs more detailed.

Any case firstly it worth to figure out can I be confedent of it

nineteendo

nineteendo commented on May 19, 2025

@nineteendo
Contributor

Using this code: https://stackoverflow.com/a/1718843

$ brew install python-freethreading
$ python3.13t -VV   
Python 3.13.3 experimental free-threading build (main, Apr  8 2025, 13:54:08) [Clang 15.0.0 (clang-1500.1.0.2.5)]
$ python3.13t tmp.py
Traceback (most recent call last):
  File "/Users/wannes/Desktop/tmp.py", line 17, in <module>
    assert i == 1000000, i
           ^^^^^^^^^^^^
AssertionError: 105144
nineteendo

nineteendo commented on May 19, 2025

@nineteendo
Contributor

https://discuss.python.org/t/atomic-and-thread-safe-in-python-world/51575/3:

I personally think reasoning about it in terms of bytecodes is more confusion than its worth. Sure, if an operation spans multiple bytecodes, certainly don’t treat it as atomic. [...] (Also the details of when bytecodes can drop the GIL changes, e.g. in Python 3.10 bpo-29988: Only check evalbreaker after calls and on backwards egdes. by markshannon · Pull Request #18334 · python/cpython · GitHub means a lot of trivial attempts to demonstrate non-atomicity will no longer reproduce in Python 3.10 and newer, but this isn’t guaranteed by the language)

changed the title [-]Does CPython >= 3.10 support a thread-safe increment ?[/-] [+]Does CPython >= 3.10, <=3.12 support a thread-safe increment?[/+] on May 19, 2025
tvvister

tvvister commented on May 19, 2025

@tvvister
Author

Using this code: https://stackoverflow.com/a/1718843

$ brew install python-freethreading
$ python3.13t -VV
Python 3.13.3 experimental free-threading build (main, Apr 8 2025, 13:54:08) [Clang 15.0.0 (clang-1500.1.0.2.5)]
$ python3.13t tmp.py
Traceback (most recent call last):
File "/Users/wannes/Desktop/tmp.py", line 17, in
assert i == 1000000, i
^^^^^^^^^^^^
AssertionError: 105144

Seems free threading python doesn't have "GIL safety". Poor programmers!
By the way, I updated the topic

tvvister

tvvister commented on May 19, 2025

@tvvister
Author

Using this code: https://stackoverflow.com/a/1718843

$ brew install python-freethreading
$ python3.13t -VV
Python 3.13.3 experimental free-threading build (main, Apr 8 2025, 13:54:08) [Clang 15.0.0 (clang-1500.1.0.2.5)]
$ python3.13t tmp.py
Traceback (most recent call last):
File "/Users/wannes/Desktop/tmp.py", line 17, in
assert i == 1000000, i
^^^^^^^^^^^^
AssertionError: 105144

I rewrote the example from SO. My question is about int '+=' ('-=') operation.

import time
class something(object):
    def __init__(self,c):
        self.c=c
    def inc(self):
        time.sleep(0.001)
        self.c += 1
        time.sleep(0.001)


x = something(0)
import threading
threads = []
for _ in range(10000):
    thread = threading.Thread(target=x.inc)
    threads.append(thread)
    thread.start()
for th in threads:
    th.join()
print(x.c)

Now it is working correct way. BTW, it is interesting, what about the results of usual version of python 3.13, which doesn't support real multithreading, but works with old fashion multithreading through GIL ?

gaogaotiantian

gaogaotiantian commented on May 19, 2025

@gaogaotiantian
Member

It does not support a thread-safe increment. It happens to be correct. The reason is 3.10 changed where GIL could be acquired. People should not rely on this "feature" and it could be changed anytime in the future. Free-threaded version of course gave different results :)

tvvister

tvvister commented on May 19, 2025

@tvvister
Author

It does not support a thread-safe increment. It happens to be correct. The reason is 3.10 changed where GIL could be acquired. People should not rely on this "feature" and it could be changed anytime in the future. Free-threaded version of course gave different results :)

I guess it is not changed from 3.10x.

I've checked on python 3.8.5 and 3.13.3 (not free threading of couse). and works the way, one can think that GIL support thread-safe incrementaion for int.

I've check not only on my machine, but on online interpeters as well. The conclusion is that it is not random.

gaogaotiantian

gaogaotiantian commented on May 19, 2025

@gaogaotiantian
Member

one can think that GIL support thread-safe incrementaion for int.

One should not think that. It's not random. Like I said, a specific change in 3.10 result in that behavior. There's a different between a supported behavior, and a non-random behavior. A supported behavior is something we promise, that would not be easily changed in future versions, which you can rely on. A non-random behavior is something that happens to be like that because of some private implementation detail, which could be changed anytime in the future.

tvvister

tvvister commented on May 21, 2025

@tvvister
Author

one can think that GIL support thread-safe incrementaion for int.

One should not think that. It's not random. Like I said, a specific change in 3.10 result in that behavior. There's a different between a supported behavior, and a non-random behavior. A supported behavior is something we promise, that would not be easily changed in future versions, which you can rely on. A non-random behavior is something that happens to be like that because of some private implementation detail, which could be changed anytime in the future.

There are thoughts about the topic.

Python is not fast, so it's frequently used as glue to call optimized code like numpy.
Making thread-safe increment, I would prefer to avoid an extra lock and use GIL only. We can investigate if the decision is more efficient or not. Let's say - yes, but does it mean that I use the most efficient solution? May I assume that python is not a room for this kind of optimization?
What about the same question but for the future of python?

upd.
I'v made a check:

import concurrent.futures
from threading import Lock

lock = Lock()
def inc():
    global count
    for _ in range(1000):
        with lock:
            count += 1
        # count += 1

for threads_count in range(2, 10):
    for _ in range(3):
        count = 0
        with concurrent.futures.ThreadPoolExecutor(max_workers=threads_count) as exe:
            for _ in range(10000):
                exe.submit(inc)

        print(f"{threads_count}, {count}")

This code with threading.Lock:

real 1m6,378s
user 1m6,398s
sys 0m0,964s

and without:

real 0m20,874s
user 0m20,741s
sys 0m0,462s

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    docsDocumentation in the Doc dir

    Projects

    Status

    Todo

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

      Does CPython >= 3.10, <=3.12 support a thread-safe increment? · Issue #134212 · python/cpython

      Follow Lee on X/Twitter - Father, Husband, Serial builder creating AI, crypto, games & web tools. We are friends :) AI Will Come To Life!

      Check out: eBank.nz (Art Generator) | Netwrck.com (AI Tools) | Text-Generator.io (AI API) | BitBank.nz (Crypto AI) | ReadingTime (Kids Reading) | RewordGame | BigMultiplayerChess | WebFiddle | How.nz | Helix AI Assistant