dotnet / runtime

.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.
https://docs.microsoft.com/dotnet/core/
MIT License
15.48k stars 4.76k forks source link

[API Proposal]: Ability to get and set `ThreadLocal<T>.Value` in one method call #105607

Open rickbrew opened 4 months ago

rickbrew commented 4 months ago

Background and motivation

In my app I occasionally have to get into the weeds to do some serious cycle-trimming optimizations. Sometimes that means making use of methods like CollectionsMarshal.GetValueOrAddDefault<TKey, TValue>(...). That is, you need to do two things with a storage slot somewhere, but you only want to pay the lookup cost once. On hot code paths this can make a difference!

I'm currently writing some code that uses ThreadLocal<T> on a hot path where I need to retrieve the Value and then immediately clear it. This means I have to pay the slot lookup twice cost, once for get_Value and then again for set_Value. I don't get the impression that ThreadLocal<T> is a bottleneck, but this app is intended to be fast even on low-spec PCs and every cycle helps.

In a similar vein, a bool TryGetValue(out TValue value) method could be useful, eliminating the need to call both IsValueCreated and then Value in order to differentiate between a null Value versus a never-set Value.

API Proposal

namespace System.Threading;

public class ThreadLocal<T>
{
    // Gets the old per-thread Value, then sets it to newValue, then returns the old per-thread Value
    TValue ExchangeValue(TValue newValue);
}

API Usage

private readonly ThreadLocal<Bitmap?> perThreadBitmap = new();

public void BeginRenderingTask(SizeInt32 bitmapSize)
{
    Debug.Assert(this.perThreadBitmap.Value is null);
    this.perThreadBitmap.Value = new Bitmap(bitmapSize);
}

public void PerformRenderingTask()
{    
    ...
}

// Without this API:
public void EndRenderingTask()
{
    using Bitmap? bitmap = this.perThreadBitmap.Value;
    Debug.Assert(buffer is not null);
    this.perThreadBitmap.Value = null;
}

// With this API:
public void EndRenderingTask()
{
    using Bitmap? bitmap = this.perThreadBitmap.ExchangeValue(null);
    Debug.Assert(buffer is not null);
}

Alternative Designs

I'm sure there are several different ways to articulate this API. Whether or not it should be in ThreadLocal<T> or ThreadingMarshal, or wherever, is not something I have a strong opinion on.

Risks

No response

dotnet-policy-service[bot] commented 4 months ago

Tagging subscribers to this area: @mangod9 See info in area-owners.md if you want to be subscribed.

rickbrew commented 4 months ago

Worth pointing out that it is possible to work around this by using something like ThreadLocal<StrongBox<T>>. This does cost an extra allocation per thread.

colejohnson66 commented 4 months ago

Would a ref property be better? ref T ValueRef { get; }