Skip to content

Commit b0f578c

Browse files
committed
Remove class constraint from Interlocked.{Compare}Exchange
Today `Interlocked.CompareExchange<T>` and `Interlocked.Exchange<T>` support only reference type `T`s. Now that we have corresponding {Compare}Exchange methods that support types of size 1, 2, 4, and 8, we can remove the constraint and support any `T` that's either a reference type, a primitive type, or an enum type, making the generic overload more useful and avoiding consumers needing to choose less-than-ideal types just because of the need for atomicity with Interlocked.{Compare}Exchange.
1 parent 90d4c7d commit b0f578c

File tree

7 files changed

+131
-139
lines changed

7 files changed

+131
-139
lines changed

src/coreclr/System.Private.CoreLib/src/System/Threading/Interlocked.CoreCLR.cs

Lines changed: 1 addition & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Diagnostics;
45
using System.Diagnostics.CodeAnalysis;
56
using System.Runtime.CompilerServices;
67
using System.Runtime.InteropServices;
@@ -102,21 +103,6 @@ public static long Exchange(ref long location1, long value)
102103
[return: NotNullIfNotNull(nameof(location1))]
103104
[MethodImpl(MethodImplOptions.InternalCall)]
104105
private static extern object? ExchangeObject([NotNullIfNotNull(nameof(value))] ref object? location1, object? value);
105-
106-
// The below whole method reduces to a single call to Exchange(ref object, object) but
107-
// the JIT thinks that it will generate more native code than it actually does.
108-
109-
/// <summary>Sets a variable of the specified type <typeparamref name="T"/> to a specified value and returns the original value, as an atomic operation.</summary>
110-
/// <param name="location1">The variable to set to the specified value.</param>
111-
/// <param name="value">The value to which the <paramref name="location1"/> parameter is set.</param>
112-
/// <returns>The original value of <paramref name="location1"/>.</returns>
113-
/// <exception cref="NullReferenceException">The address of location1 is a null pointer.</exception>
114-
/// <typeparam name="T">The type to be used for <paramref name="location1"/> and <paramref name="value"/>. This type must be a reference type.</typeparam>
115-
[Intrinsic]
116-
[return: NotNullIfNotNull(nameof(location1))]
117-
[MethodImpl(MethodImplOptions.AggressiveInlining)]
118-
public static T Exchange<T>([NotNullIfNotNull(nameof(value))] ref T location1, T value) where T : class? =>
119-
Unsafe.As<T>(Exchange(ref Unsafe.As<T, object?>(ref location1), value));
120106
#endregion
121107

122108
#region CompareExchange
@@ -183,29 +169,6 @@ public static long CompareExchange(ref long location1, long value, long comparan
183169
[MethodImpl(MethodImplOptions.InternalCall)]
184170
[return: NotNullIfNotNull(nameof(location1))]
185171
private static extern object? CompareExchangeObject(ref object? location1, object? value, object? comparand);
186-
187-
// Note that getILIntrinsicImplementationForInterlocked() in vm\jitinterface.cpp replaces
188-
// the body of the following method with the following IL:
189-
// ldarg.0
190-
// ldarg.1
191-
// ldarg.2
192-
// call System.Threading.Interlocked::CompareExchange(ref Object, Object, Object)
193-
// ret
194-
// The workaround is no longer strictly necessary now that we have Unsafe.As but it does
195-
// have the advantage of being less sensitive to JIT's inliner decisions.
196-
197-
/// <summary>Compares two instances of the specified reference type <typeparamref name="T"/> for reference equality and, if they are equal, replaces the first one.</summary>
198-
/// <param name="location1">The destination, whose value is compared by reference with <paramref name="comparand"/> and possibly replaced.</param>
199-
/// <param name="value">The value that replaces the destination value if the comparison by reference results in equality.</param>
200-
/// <param name="comparand">The object that is compared by reference to the value at <paramref name="location1"/>.</param>
201-
/// <returns>The original value in <paramref name="location1"/>.</returns>
202-
/// <exception cref="NullReferenceException">The address of <paramref name="location1"/> is a null pointer.</exception>
203-
/// <typeparam name="T">The type to be used for <paramref name="location1"/>, <paramref name="value"/>, and <paramref name="comparand"/>. This type must be a reference type.</typeparam>
204-
[Intrinsic]
205-
[MethodImpl(MethodImplOptions.AggressiveInlining)]
206-
[return: NotNullIfNotNull(nameof(location1))]
207-
public static T CompareExchange<T>(ref T location1, T value, T comparand) where T : class? =>
208-
Unsafe.As<T>(CompareExchange(ref Unsafe.As<T, object?>(ref location1), value, comparand));
209172
#endregion
210173

211174
#region Add

src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Interlocked.cs

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,6 @@ public static long CompareExchange(ref long location1, long value, long comparan
3636
#endif
3737
}
3838

39-
[Intrinsic]
40-
[return: NotNullIfNotNull(nameof(location1))]
41-
[MethodImpl(MethodImplOptions.AggressiveInlining)]
42-
public static T CompareExchange<T>(ref T location1, T value, T comparand) where T : class?
43-
{
44-
return Unsafe.As<T>(CompareExchange(ref Unsafe.As<T, object?>(ref location1), value, comparand));
45-
}
46-
4739
[Intrinsic]
4840
[MethodImpl(MethodImplOptions.AggressiveInlining)]
4941
[return: NotNullIfNotNull(nameof(location1))]
@@ -92,16 +84,6 @@ public static long Exchange(ref long location1, long value)
9284
#endif
9385
}
9486

