Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Task<T>.Result and string concatenation

Tags:

c#

.net

c#-5.0

I was playing with async / await when I came across the following:

class C
{
    private static string str;

    private static async Task<int> FooAsync()
    {
        str += "2";
        await Task.Delay(100);
        str += "4";
        return 5;
    }

    private static void Main(string[] args)
    {
        str = "1";
        var t = FooAsync();
        str += "3";

        str += t.Result; // Line X

        Console.WriteLine(str);
    }
}

I expected the result to be "12345", but it was "1235". Somehow '4' was eaten up.

If I split Line X into:

int i = t.Result;
str += i;

Then the expected "12345" results.

Why is it so? (Using VS2012)

like image 432
Nubcase Avatar asked May 24 '13 20:05

Nubcase


3 Answers

Why is it so? (Using VS2012)

You're running this in a console application, which means there is no current synchronization context.

As such, the portion of the FooAsync() method after the await runs in a separate thread. When you do str += t.Result you're effectively making a race condition between the += 4 call, and the += t.Result. This is because string += is not an atomic operation.

If you were to run the same code within a Windows Forms or WPF application, the synchronization context would be captured and used for the += "4", which means this would all run on the same thread, and you wouldn't see this issue.

like image 62
Reed Copsey Avatar answered Oct 02 '22 17:10

Reed Copsey


C# statements of the form x += y; are expanded to x = x + y; at compile time.

str += t.Result; becomes str = str + t.Result;, where str is read before getting t.Result. At this point in time, str is "123". When the continuation in FooAsync runs, it modifies str and then returns 5. So str is now "1234". But then the value of str that was read before the continuation in FooAsync ran (which is "123") is concatenated with 5 to assign to str the value "1235".

When you break it into two statements, int i = t.Result; str += i;, this behavior can't happen.

like image 28
Timothy Shields Avatar answered Oct 02 '22 15:10

Timothy Shields


This is a race condition. You're not properly synchronizing access to the shared variable between the two threads of execution that you have.

Your code is probably doing something like this:

  • sets the string to be "1"
  • call FooAsync
  • append 2
  • when the await is called the main method continues executing, the callback in FooAsync will run in the thread pool; from here on out things are indeterminate.
  • the main thread appends 3 to the string

Then we get to the interesting line:

str += t.Result;

Here it's broken up into several smaller operations. It will first get the current value of str. At this point in time the async method (in all probability) hasn't finished yet, so it'll be "123". It then waits for the task to complete (because Result forces a blocking wait) and adds the result of the task, in this case 5 to the end of the string.

The async callback will have grabbed and re-set str after the main thread had already grabbed the current value of str, and then it will overwrite str without it ever being read as the main thread is soon to overwrite it.

like image 36
Servy Avatar answered Oct 02 '22 15:10

Servy