Using await inside Interlocked.Exchange crashes the C# compiler [closed]
Asked Answered
S

0

20

Ignore for a moment the absurdity of awaiting an Enumerable.Range call. It's just there to elicit the crash-y behavior. It just as easily could be a method that's doing some network IO to build a collection of value objects. (Indeed, this was where I saw the crash occur.)

If I comment out the Interlocked.Exchange line, the compiler doesn't crash.

public class Launcher
{
    private static IEnumerable<int> _foo;
    static void Main(string[] args)
    {
        DoWorkAsync().Wait();
    }

    private static async Task DoWorkAsync()
    {
        RefreshCache();
        foreach (var element in _foo)
        {
            Console.WriteLine(element);
        }

        Console.ReadLine();
    }

    private static async void RefreshCache()
    {
        Interlocked.Exchange(ref _foo, await Cache());
    }

    private static async Task<IEnumerable<int>> Cache()
    {
        return Enumerable.Range(0, 10);
    }
}

Changing RefreshCache() to this keeps the compiler from crashing:

private static async void RefreshCache()
{
    var foo = await Cache();
    Interlocked.Exchange(ref _foo, foo);
}

Edit:

An even simpler, self contained reproduce provided by @Servy

public class Worker
{
    private static int foo;
    private static async void DoWork()
    {
        DoNothing(ref foo, await Task.FromResult(0));
    }

    private static void DoNothing(ref int item, int other)
    {
    }
}
Snowber answered 20/5, 2015 at 18:55 Comment(20)
Reproduces on my machine as well. Pinged @JaredPar about it, he'll try reproducing it on roslyn. I'll try it as well.Regazzi
This doesn't re-produce on Roslyn. Interesting.Regazzi
@YuvalItzchakov Those were the exact same results I had, C#5.0 (VS2013) has the error, C#6.0 (VS2015) does not. What's even more interesting, is telling VS2015 to compile as C#5.0 does not throw the error.Legislature
I was just thinking on my way into work yesterday "I will never find a bug in any compiler, ever. It will always be PEBCAK."Snowber
Interlocked.Exchange introduces a memory barrier and thus is treated differently by the compiler. So I can understand why the first version wouldn't work. But no the second one.Locular
@Locular I don't understand why the introduction of a memory barrier would cause the compiler to crash. I don't even see why it would fail to work properly at runtime, but crashing the compiler (and not even successfully providing a compile time error) is unquestionably a bug.Canvass
@Locular Even though a memory barrier is introduced, I still don't see why the first should fail, specifically causing a compiler crash.Regazzi
Could this be a bug in the compiler related to the use of ref and not necessarily Interlocked.Exchange?Taurus
Using private static void DoNothing<T>(ref T item, T other) { } instead of Interlocked.Exchange also crashed the compiler. private static void DoNothing(ref IEnumerable<int> item, IEnumerable<int> other) { } also crashes the compiler, so it's not related to generics at all.Canvass
What happens if you create a method DoInterlock<T>(T foo) { Interlocked.Exchange(ref _foo, foo); }and just call it from the async method? Does it still crash?Locular
So I've been working on minimizing the example. There needs to be a call to a method with a ref parameter, a static field needs to be supplied as the reference to that field, the method needs to have a second (non-ref) argument that uses an await as the expression for the parameter.Canvass
Issue opened on GitHubRegazzi
The C# language does not know what Interlocked even is. It would be invalid for the compiler to special-case on it. This is not specific to Interlocked. I think it's funny that the C# team forgot to test await in combination with ref :)Salinometer
@Salinometer Indeed, I've already said that this can be reproduced without using Interlocked. It's not special. You can see my smallest repo in the github link (and now in the question, it seems).Canvass
@Canvass I noticed that and this was my first thought as well. I'm just pointing out for others how to correctly think about this bug.Salinometer
Edited the simpler repro to the question.Regazzi
@Salinometer Interestingly enough, it's not just the use of ref and await. If the referenced variable isn't a static field (i.e. being an instance field or a local) then it works fine, which seems particularly odd. So certain uses of await with a ref method work fine.Canvass
There is no question here at all, so this should be closed. You found a compiler bug that reproduces in one version of the compiler; report it to Microsoft, not to StackOverflow.Left
Well, it was an implied question: is this PEBCAK? Is it a configuration problem? Is it a problem because I just installed 2015CTP alongside 2013 just before doing this? etc. I completely expected it to be PEBCAK or a configuration conflict. But you're right, I'll close it up, because the other possibilities have been ruled out. I do wonder if it'll be fixed, though, for older compiler versions.Snowber
I'm voting to close this question as off-topic because it's not a question that has an answer: it's a compiler bug.Snowber

© 2022 - 2024 — McMap. All rights reserved.