Skip to content

Commit

Permalink
The use of ThreadLocal<bool> caused memory leaks in CyclicDependencyV…
Browse files Browse the repository at this point in the history
…alidator. Instead, it now stores thread ids. Fixes #956
  • Loading branch information
dotnetjunkie committed Sep 28, 2022
1 parent fc56e43 commit 592629e
Showing 1 changed file with 76 additions and 8 deletions.
84 changes: 76 additions & 8 deletions src/SimpleInjector/Internals/CyclicDependencyValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,32 @@

namespace SimpleInjector.Internals
{
using System.Threading;
using System;
using System.Collections.Generic;

/// <summary>
/// Allows verifying whether a given type has a direct or indirect dependency on itself. Verifying is done
/// by preventing recursive calls to an InstanceProducer. A CyclicDependencyValidator instance checks a
/// single InstanceProducer and therefore a single service type.
/// DESIGN: An instance or expression can be requested from multiple threads simultaniously, which means
/// that this class must be thread-safe but also prevent reporting false positives in cyclic dependencies
/// when called from multiple thread. Originally, this was done by wrapping a ThreadLocal{bool}, but this
/// lead to memory leaks, as in most cases the ThreadLocal{bool} couldn't be disposed. THat's why this
/// class now wraps a list of currently entered threads. Because the list is nulled when there are no
/// entering threads, this is a very memory-efficient solution.
/// </summary>
internal sealed class CyclicDependencyValidator
{
private readonly ThreadLocal<bool> producerVisited = new ThreadLocal<bool>();
private readonly InstanceProducer producer;

// If needed, we can do an extra optimization, which is to have an extra int field for the first
// entered thread. This prevents the list from having to be newed in most cases, as typically there
// is only on thread entering at the same time. This does complicate the code of this class though.
// Typically, however, we're not that concerned with producing garbage during the warmup phase.
// There are quite a few cases were we produce extensive garbage. Producing garbage in the happy
// path, however, is somethings we prevent at all costs.
private List<int>? enteredThreads;

internal CyclicDependencyValidator(InstanceProducer producer)
{
this.producer = producer;
Expand All @@ -23,16 +37,70 @@ internal CyclicDependencyValidator(InstanceProducer producer)
// Checks whether this is a recursive call (and thus a cyclic dependency) and throw in that case.
internal void Check()
{
if (this.producerVisited.Value)
lock (this)
{
if (this.IsCurrentThreadReentering())
{
throw new CyclicDependencyException(
this.producer, this.producer.Registration.ImplementationType);
}

this.MarkCurrentThreadAsEntering();
}
}

// Resets the validator to its initial state.
internal void Reset()
{
lock (this)
{
this.MarkCurrentThreadAsLeaving();
}
}

private bool IsCurrentThreadReentering()
{
if (this.enteredThreads != null)
{
int currentThreadId = Environment.CurrentManagedThreadId;

foreach (int threadId in this.enteredThreads)
{
if (threadId == currentThreadId) return true;
}
}

return false;
}

private void MarkCurrentThreadAsEntering()
{
int currentThreadId = Environment.CurrentManagedThreadId;

if (this.enteredThreads is null)
{
throw new CyclicDependencyException(
this.producer, this.producer.Registration.ImplementationType);
this.enteredThreads = new(1);
}

this.producerVisited.Value = true;
this.enteredThreads.Add(currentThreadId);
}

// Resets the validator to its initial state.
internal void Reset() => this.producerVisited.Value = false;
private void MarkCurrentThreadAsLeaving()
{
if (this.enteredThreads != null)
{
this.enteredThreads.Remove(Environment.CurrentManagedThreadId);

if (this.enteredThreads.Count == 0)
{
// Nullify the list. Although this causes the production of way more
// garbage during the warm-up phase when the instance producer is
// depended upon a lot, it ensures that the least amount of memory
// is used when the warmup phase is complete and all expression trees
// have been compiled.
this.enteredThreads = null;
}
}
}
}
}

0 comments on commit 592629e

Please sign in to comment.