95-
[Intrinsic]
96-
[MethodImpl(MethodImplOptions.AggressiveInlining)]
97-
[return: NotNullIfNotNull(nameof(location1))]
98-
public static T Exchange<T>([NotNullIfNotNull(nameof(value))] ref T location1, T value) where T : class?
99-
{
100-
if (Unsafe.IsNullRef(ref location1))
101-
ThrowHelper.ThrowNullReferenceException();
102-
return Unsafe.As<T>(RuntimeImports.InterlockedExchange(ref Unsafe.As<T, object?>(ref location1), value));
103-
}
104-
10587
[Intrinsic]
10688
[MethodImpl(MethodImplOptions.AggressiveInlining)]
10789
[return: NotNullIfNotNull(nameof(location1))]

src/coreclr/vm/jitinterface.cpp

Lines changed: 0 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -7239,46 +7239,6 @@ bool getILIntrinsicImplementationForUnsafe(MethodDesc * ftn,
72397239
return false;
72407240
}
72417241

7242-
bool getILIntrinsicImplementationForInterlocked(MethodDesc * ftn,
7243-
CORINFO_METHOD_INFO * methInfo)
7244-
{
7245-
STANDARD_VM_CONTRACT;
7246-
7247-
_ASSERTE(CoreLibBinder::IsClass(ftn->GetMethodTable(), CLASS__INTERLOCKED));
7248-
7249-
// We are only interested if ftn's token and CompareExchange<T> token match
7250-
if (ftn->GetMemberDef() != CoreLibBinder::GetMethod(METHOD__INTERLOCKED__COMPARE_EXCHANGE_T)->GetMemberDef())
7251-
return false;
7252-
7253-
// Get MethodDesc for non-generic System.Threading.Interlocked.CompareExchange()
7254-
MethodDesc* cmpxchgObject = CoreLibBinder::GetMethod(METHOD__INTERLOCKED__COMPARE_EXCHANGE_OBJECT);
7255-
7256-
// Setup up the body of the method
7257-
static BYTE il[] = {
7258-
CEE_LDARG_0,
7259-
CEE_LDARG_1,
7260-
CEE_LDARG_2,
7261-
CEE_CALL,0,0,0,0,
7262-
CEE_RET
7263-
};
7264-
7265-
// Get the token for non-generic System.Threading.Interlocked.CompareExchange(), and patch [target]
7266-
mdMethodDef cmpxchgObjectToken = cmpxchgObject->GetMemberDef();
7267-
il[4] = (BYTE)((int)cmpxchgObjectToken >> 0);
7268-
il[5] = (BYTE)((int)cmpxchgObjectToken >> 8);
7269-
il[6] = (BYTE)((int)cmpxchgObjectToken >> 16);
7270-
il[7] = (BYTE)((int)cmpxchgObjectToken >> 24);
7271-
7272-
// Initialize methInfo
7273-
methInfo->ILCode = const_cast<BYTE*>(il);
7274-
methInfo->ILCodeSize = sizeof(il);
7275-
methInfo->maxStack = 3;
7276-
methInfo->EHcount = 0;
7277-
methInfo->options = (CorInfoOptions)0;
7278-
7279-
return true;
7280-
}
7281-
72827242
bool IsBitwiseEquatable(TypeHandle typeHandle, MethodTable * methodTable)
72837243
{
72847244
if (!methodTable->IsValueType() ||
@@ -7628,10 +7588,6 @@ static void getMethodInfoHelper(
76287588
{
76297589
fILIntrinsic = getILIntrinsicImplementationForUnsafe(ftn, methInfo);
76307590
}
7631-
else if (CoreLibBinder::IsClass(pMT, CLASS__INTERLOCKED))
7632-
{
7633-
fILIntrinsic = getILIntrinsicImplementationForInterlocked(ftn, methInfo);
7634-
}
76357591
else if (CoreLibBinder::IsClass(pMT, CLASS__RUNTIME_HELPERS))
76367592
{
76377593
fILIntrinsic = getILIntrinsicImplementationForRuntimeHelpers(ftn, methInfo);

src/libraries/System.Private.CoreLib/src/Resources/Strings.resx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4322,6 +4322,9 @@
43224322
<data name="NotSupported_EmitDebugInfo" xml:space="preserve">
43234323
<value>Emitting debug info is not supported for this member.</value>
43244324
</data>
4325+
<data name="NotSupported_ReferenceEnumOrPrimitiveTypeRequired" xml:space="preserve">
4326+
<value>The specified type must be a reference type, an enum type, or a primitive type.</value>
4327+
</data>
43254328
<data name="Argument_BadFieldForInitializeArray" xml:space="preserve">
43264329
<value>The field is invalid for initializing array or span.</value>
43274330
</data>

src/libraries/System.Private.CoreLib/src/System/Threading/Interlocked.cs

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System.Diagnostics;
5+
using System.Diagnostics.CodeAnalysis;
56
using System.Runtime.CompilerServices;
7+
using System.Runtime.InteropServices;
68

79
namespace System.Threading
810
{
@@ -222,6 +224,65 @@ public static UIntPtr Exchange(ref UIntPtr location1, UIntPtr value)
222224
return (UIntPtr)Exchange(ref Unsafe.As<UIntPtr, int>(ref location1), (int)value);
223225
#endif
224226
}
227+
228+
/// <summary>Sets a variable of the specified type <typeparamref name="T"/> to a specified value and returns the original value, as an atomic operation.</summary>
229+
/// <param name="location1">The variable to set to the specified value.</param>
230+
/// <param name="value">The value to which the <paramref name="location1"/> parameter is set.</param>
231+
/// <returns>The original value of <paramref name="location1"/>.</returns>
232+
/// <exception cref="NullReferenceException">The address of location1 is a null pointer.</exception>
233+
/// <exception cref="NotSupportedException">An unsupported <typeparamref name="T"/> is specified.</exception>
234+
/// <typeparam name="T">
235+
/// The type to be used for <paramref name="location1"/> and <paramref name="value"/>.
236+
/// This type must be a reference type, an enum type (i.e. typeof(T).IsEnum is true), or a primitive type (i.e. typeof(T).IsPrimitive is true).
237+
/// </typeparam>
238+
[return: NotNullIfNotNull(nameof(location1))]
239+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
240+
public static T Exchange<T>([NotNullIfNotNull(nameof(value))] ref T location1, T value)
241+
{
242+
// Handle all reference types with CompareExchange(ref object, ...).
243+
if (!typeof(T).IsValueType)
244+
{
245+
object? result = Exchange(ref Unsafe.As<T, object?>(ref location1), value);
246+
return Unsafe.As<object?, T>(ref result);
247+
}
248+
249+
// Handle everything else with a CompareExchange overload for the unsigned integral type of the corresponding size.
250+
// Only primitive types and enum types (which are backed by primitive types) are supported.
251+
if (!typeof(T).IsPrimitive && !typeof(T).IsEnum)
252+
{
253+
throw new NotSupportedException(SR.NotSupported_ReferenceEnumOrPrimitiveTypeRequired);
254+
}
255+
256+
if (Unsafe.SizeOf<T>() == 1)
257+
{
258+
return Unsafe.BitCast<byte, T>(
259+
Exchange(
260+
ref Unsafe.As<T, byte>(ref location1),
261+
Unsafe.BitCast<T, byte>(value)));
262+
}
263+
264+
if (Unsafe.SizeOf<T>() == 2)
265+
{
266+
return Unsafe.BitCast<ushort, T>(
267+
Exchange(
268+
ref Unsafe.As<T, ushort>(ref location1),
269+
Unsafe.BitCast<T, ushort>(value)));
270+
}
271+
272+
if (Unsafe.SizeOf<T>() == 4)
273+
{
274+
return Unsafe.BitCast<uint, T>(
275+
Exchange(
276+
ref Unsafe.As<T, uint>(ref location1),
277+
Unsafe.BitCast<T, uint>(value)));
278+
}
279+
280+
Debug.Assert(Unsafe.SizeOf<T>() == 8);
281+
return Unsafe.BitCast<ulong, T>(
282+
Exchange(
283+
ref Unsafe.As<T, ulong>(ref location1),
284+
Unsafe.BitCast<T, ulong>(value)));
285+
}
225286
#endregion
226287

227288
#region CompareExchange
@@ -413,6 +474,70 @@ public static UIntPtr CompareExchange(ref UIntPtr location1, UIntPtr value, UInt
413474
return (UIntPtr)CompareExchange(ref Unsafe.As<UIntPtr, int>(ref location1), (int)value, (int)comparand);
414475
#endif
415476
}
477+
478+
/// <summary>Compares two instances of the specified type <typeparamref name="T"/> for equality and, if they are equal, replaces the first one.</summary>
479+
/// <param name="location1">The destination, whose value is compared with <paramref name="comparand"/> and possibly replaced.</param>
480+
/// <param name="value">The value that replaces the destination value if the comparison results in equality.</param>
481+
/// <param name="comparand">The object that is compared to the value at <paramref name="location1"/>.</param>
482+
/// <returns>The original value in <paramref name="location1"/>.</returns>
483+
/// <exception cref="NullReferenceException">The address of <paramref name="location1"/> is a null pointer.</exception>
484+
/// <exception cref="NotSupportedException">An unsupported <typeparamref name="T"/> is specified.</exception>
485+
/// <typeparam name="T">
486+
/// The type to be used for <paramref name="location1"/>, <paramref name="value"/>, and <paramref name="comparand"/>.
487+
/// This type must be a reference type, an enum type (i.e. typeof(T).IsEnum is true), or a primitive type (i.e. typeof(T).IsPrimitive is true).
488+
/// </typeparam>
489+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
490+
[return: NotNullIfNotNull(nameof(location1))]
491+
public static T CompareExchange<T>(ref T location1, T value, T comparand)
492+
{
493+
// Handle all reference types with CompareExchange(ref object, ...).
494+
if (!typeof(T).IsValueType)
495+
{
496+
object? result = CompareExchange(ref Unsafe.As<T, object?>(ref location1), value, comparand);
497+
return Unsafe.As<object?, T>(ref result);
498+
}
499+
500+
// Handle everything else with a CompareExchange overload for the unsigned integral type of the corresponding size.
501+
// Only primitive types and enum types (which are backed by primitive types) are supported.
502+
if (!typeof(T).IsPrimitive && !typeof(T).IsEnum)
503+
{
504+
throw new NotSupportedException(SR.NotSupported_ReferenceEnumOrPrimitiveTypeRequired);
505+
}
506+
507+
if (Unsafe.SizeOf<T>() == 1)
508+
{
509+
return Unsafe.BitCast<byte, T>(
510+
CompareExchange(
511+
ref Unsafe.As<T, byte>(ref location1),
512+
Unsafe.BitCast<T, byte>(value),
513+
Unsafe.BitCast<T, byte>(comparand)));
514+
}
515+
516+
if (Unsafe.SizeOf<T>() == 2)
517+
{
518+
return Unsafe.BitCast<ushort, T>(
519+
CompareExchange(
520+
ref Unsafe.As<T, ushort>(ref location1),
521+
Unsafe.BitCast<T, ushort>(value),
522+
Unsafe.BitCast<T, ushort>(comparand)));
523+
}
524+
525+
if (Unsafe.SizeOf<T>() == 4)
526+
{
527+
return Unsafe.BitCast<uint, T>(
528+
CompareExchange(
529+
ref Unsafe.As<T, uint>(ref location1),
530+
Unsafe.BitCast<T, uint>(value),
531+
Unsafe.BitCast<T, uint>(comparand)));
532+
}
533+
534+
Debug.Assert(Unsafe.SizeOf<T>() == 8);
535+
return Unsafe.BitCast<ulong, T>(
536+
CompareExchange(
537+
ref Unsafe.As<T, ulong>(ref location1),
538+
Unsafe.BitCast<T, ulong>(value),
539+
Unsafe.BitCast<T, ulong>(comparand)));
540+
}
416541
#endregion
417542

418543
#region Add

src/libraries/System.Threading/ref/System.Threading.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -269,7 +269,7 @@ public static partial class Interlocked
269269
[System.CLSCompliantAttribute(false)]
270270
public static ulong CompareExchange(ref ulong location1, ulong value, ulong comparand) { throw null; }
271271
[return: System.Diagnostics.CodeAnalysis.NotNullIfNotNullAttribute("location1")]
272-
public static T CompareExchange<T>(ref T location1, T value, T comparand) where T : class? { throw null; }
272+
public static T CompareExchange<T>(ref T location1, T value, T comparand) { throw null; }
273273
public static int Decrement(ref int location) { throw null; }
274274
public static long Decrement(ref long location) { throw null; }
275275
[System.CLSCompliantAttribute(false)]
@@ -296,7 +296,7 @@ public static partial class Interlocked
296296
[System.CLSCompliantAttribute(false)]
297297
public static ulong Exchange(ref ulong location1, ulong value) { throw null; }
298298
[return: System.Diagnostics.CodeAnalysis.NotNullIfNotNullAttribute("location1")]
299-
public static T Exchange<T>([System.Diagnostics.CodeAnalysis.NotNullIfNotNullAttribute("value")] ref T location1, T value) where T : class? { throw null; }
299+
public static T Exchange<T>([System.Diagnostics.CodeAnalysis.NotNullIfNotNullAttribute("value")] ref T location1, T value) { throw null; }
300300
public static int Increment(ref int location) { throw null; }
301301
public static long Increment(ref long location) { throw null; }
302302
[System.CLSCompliantAttribute(false)]

src/mono/System.Private.CoreLib/src/System/Threading/Interlocked.Mono.cs

Lines changed: 0 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -72,47 +72,10 @@ public static partial class Interlocked
7272
[MethodImplAttribute(MethodImplOptions.InternalCall)]
7373
public static extern long CompareExchange(ref long location1, long value, long comparand);
7474

75-
[return: NotNullIfNotNull(nameof(location1))]
76-
[Intrinsic]
77-
public static T CompareExchange<T>(ref T location1, T value, T comparand) where T : class?
78-
{
79-
if (Unsafe.IsNullRef(ref location1))
80-
throw new NullReferenceException();
81-
// Besides avoiding coop handles for efficiency,
82-
// and correctness, this also appears needed to
83-
// avoid an assertion failure in the runtime, related to
84-
// coop handles over generics.
85-
//
86-
// See CompareExchange(object) for comments.
87-
//
88-
// This is not entirely convincing due to lack of volatile.
89-
//
90-
T? result = null;
91-
// T : class so call the object overload.
92-
CompareExchange(ref Unsafe.As<T, object?>(ref location1), ref Unsafe.As<T, object?>(ref value), ref Unsafe.As<T, object?>(ref comparand), ref Unsafe.As<T, object?>(ref result!));
93-
return result;
94-
}
95-
9675
[Intrinsic]
9776
[MethodImplAttribute(MethodImplOptions.InternalCall)]
9877
public static extern long Exchange(ref long location1, long value);
9978

100-
[return: NotNullIfNotNull(nameof(location1))]
101-
[Intrinsic]
102-
public static T Exchange<T>([NotNullIfNotNull(nameof(value))] ref T location1, T value) where T : class?
103-
{
104-
if (Unsafe.IsNullRef(ref location1))
105-
throw new NullReferenceException();
106-
// See CompareExchange(T) for comments.
107-
//
108-
// This is not entirely convincing due to lack of volatile.
109-
//
110-
T? result = null;
111-
// T : class so call the object overload.
112-
Exchange(ref Unsafe.As<T, object?>(ref location1), ref Unsafe.As<T, object?>(ref value), ref Unsafe.As<T, object?>(ref result!));
113-
return result;
114-
}
115-
11679
[Intrinsic]
11780
[MethodImplAttribute(MethodImplOptions.InternalCall)]
11881
public static extern long Read(ref long location);

0 commit comments

Comments
 (0)