New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
race condition in threading._newname() #56075
Comments
The _newname() function has no locking. It is called from the new thread constructor. Such constructor is executed within parent thread. So, when several threads create new threads simultaneously, there can be race condition, leading to the same name for two (or even more) threads. 8<-------------------------------- >>> import threading
>>> import dis
>>> dis.dis(threading._newname)
403 0 LOAD_GLOBAL 0 (_counter)
3 LOAD_CONST 1 (1)
6 BINARY_ADD
7 STORE_GLOBAL 0 (_counter)
404 10 LOAD_FAST 0 (template)
13 LOAD_GLOBAL 0 (_counter)
16 BINARY_MODULO
17 RETURN_VALUE
>>>
8< The race condition can be achieved between BINARY_ADD and STORE_GLOBAL. Several threads can do BINARY_ADD before the first one will do STORE_GLOBAL. All racing threads will get the same name. 8<-------------------------------- $ for i in `seq 0 100`; do python thread_test.py |\
awk -F Thread- '{print $2}' |\
grep -vE '^$' |\
sort |\
uniq -c -d; done
2 35
2 12
...
8< As you see, there are cases when several threads can get same name. Proposals: use thread-safe increment counter (with atomic get-increment) like itertools.counter() (that does not release GIL) |
Do you want to provide a patch? |
Ok, patch attached. Patch made for Python: 2.6 |
Any news? I hope, the change is trivial enough… |
Are you sure that counter.next() cannot release the GIL? Remember that any DECREF can trigger the garbage collector and execute arbitrary code... |
counter.next() is a C routine and it is atomic from Python's point of view — if I understand right. The test shows that original threading.py leads to a (rare) race here, while with counter object there is no race condition. |
I think the patch is correct. FWIW, my style is to prebind the next method, making the counter completely self-contained (like a closure): +_counter = itertools.count().next
def _newname(template="Thread-%d"):
global _counter
- _counter = _counter + 1
- return template % _counter
+ return template % _counter() |
With or without Raymond's suggested change, "global _counter" is not necessary anymore. |
New changeset a6b4add67168 by R David Murray in branch '2.7': New changeset a6906b9e21d5 by R David Murray in branch '3.4': New changeset e9afcce9a154 by R David Murray in branch 'default': |
I committed a version of this that uses Raymond's suggestion and also makes sure that the first thread is named Thread-1 as it has been in the past (there was a test failure without that fix, but I also think it makes sense for it to be Thread-1 since one would normally think of the main thread as thread 0). I did not try to turn the test script into a test case because it would take a long time to run if it is to have a reasonable chance of displaying the bug, and it would never be a consistent failure. Thanks, Peter; sorry it took so long to get this committed. |
Note: these values reflect the state of the issue at the time it was migrated and might not reflect the current state.
Show more details
GitHub fields:
bugs.python.org fields:
The text was updated successfully, but these errors were encountered: