Bug 24486 - Memory leak using async/await
Summary: Memory leak using async/await
Status: RESOLVED NOT_REPRODUCIBLE
Alias: None
Product: Runtime
Classification: Mono
Component: GC ()
Version: 3.8.0
Hardware: Other Linux
: --- normal
Target Milestone: ---
Assignee: Bugzilla
URL:
Depends on:
Blocks:
 
Reported: 2014-11-13 04:12 UTC by Michael Meijer
Modified: 2015-03-05 10:30 UTC (History)
7 users (show)

Tags:
Is this bug a regression?: ---
Last known good build:

Notice (2018-05-24): bugzilla.xamarin.com is now in read-only mode.

Please join us on Visual Studio Developer Community and in the Xamarin and Mono organizations on GitHub to continue tracking issues. Bugzilla will remain available for reference in read-only mode. We will continue to work on open Bugzilla bugs, copy them to the new locations as needed for follow-up, and add the new items under Related Links.

Our sincere thanks to everyone who has contributed on this bug tracker over the years. Thanks also for your understanding as we make these adjustments and improvements for the future.


Please create a new report on GitHub or Developer Community with your current version information, steps to reproduce, and relevant error messages or log files if you are hitting an issue that looks similar to this resolved bug and you do not yet see a matching new report.

Related Links:
Status:
RESOLVED NOT_REPRODUCIBLE

Description Michael Meijer 2014-11-13 04:12:04 UTC
On Mono 3.X.Y on Windows (PC) and Linux (Ubuntu 14.04 on Beagle Bone Black and VMs) we've experienced memory leakage when using async/await on even the most trivial/stripped-down use cases:

static void Main(string[] args)
{
    var max = 5;
    var tasks = new Task[max];
    for (var i = 0; i < max; ++i) tasks[i] = Loop(i);

    Task.WhenAll(tasks).Wait();
}

static async Task Loop(int i)
{
    while (true)
    {
        await Task.Yield();
    }
}

We've tried both official releases of Mono and custom builds of Mono against latest source code. Also we've used both Mono and C# .NET compilers. The leakage causes the OS to kill the Mono process at some point in our applications. The leakage seems to be worse when more state is kept along the asynchronous call chain.

Please note that the same code in .NET on Windows shows no memory leakage.
Comment 1 Marek Safar 2014-11-18 05:30:29 UTC
I cannot see any leak here, using slightly improved test

using System.Threading.Tasks;
using System.Threading;
using System;

class X
{
	static void Main (string[] args)
	{
		var max = 5;
		var tasks = new Task[max];
		for (var i = 0; i < max; ++i)
			tasks [i] = Loop (i);

		Task.WhenAll (tasks).Wait ();
	}

	static int c;

	static async Task Loop (int i)
	{
		while (true) {
			if (Interlocked.Increment (ref c) % 100000 == 0)
				Console.WriteLine (c + ": " + GC.GetTotalMemory (true));

			await Task.Yield ();
		}
	}
}
Comment 2 Michael Meijer 2014-11-24 05:55:05 UTC
Marek you are right. Unfortunately the example was too simplistic to trigger the behavior, my mistake.

Below I added the simplified code we use to periodically execute an asynchronous task. A function is executed for every window, awaiting the remainder of the window if the function completes before the next window. 

This one does lead to the memory leak. It eventually lead to an out-of-memory exception on my machine. Although not sure if it can be explained by allocated heap memory. Moreover it seems to execute significantly slower on Mono than it does on .NET. Please verify if you can reproduce.

using System;
using System.Threading;
using System.Threading.Tasks;

class Program
{
    public static async Task PeriodicallyExecuteAsync(
        Func<CancellationToken, Task<bool>> func,
        TimeSpan interval,
        CancellationToken cancellationToken = default(CancellationToken))
    {
        var done = false;

        while (!done)
        {
            // stop when cancelled
            if (cancellationToken.IsCancellationRequested)
            {
                break;
            }

            // wait until "current" window is passed
            var now = DateTimeOffset.UtcNow;
            var next = new DateTimeOffset((now.Ticks / interval.Ticks) * interval.Ticks + interval.Ticks, now.Offset);
            var remainder = (next - now); // may need to add a few milliseconds to compensate for scehduler resolution?
                
            if (remainder > TimeSpan.Zero)
            {
                await Task.Delay(remainder, cancellationToken);
            }

            // window specific (but linked) cancellation token source
            using (var childTokenSource = CancellationTokenSource.CreateLinkedTokenSource(new[] { cancellationToken }))
            {
                // optionally: kill after window
                //childTokenSource.CancelAfter(interval);

                // awaiting function, result determines continuation of periodicity
                done = await func(childTokenSource.Token);
            }
        }
    }

    static void Main(string[] args)
    {
        var max = 1000;
        var tasks = new Task[max];
        var r = 0L;

        for (var i = 0; i < max; ++i)
        {
            tasks[i] = PeriodicallyExecuteAsync(
                async cancellationToken =>
                    {
                        if (Interlocked.Increment(ref r) % 10000 == 0)
                        {
                            Console.WriteLine("{0}: {1}", r, GC.GetTotalMemory(true));
                        }

                        await Task.Yield();

                        return false;
                    },
                TimeSpan.FromMilliseconds(50));
        }

        Task.WhenAll(tasks).Wait();
    }
}
Comment 3 Marek Safar 2014-11-28 08:12:07 UTC
I cannot reproduce the issue using your test code. When running on my Mac machine with mono 3.10 or 3.12 I get no leak

10000: 5500384
20000: 5349144
....
7900000: 5381456
7910000: 5630032
7920000: 5488960
7930000: 5640400
7940000: 5456240
Comment 4 Michael Meijer 2014-12-01 04:56:48 UTC
We've not seen the behavior on Mac, only on Mono for Windows and Linux (Ubuntu). Those are the platforms we work with/target our software.
Comment 5 Alexander Kyte 2015-02-23 12:55:23 UTC
10000: 4921952
20000: 5041648
30000: 5141968
40000: 5092144
50000: 5098176
60000: 5219112
70000: 5074480
80000: 5132888
90000: 5072248
100000: 5168096
110000: 5092160
120000: 5194480
130000: 5048688
140000: 5176128
150000: 5119344
160000: 5189936
170000: 5099856
180000: 5214232
190000: 5104912
200000: 5182288
210000: 5065408
220000: 5179144
.....
.....
8720000: 5182488
8730000: 5076904
8740000: 5006352
8750000: 5140448
^C
vagrant@precise32 ~ $ uname -a
Linux precise32 3.2.0-23-generic-pae #36-Ubuntu SMP Tue Apr 10 22:19:09 UTC 2012 i686 i686 i386 GNU/Linux

I cannot reproduce on ubuntu.
Comment 6 Michael Meijer 2015-03-05 10:30:35 UTC
Cannot reproduce.