I am actively trying to get race condition and cause problem in calculation for demonstration purposes but i can't achieve such problem simply.
My tought process was to create a counter variable, reach it from diffrent threads and async functions (i did not tried mp since it pauses process) and increase it by one.
Running 3 instances of for loops in range x and increasing the counter by 1 in each loop i expected to get value lower then 3x due to race condition.
I did not use locks, but i still get 3x value each run. Does it due to GIL update? i have tried with python versions 3.10 / 3.11 / 3.13. What should i do to get race condition simple structure
my code to get race condition
import threading
import asyncio
def multithreading_race_condition():
counter2 = 0
def increment():
nonlocal counter2
for _ in range(10000):
counter2 = counter2 + 1
threads = [threading.Thread(target=increment) for _ in range(3)]
for t in threads:
t.start()
for t in threads:
t.join()
print(f"Multithreading Final Counter: {counter2}")
async def asyncio_race_condition():
counter3 = 0
async def increment():
nonlocal counter3
for _ in range(10000):
counter3 = counter3 + 1
tasks = [asyncio.create_task(increment()) for _ in range(3)]
await asyncio.gather(*tasks)
print(f"Asyncio Final Counter: {counter3}")
def main():
print("\nMultithreading Example:")
multithreading_race_condition()
print("\nAsyncio Example:")
asyncio.run(asyncio_race_condition())
if __name__ == "__main__":
main()
my output is
Multithreading Example:
Multithreading Final Counter: 30000
Asyncio Example:
Asyncio Final Counter: 30000
The time between fetching the value of counter
, incrementing it and reassigning it back is very short.
counter = counter + 1
In order to force a race condition for demonstration purposes, you should extend that window perhaps with sleep
tmp = counter
time.sleep(random.random())
counter = tmp + 1
I would also increase the number of concurrent tasks to give more chance for an issue to pop up.
This should dramatically illustrate things.
import threading
import asyncio
import time
import random
def multithreading_race_condition():
counter = 0
iteration_count = 10
task_count = 10
def increment():
nonlocal counter
for _ in range(iteration_count):
tmp = counter
time.sleep(random.random())
counter = tmp + 1
threads = [threading.Thread(target=increment) for _ in range(task_count)]
for t in threads:
t.start()
for t in threads:
t.join()
print(f"Multithreading Final Counter was {counter} expected {iteration_count * task_count}")
async def asyncio_race_condition():
counter = 0
iteration_count = 10
task_count = 10
async def increment():
nonlocal counter
for _ in range(iteration_count):
tmp = counter
await asyncio.sleep(random.random())
counter = tmp + 1
tasks = [asyncio.create_task(increment()) for _ in range(task_count)]
await asyncio.gather(*tasks)
print(f"Asyncio Final Counter was {counter} expected {iteration_count * task_count}")
def main():
print("\nMultithreading Example:")
multithreading_race_condition()
print("\nAsyncio Example:")
asyncio.run(asyncio_race_condition())
if __name__ == "__main__":
main()
That should likely give you something like:
Multithreading Example:
Multithreading Final Counter was 10 expected 100
Asyncio Example:
Asyncio Final Counter was 10 expected 100
Dramatically illustrating a race condition.
Here is a second way to demonstrate a race using code a little more like what you have.
Note that if counter
is loaded first you more likely will find a race and if loaded last you are likely not to find one. You can use dis
to see how that might be true:
import dis
test = """
counter = counter + 1 + (time.sleep(random.random()) or 0)
"""
print(dis.dis(test))
test2 = """
counter = 1 + (time.sleep(random.random()) or 0) + counter
"""
print(dis.dis(test2))
So, here are two examples that difffer only in the order of operations on either side of a long opperation. The first is very likely to demonstrate a race condition while the second is unlikely (though not impossible) to do so.
import threading
import time
import random
def multithreading_race_condition():
counter = 0
def increment():
nonlocal counter
for _ in range(10):
counter = counter + 1 + (time.sleep(random.random()) or 0)
#counter = 1 + (time.sleep(random.random()) or 0) + counter
threads = [threading.Thread(target=increment) for _ in range(10)]
for t in threads:
t.start()
for t in threads:
t.join()
print(f"Multithreading Final Counter: {counter}")
def multithreading_no_race_condition():
counter = 0
def increment():
nonlocal counter
for _ in range(10):
#counter = counter + 1 + (time.sleep(random.random()) or 0)
counter = 1 + (time.sleep(random.random()) or 0) + counter
threads = [threading.Thread(target=increment) for _ in range(10)]
for t in threads:
t.start()
for t in threads:
t.join()
print(f"Multithreading Final Counter: {counter}")
def main():
print("\nMultithreading Example with race:")
multithreading_race_condition()
print("\nMultithreading Example with (probably) no race:")
multithreading_no_race_condition()
if __name__ == "__main__":
main()
I am guessing that will give you:
Multithreading Example with race:
Multithreading Final Counter: 10
Multithreading Example with (probably) no race:
Multithreading Final Counter: 100