Skip to content
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

Data racing when using BINARY_OP and async functions #125784

Closed
yanyongyu opened this issue Oct 21, 2024 · 2 comments
Closed

Data racing when using BINARY_OP and async functions #125784

yanyongyu opened this issue Oct 21, 2024 · 2 comments
Labels
interpreter-core (Objects, Python, Grammar, and Parser dirs) topic-asyncio type-bug An unexpected behavior, bug, or error

Comments

@yanyongyu
Copy link

yanyongyu commented Oct 21, 2024

Bug report

Bug description:

I have encountered some racing issue when using binary_op and async functions like this:

import asyncio

result = 0


async def some_func(data: int):
    await asyncio.sleep(1)
    return data


async def test(data: int):
    global result

    result += await some_func(data)


async def main():
    tasks = [asyncio.create_task(test(i)) for i in range(10)]
    await asyncio.gather(*tasks)

    print(result)


asyncio.run(main())

In the test function, a binary operation (addition) is performed on the global variable result. Multiple tasks may attempt to read and update the value of result. But, it seems the binary_op += load_global result first and then await the right-side value. The atomicity of the addition binary operation is not guaranteed. This causes the data override.

This issue also occurred when using dict, update the dict value and some other cases.

Example
import asyncio

result = {"value": 0}


async def some_func(data: int):
    await asyncio.sleep(1)
    return data


async def test(data: int):
    result["value"] += await some_func(data)


async def main():
    tasks = [asyncio.create_task(test(i)) for i in range(10)]
    await asyncio.gather(*tasks)

    print(result)


asyncio.run(main())

I'm not sure if this operation is as-designed or if this is documented behavior. Maybe this error case should be highlighted in the documentation.

cc @RF-Tar-Railt, @ProgramRipper

CPython versions tested on:

3.12, 3.13

Operating systems tested on:

Linux

@yanyongyu yanyongyu added the type-bug An unexpected behavior, bug, or error label Oct 21, 2024
@tomasr8 tomasr8 added topic-asyncio interpreter-core (Objects, Python, Grammar, and Parser dirs) labels Oct 21, 2024
@chenxuan353
Copy link

chenxuan353 commented Oct 21, 2024

Can also be reproduced when passed as a parameter

import asyncio
async def some_func(data: int):
    await asyncio.sleep(1)
    return data

async def test(result: dict, data: int):
    result["value"] += await some_func(data)

async def main():
    result = {"value": 0}
    tasks = [asyncio.create_task(test(result, i)) for i in range(10)]
    await asyncio.gather(*tasks)

    print(result)

asyncio.run(main())

@JelleZijlstra
Copy link
Member

This behavior is documented in the language reference (https://docs.python.org/3/reference/simple_stmts.html#augmented-assignment-statements):

Unlike normal assignments, augmented assignments evaluate the left-hand side before evaluating the right-hand side. For example, a[i] += f(x) first looks-up a[i], then it evaluates f(x) and performs the addition, and lastly, it writes the result back to a[i].

You get a data race because you first read, then await (giving other tasks an opportunity to run), then write. To avoid data races in asyncio code, you cannot await inside the augmented assignment. Instead, first compute your result and then mutate the global:

value = await some_func(data)
result += value

@JelleZijlstra JelleZijlstra closed this as not planned Won't fix, can't repro, duplicate, stale Oct 21, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
interpreter-core (Objects, Python, Grammar, and Parser dirs) topic-asyncio type-bug An unexpected behavior, bug, or error
Projects
Status: Done
Development

No branches or pull requests

4 participants