diff --git a/src/System.Threading/ref/System.Threading.cs b/src/System.Threading/ref/System.Threading.cs index 2347a4f1d824..a564bfde4ca7 100644 --- a/src/System.Threading/ref/System.Threading.cs +++ b/src/System.Threading/ref/System.Threading.cs @@ -171,6 +171,7 @@ public static partial class Interlocked public static int Increment(ref int location) { throw null; } public static long Increment(ref long location) { throw null; } public static void MemoryBarrier() { } + public static void MemoryBarrierProcessWide() { } public static long Read(ref long location) { throw null; } } public static partial class LazyInitializer diff --git a/src/System.Threading/tests/Configurations.props b/src/System.Threading/tests/Configurations.props index c398e42e8994..77a4b65bc94a 100644 --- a/src/System.Threading/tests/Configurations.props +++ b/src/System.Threading/tests/Configurations.props @@ -3,6 +3,7 @@ netstandard; + netcoreapp; \ No newline at end of file diff --git a/src/System.Threading/tests/InterlockedTests.cs b/src/System.Threading/tests/InterlockedTests.cs index ebdd8e6ae16d..83cac4605280 100644 --- a/src/System.Threading/tests/InterlockedTests.cs +++ b/src/System.Threading/tests/InterlockedTests.cs @@ -8,7 +8,7 @@ namespace System.Threading.Tests { - public class InterlockedTests + public partial class InterlockedTests { [Fact] public void IncrementDecrement_int() diff --git a/src/System.Threading/tests/InterlockedTests.netcoreapp.cs b/src/System.Threading/tests/InterlockedTests.netcoreapp.cs new file mode 100644 index 000000000000..8349ffcf6ca9 --- /dev/null +++ b/src/System.Threading/tests/InterlockedTests.netcoreapp.cs @@ -0,0 +1,124 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.Diagnostics; +using System.Threading.Tasks; +using System.Runtime.CompilerServices; +using Xunit; + +namespace System.Threading.Tests +{ + public partial class InterlockedTests + { + // Taking this lock on the same thread repeatedly is very fast because it has no interlocked operations. + // Switching the thread where the lock is taken is expensive because of allocation and FlushProcessWriteBuffers. + private class AsymmetricLock + { + public class LockCookie + { + internal LockCookie(int threadId) + { + ThreadId = threadId; + Taken = false; + } + + public void Exit() + { + Debug.Assert(ThreadId == Environment.CurrentManagedThreadId); + Taken = false; + } + + internal readonly int ThreadId; + internal bool Taken; + } + + private LockCookie _current = new LockCookie(-1); + + [MethodImpl(MethodImplOptions.NoInlining)] + private static T VolatileReadWithoutBarrier(ref T location) + { + return location; + } + + // Returning LockCookie to call Exit on is the fastest implementation because of it works naturally with the RCU pattern. + // The traditional Enter/Exit lock interface would require thread local storage or some other scheme to reclaim the cookie. + public LockCookie Enter() + { + int currentThreadId = Environment.CurrentManagedThreadId; + + LockCookie entry = _current; + + if (entry.ThreadId == currentThreadId) + { + entry.Taken = true; + + // + // If other thread started stealing the ownership, we need to take slow path. + // + // Make sure that the compiler won't reorder the read with the above write by wrapping the read in no-inline method. + // RyuJIT won't reorder them today, but more advanced optimizers might. Regular Volatile.Read would be too big of + // a hammer because of it will result into memory barrier on ARM that we do not need here. + // + // + if (VolatileReadWithoutBarrier(ref _current) == entry) + { + return entry; + } + + entry.Taken = false; + } + + return EnterSlow(); + } + + private LockCookie EnterSlow() + { + // Attempt to steal the ownership. Take a regular lock to ensure that only one thread is trying to steal it at a time. + lock (this) + { + // We are the new fast thread now! + var oldEntry = _current; + _current = new LockCookie(Environment.CurrentManagedThreadId); + + // After MemoryBarrierProcessWide, we can be sure that the Volatile.Read done by the fast thread will see that it is not a fast + // thread anymore, and thus it will not attempt to enter the lock. + Interlocked.MemoryBarrierProcessWide(); + + // Keep looping as long as the lock is taken by other thread + SpinWait sw = new SpinWait(); + while (oldEntry.Taken) + sw.SpinOnce(); + + _current.Taken = true; + return _current; + } + } + } + + [Fact] + public void MemoryBarrierProcessWide() + { + // Stress MemoryBarrierProcessWide correctness using a simple AsymmetricLock + + AsymmetricLock asymmetricLock = new AsymmetricLock(); + List threads = new List(); + int count = 0; + for (int i = 0; i < 1000; i++) + { + threads.Add(Task.Run(() => + { + for (int j = 0; j < 1000; j++) + { + var cookie = asymmetricLock.Enter(); + count++; + cookie.Exit(); + } + })); + } + Task.WaitAll(threads.ToArray()); + Assert.Equal(1000*1000, count); + } + } +} diff --git a/src/System.Threading/tests/System.Threading.Tests.csproj b/src/System.Threading/tests/System.Threading.Tests.csproj index 0986636e9d74..48bf771e14f0 100644 --- a/src/System.Threading/tests/System.Threading.Tests.csproj +++ b/src/System.Threading/tests/System.Threading.Tests.csproj @@ -17,6 +17,7 @@ +