Improved threadsafety of Application.MainLoop.AddTimeout

This commit is contained in:
Thomas Nind
2022-04-29 09:50:09 +01:00
parent 7a12d7c85e
commit 735996f5e1
2 changed files with 66 additions and 14 deletions

View File

@@ -51,6 +51,7 @@ namespace Terminal.Gui {
}
internal SortedList<long, Timeout> timeouts = new SortedList<long, Timeout> ();
object timeoutsLockToken = new object ();
internal List<Func<bool>> idleHandlers = new List<Func<bool>> ();
/// <summary>
@@ -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 <c>false</c> 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;
@@ -170,18 +171,20 @@ namespace Terminal.Gui {
void RunTimers ()
{
long now = DateTime.UtcNow.Ticks;
var copy = timeouts;
timeouts = new SortedList<long, Timeout> ();
foreach (var t in copy) {
var k = t.Key;
var timeout = t.Value;
if (k < now) {
if (timeout.Callback (this))
AddTimeout (timeout.Span, timeout);
} else {
lock (timeouts) {
timeouts.Add (k, timeout);
lock (timeoutsLockToken) {
long now = DateTime.UtcNow.Ticks;
var copy = timeouts;
timeouts = new SortedList<long, Timeout> ();
foreach (var t in copy) {
var k = t.Key;
var timeout = t.Value;
if (k < now) {
if (timeout.Callback (this))
AddTimeout (timeout.Span, timeout);
} else {
timeouts.Add (k, timeout);
}
}
}

View File

@@ -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 >= 100 * 100 * 2);
}
}
}