Search code examples
pythonpython-3.xmultithreadingasynchronousrace-condition

I am trying to cause race condition for demonstration purposes but fail to fail


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

Solution

  • 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