Skip to content

Consider replacing SynchronizedDictionary with ConcurrentDictionary #743

Open

Description

@Youssef1313

I think the BCL ConcurrentDictionary should be able to perform much better than the current SynchronizedDictionary implementation.

Using Moq, which depends on Castle.Core, I observe threadpool starvation in a parallelized test run. The threadpool threads are mostly in stack traces similar to this one:

Image
  • Using ConcurrentDictionary might lock less. I guess it creates some buckets per the hash code, and the different buckets get different locks, which allows for more concurrent access without breaking the type's thread safety.
  • Or, if you have other approaches where you can eliminate the need to lock altogether, that also might be a good option.

Activity

  1. Youssef1313 commented on May 2, 2026

    @Youssef1313
    Author

    FYI @kzu as well

  2. stakx commented on May 2, 2026

    @stakx
    Member

    Hi @Youssef1313, thanks for looking into it.

    I'm open to switch to a standard FCL type, but I think it would be good to have some kind of before/after benchmark showing that ConcurrentDictionary actually does remove a significant bottleneck. Could you provide something along that line? (E.g. a small BenchmarkDotNet project that we can drop into the Castle.Core solution file and run against DynamicProxy both before and after the change?)

  3. Youssef1313 commented on May 4, 2026

    @Youssef1313
    Author

    See the last comment on #744. Somehow my changes actually broke the thread-safety guarantees currently on master. I don't think I'll get into investigating this deeper, but generally speaking, I think there is some sort of threadpool starvation under parallel loads that needs to be somehow addressed.

  4. stakx commented on May 4, 2026

    @stakx
    Member

    @Youssef1313, when it comes to DynamicProxy thread safety, this here is quite possibly the most important code location:

    var proxyType = Scope.TypeCache.GetOrAdd(GetCacheKey(), cacheKey =>
    {
    notFoundInTypeCache = true;
    Logger.DebugFormat("No cached proxy type was found for target type {0}.", targetType.FullName);
    EnsureOptionsOverrideEqualsAndGetHashCode();
    var name = Scope.NamingScope.GetUniqueName("Castle.Proxies." + targetType.Name + "Proxy");
    return GenerateType(name, Scope.NamingScope.SafeSubScope());
    });

    This TypeCache.GetOrAdd must be exclusively locking, i. e. only one thread at a time may go ahead and generate its type. Almost all of DynamicProxy's code generation is kicked off here. Most other code may then silently assume that there is no other concurrent code generation going on, because we guarantee that at the above site. This is why you see those {GetOrAdd,AddOrUpdate}WithoutTakingLock (emphasis on "without taking lock") method calls almost everywhere else.

    If I remember correctly (but I could be wrong), ConcurrentDictionary.GetOrAdd has slightly different semantics: several factory lambdas may execute concurrently (at the same time) which, if true, would break DynamicProxy.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions