diff --git a/Terminal.Gui/Core/MainLoop.cs b/Terminal.Gui/Core/MainLoop.cs index 3812212d4..7c4076eec 100644 --- a/Terminal.Gui/Core/MainLoop.cs +++ b/Terminal.Gui/Core/MainLoop.cs @@ -51,6 +51,7 @@ namespace Terminal.Gui { } internal SortedList timeouts = new SortedList (); + object timeoutsLockToken = new object (); internal List> idleHandlers = new List> (); /// @@ -117,7 +118,7 @@ namespace Terminal.Gui { void AddTimeout (TimeSpan time, Timeout timeout) { - lock (timeouts) { + lock (timeoutsLockToken) { var k = (DateTime.UtcNow + time).Ticks; while (timeouts.ContainsKey (k)) { k = (DateTime.UtcNow + time).Ticks; @@ -159,7 +160,7 @@ namespace Terminal.Gui { /// This method also returns false if the timeout is not found. public bool RemoveTimeout (object token) { - lock (timeouts) { + lock (timeoutsLockToken) { var idx = timeouts.IndexOfValue (token as Timeout); if (idx == -1) return false; @@ -171,8 +172,17 @@ namespace Terminal.Gui { void RunTimers () { long now = DateTime.UtcNow.Ticks; - var copy = timeouts; - timeouts = new SortedList (); + SortedList copy; + + // lock prevents new timeouts being added + // after we have taken the copy but before + // we have allocated a new list (which would + // result in lost timeouts or errors during enumeration) + lock (timeoutsLockToken) { + copy = timeouts; + timeouts = new SortedList (); + } + foreach (var t in copy) { var k = t.Key; var timeout = t.Value; @@ -180,11 +190,12 @@ namespace Terminal.Gui { if (timeout.Callback (this)) AddTimeout (timeout.Span, timeout); } else { - lock (timeouts) { + lock (timeoutsLockToken) { timeouts.Add (k, timeout); } } } + } void RunIdle () diff --git a/UnitTests/ApplicationTests.cs b/UnitTests/ApplicationTests.cs index 2ceceefbd..84b658861 100644 --- a/UnitTests/ApplicationTests.cs +++ b/UnitTests/ApplicationTests.cs @@ -1286,5 +1286,54 @@ namespace Terminal.Gui.Core { var cultures = Application.GetSupportedCultures (); Assert.Equal (cultures.Count, Application.SupportedCultures.Count); } + + [Fact, AutoInitShutdown] + public void TestAddManyTimeouts () + { + int delegatesRun = 0; + int numberOfThreads = 100; + int numberOfTimeoutsPerThread = 100; + + + // start lots of threads + for (int i = 0; i < numberOfThreads; i++) { + + var myi = i; + + Task.Run (() => { + Task.Delay (100).Wait (); + + // each thread registers lots of 1s timeouts + for(int j=0;j< numberOfTimeoutsPerThread; j++) { + + Application.MainLoop.AddTimeout (TimeSpan.FromSeconds(1), (s) => { + + // each timeout delegate increments delegatesRun count by 1 every second + Interlocked.Increment (ref delegatesRun); + return true; + }); + } + + // if this is the first Thread created + if (myi == 0) { + + // let the timeouts run for a bit + Task.Delay (5000).Wait (); + + // then tell the application to quuit + Application.MainLoop.Invoke (() => Application.RequestStop ()); + } + }); + } + + // blocks here until the RequestStop is processed at the end of the test + Application.Run (); + + // undershoot a bit to be on the safe side. The 5000 ms wait allows the timeouts to run + // a lot but all those timeout delegates could end up going slowly on a slow machine perhaps + // so the final number of delegatesRun might vary by computer. So for this assert we say + // that it should have run at least 2 seconds worth of delegates + Assert.True (delegatesRun >= numberOfThreads * numberOfTimeoutsPerThread * 2); + } } }