dotnet / csharpstandard

Working space for ECMA-TC49-TG2, the C# standard committee.
Creative Commons Attribution 4.0 International
714 stars 84 forks source link

Await guarantees #322

Open vladd opened 3 years ago

vladd commented 3 years ago

Consider the following simple program:

using System;
using System.Threading.Tasks;

int a = 1;
await Task.Yield();
Console.WriteLine(a);

I'm trying to prove (based on the standard) that the output will be exactly 1 and not e. g. 0 due to threading issues.

The parts of the code on the different sides of await can run in different threads, so there is no guarantee of data dependence preservation through the first bullet in §8.10 Execution order.

There is no explicit synchronization (lock, etc.), no threads are started (the second thread is a thread pool thread), no volatile fields are explicitly accessed, so nothing from the third bullet seems to be directly applicable.

It's clear that the synchronization must be done in some form, perhaps by ThreadPool.QueueUserWorkItem, but I couldn't find anything in the standard which guarantees this behavior.

I assume that the guarantees like these are very basic and must be listed in the standard and not forward to the concrete implementation details or CLR specs.

jskeet commented 3 years ago

I assume that the guarantees like these are very basic and must be listed in the standard and not forward to the concrete implementation details or CLR specs.

Unfortunately the C# standard has relatively little to say around data dependence. We would like an update CLI standard, with clear paths from one standard to another (to avoid duplication, but maintain clarity) but I'm afraid that's not the case at the moment.

vladd commented 3 years ago

@jskeet I understand that without updating CLI specs it could be not possible to express the needed guarantees in C# standard. However await it a pure language construct, having no CLI counterpart, so I hoped that the guarantees like the one being discussed can be given directly (perhaps together with the definition of async methods).

By the way, it it theoretically possible to construct a custom awaitable which would move the continuation execution into a different thread but make no synchronization at all (thus the value in Console.WriteLine would indeed be possibly 0)? Or is it taken care of on the async state machine level?

jskeet commented 3 years ago

@vladd: I'd need to think about that. But I really don't think we're likely to address this any time soon, I'm afraid - catching up in terms of regular C# features definitely takes priority.

vladd commented 3 years ago

There's no doubt that the regular features have higher priority. I'd however like the issue to stay: the need for multithreading guarantees shouldn't be forgotten.

jskeet commented 3 years ago

We discussed this in our meeting last week. There was some discussion around whether await actually introduces any additional requirements for the language, and ended up agreeing that it did (IIRC). We believe there are more interesting examples here which should be considered. Here's one to start with:

class MemoryTest
{
    private int value;

    public async Task Check()
    {
        value = 1;
        await Task.Yield();
        await IncrementAsync();
        if (value != 2)
        {
            throw new Exception("Unexpected value");
        }
    }

    private async Task IncrementAsync()
    {
        await Task.Yield();
        value++;
    }
}

Here, the interesting questions are:

(And obviously, this is more in terms of "is it guaranteed to" rather than "does it do it at all".)

We don't expect to do anything yet, but we do consider this an interesting topic for further work in the future.

vladd commented 3 years ago

@jskeet In my actual code, I'm using a defensive lock for the cases like in your last example, because for me it was a kind of "unprotected multithreaded access to shared data". The case of local variables was (implicitly) special. Perhaps the lock can be safely elided.