Expose and test APIs for some threading metrics (CoreFX) (dotnet/corefx#37401)
authorKoundinya Veluri <kouvel@users.noreply.github.com>
Thu, 9 May 2019 10:44:39 +0000 (03:44 -0700)
committerStephen Toub <stoub@microsoft.com>
Thu, 9 May 2019 10:44:39 +0000 (06:44 -0400)
* Expose and test APIs for some threading metrics (CoreFX)

- API review: https://github.com/dotnet/corefx/issues/35500
- Depends on https://github.com/dotnet/coreclr/pull/22754, https://github.com/dotnet/corert/pull/7066

* Separate and expose pending local vs global work item count

* Remove local/global variants of PendingWorkItemCount

* Remove unrelated test

* Add test for a fix to ThreadLocal.Values property throwing NullReferenceException when disposed

Fix is in https://github.com/dotnet/corert/pull/7066

* Fix build

* Fix test

* Add API compat baselines for uapaot

* Fix test

* Use RemoteExecutor for MetricsTest

* Address feedback

Commit migrated from https://github.com/dotnet/corefx/commit/34fe566b6f6bd739a876e7e1dc48cf631647e5d7

21 files changed:
src/libraries/Common/tests/System/Threading/ThreadTestHelpers.cs
src/libraries/System.Threading.Overlapped/tests/Configurations.props
src/libraries/System.Threading.Overlapped/tests/System.Threading.Overlapped.Tests.csproj
src/libraries/System.Threading.Overlapped/tests/ThreadPoolBoundHandle_IntegrationTests.netcoreapp.cs [new file with mode: 0644]
src/libraries/System.Threading.Thread/tests/System.Threading.Thread.Tests.csproj
src/libraries/System.Threading.Thread/tests/ThreadTests.netcoreapp.cs
src/libraries/System.Threading.ThreadPool/ref/System.Threading.ThreadPool.cs
src/libraries/System.Threading.ThreadPool/src/ApiCompatBaseline.uapaot.txt [new file with mode: 0644]
src/libraries/System.Threading.ThreadPool/tests/Configurations.props
src/libraries/System.Threading.ThreadPool/tests/System.Threading.ThreadPool.Tests.csproj
src/libraries/System.Threading.ThreadPool/tests/ThreadPoolTests.netcoreapp.cs
src/libraries/System.Threading.Timer/tests/Configurations.props
src/libraries/System.Threading.Timer/tests/System.Threading.Timer.Tests.csproj
src/libraries/System.Threading.Timer/tests/TimerFiringTests.cs
src/libraries/System.Threading/ref/System.Threading.cs
src/libraries/System.Threading/src/ApiCompatBaseline.uapaot.txt [new file with mode: 0644]
src/libraries/System.Threading/tests/Configurations.props
src/libraries/System.Threading/tests/MonitorTests.cs
src/libraries/System.Threading/tests/MonitorTests.netcoreapp.cs [new file with mode: 0644]
src/libraries/System.Threading/tests/System.Threading.Tests.csproj
src/libraries/System.Threading/tests/ThreadLocalTests.cs

index d573575..4ebcdcb 100644 (file)
@@ -123,13 +123,29 @@ namespace System.Threading.Tests
             WaitForConditionWithCustomDelay(condition, () => Thread.Yield());
         }
 
+        public static void WaitForConditionWithoutRelinquishingTimeSlice(Func<bool> condition)
+        {
+            WaitForConditionWithCustomDelay(condition, () => Thread.SpinWait(1));
+        }
+
         public static void WaitForConditionWithCustomDelay(Func<bool> condition, Action delay)
         {
-            var startTime = DateTime.Now;
-            while (!condition())
+            if (condition())
+            {
+                return;
+            }
+
+            int startTimeMs = Environment.TickCount;
+            while (true)
             {
-                Assert.True((DateTime.Now - startTime).TotalMilliseconds < UnexpectedTimeoutMilliseconds);
                 delay();
+
+                if (condition())
+                {
+                    return;
+                }
+
+                Assert.InRange(Environment.TickCount - startTimeMs, 0, UnexpectedTimeoutMilliseconds);
             }
         }
 
index e31e3bc..ba03084 100644 (file)
@@ -1,6 +1,7 @@
 <Project DefaultTargets="Build">
   <PropertyGroup>
     <BuildConfigurations>
+      netcoreapp;
       netstandard;
       uap-Windows_NT;
     </BuildConfigurations>
index 5fc24ef..f2cef71 100644 (file)
@@ -2,13 +2,15 @@
   <PropertyGroup>
     <ProjectGuid>{861A3318-35AD-46ac-8257-8D5D2479BAD9}</ProjectGuid>
     <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
-    <Configurations>netstandard-Debug;netstandard-Release;uap-Windows_NT-Debug;uap-Windows_NT-Release</Configurations>
+    <IncludeRemoteExecutor>true</IncludeRemoteExecutor>
+    <Configurations>netcoreapp-Debug;netcoreapp-Release;netstandard-Debug;netstandard-Release;uap-Windows_NT-Debug;uap-Windows_NT-Release</Configurations>
     <TestRuntime>true</TestRuntime>
   </PropertyGroup>
   <ItemGroup>
     <Compile Include="DllImport.cs" />
     <Compile Include="ThreadPoolBoundHandle_PreAllocatedOverlappedTests.cs" />
     <Compile Include="ThreadPoolBoundHandle_IntegrationTests.cs" />
+    <Compile Include="ThreadPoolBoundHandle_IntegrationTests.netcoreapp.cs" Condition="'$(TargetGroup)' != 'netstandard'" />
     <Compile Include="ThreadPoolBoundHandle_Helpers.cs" />
     <Compile Include="AsyncResult.cs" />
     <Compile Include="Win32Handle.cs" />
@@ -21,4 +23,9 @@
     <Compile Include="ThreadPoolBoundHandle_BindHandleTests.cs" />
     <Compile Include="OverlappedTests.cs" />
   </ItemGroup>
+  <ItemGroup>
+    <Compile Include="$(CommonTestPath)\System\Threading\ThreadTestHelpers.cs">
+      <Link>CommonTest\System\Threading\ThreadTestHelpers.cs</Link>
+    </Compile>
+  </ItemGroup>
 </Project>
\ No newline at end of file
diff --git a/src/libraries/System.Threading.Overlapped/tests/ThreadPoolBoundHandle_IntegrationTests.netcoreapp.cs b/src/libraries/System.Threading.Overlapped/tests/ThreadPoolBoundHandle_IntegrationTests.netcoreapp.cs
new file mode 100644 (file)
index 0000000..7b0aebb
--- /dev/null
@@ -0,0 +1,19 @@
+// 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.Threading;
+using System.Threading.Tests;
+using Xunit;
+
+public partial class ThreadPoolBoundHandleTests
+{
+    [Fact]
+    [PlatformSpecific(TestPlatforms.Windows)] // ThreadPoolBoundHandle.BindHandle is not supported on Unix
+    public unsafe void MultipleOperationsOverSingleHandle_CompletedWorkItemCountTest()
+    {
+        long initialCompletedWorkItemCount = ThreadPool.CompletedWorkItemCount;
+        MultipleOperationsOverMultipleHandles();
+        ThreadTestHelpers.WaitForCondition(() => ThreadPool.CompletedWorkItemCount - initialCompletedWorkItemCount >= 2);
+    }
+}
index 1b617e4..4cbbd6a 100644 (file)
@@ -17,7 +17,7 @@
   </ItemGroup>
   <ItemGroup>
     <Compile Include="$(CommonTestPath)\System\Threading\ThreadTestHelpers.cs">
-      <Link>CommonTest\System\Threading\ThreadPoolHelpers.cs</Link>
+      <Link>CommonTest\System\Threading\ThreadTestHelpers.cs</Link>
     </Compile>
     <ProjectReference Include="STAMain\STAMain.csproj">
       <Name>STAMain</Name>
index 79e3063..d8f9c84 100644 (file)
@@ -8,6 +8,7 @@ namespace System.Threading.Threads.Tests
 {
     public static partial class ThreadTests
     {
+        [Fact]
         public static void GetCurrentProcessorId()
         {
             Assert.True(Thread.GetCurrentProcessorId() >= 0);
index ea68048..56fe570 100644 (file)
@@ -21,9 +21,11 @@ namespace System.Threading
         [System.ObsoleteAttribute("ThreadPool.BindHandle(IntPtr) has been deprecated.  Please use ThreadPool.BindHandle(SafeHandle) instead.", false)]
         public static bool BindHandle(System.IntPtr osHandle) { throw null; }
         public static bool BindHandle(System.Runtime.InteropServices.SafeHandle osHandle) { throw null; }
+        public static long CompletedWorkItemCount { get { throw null; } }
         public static void GetAvailableThreads(out int workerThreads, out int completionPortThreads) { throw null; }
         public static void GetMaxThreads(out int workerThreads, out int completionPortThreads) { throw null; }
         public static void GetMinThreads(out int workerThreads, out int completionPortThreads) { throw null; }
+        public static long PendingWorkItemCount { get { throw null; } }
         public static bool QueueUserWorkItem(System.Threading.WaitCallback callBack) { throw null; }
         public static bool QueueUserWorkItem(System.Threading.WaitCallback callBack, object state) { throw null; }
         public static bool QueueUserWorkItem<TState>(System.Action<TState> callBack, TState state, bool preferLocal) { throw null; }
@@ -34,6 +36,7 @@ namespace System.Threading
         public static System.Threading.RegisteredWaitHandle RegisterWaitForSingleObject(System.Threading.WaitHandle waitObject, System.Threading.WaitOrTimerCallback callBack, object state, uint millisecondsTimeOutInterval, bool executeOnlyOnce) { throw null; }
         public static bool SetMaxThreads(int workerThreads, int completionPortThreads) { throw null; }
         public static bool SetMinThreads(int workerThreads, int completionPortThreads) { throw null; }
+        public static int ThreadCount { get { throw null; } }
         [System.CLSCompliantAttribute(false)]
         public unsafe static bool UnsafeQueueNativeOverlapped(System.Threading.NativeOverlapped* overlapped) { throw null; }
         public static bool UnsafeQueueUserWorkItem(System.Threading.IThreadPoolWorkItem callBack, bool preferLocal) { throw null; }
diff --git a/src/libraries/System.Threading.ThreadPool/src/ApiCompatBaseline.uapaot.txt b/src/libraries/System.Threading.ThreadPool/src/ApiCompatBaseline.uapaot.txt
new file mode 100644 (file)
index 0000000..2aae58b
--- /dev/null
@@ -0,0 +1,4 @@
+Compat issues with assembly System.Threading.ThreadPool:
+MembersMustExist : Member 'System.Threading.ThreadPool.CompletedWorkItemCount.get()' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'System.Threading.ThreadPool.PendingWorkItemCount.get()' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'System.Threading.ThreadPool.ThreadCount.get()' does not exist in the implementation but it does exist in the contract.
index f5d3d45..2ae79c7 100644 (file)
@@ -3,6 +3,7 @@
     <BuildConfigurations>
       netcoreapp;
       netstandard;
+      uap;
     </BuildConfigurations>
   </PropertyGroup>
 </Project>
\ No newline at end of file
index fe7245c..1762ff8 100644 (file)
@@ -2,12 +2,12 @@
   <PropertyGroup>
     <ProjectGuid>{403AD1B8-6F95-4A2E-92A2-727606ABD866}</ProjectGuid>
     <IncludeRemoteExecutor>true</IncludeRemoteExecutor>
-    <Configurations>netcoreapp-Debug;netcoreapp-Release;netstandard-Debug;netstandard-Release</Configurations>
+    <Configurations>netcoreapp-Debug;netcoreapp-Release;netstandard-Debug;netstandard-Release;uap-Debug;uap-Release</Configurations>
     <TestRuntime>true</TestRuntime>
   </PropertyGroup>
   <ItemGroup>
     <Compile Include="ThreadPoolTests.cs" />
-    <Compile Include="ThreadPoolTests.netcoreapp.cs" Condition="'$(TargetGroup)' == 'netcoreapp'" />
+    <Compile Include="ThreadPoolTests.netcoreapp.cs" Condition="'$(TargetGroup)' != 'netstandard'" />
   </ItemGroup>
   <ItemGroup>
     <Compile Include="$(CommonTestPath)\System\Threading\ThreadTestHelpers.cs">
index 2ddd1f4..567f69e 100644 (file)
@@ -5,6 +5,8 @@
 using System.Collections.Generic;
 using System.Linq;
 using System.Threading.Tasks;
+using System.Threading.Tests;
+using Microsoft.DotNet.RemoteExecutor;
 using Xunit;
 
 namespace System.Threading.ThreadPools.Tests
@@ -188,5 +190,145 @@ namespace System.Threading.ThreadPools.Tests
             public InvalidWorkItemAndTask(Action action) : base(action) { }
             public void Execute() { }
         }
+
+        [ConditionalFact(nameof(HasAtLeastThreeProcessors))]
+        public void MetricsTest()
+        {
+            RemoteExecutor.Invoke(() =>
+            {
+                int processorCount = Environment.ProcessorCount;
+                if (processorCount <= 2)
+                {
+                    return;
+                }
+
+                bool waitForWorkStart = false;
+                var workStarted = new AutoResetEvent(false);
+                var localWorkScheduled = new AutoResetEvent(false);
+                int completeWork = 0;
+                int queuedWorkCount = 0;
+                var allWorkCompleted = new ManualResetEvent(false);
+                Exception backgroundEx = null;
+                Action work = () =>
+                {
+                    if (waitForWorkStart)
+                    {
+                        workStarted.Set();
+                    }
+                    try
+                    {
+                        // Blocking can affect thread pool thread injection heuristics, so don't block, pretend like a
+                        // long-running CPU-bound work item
+                        ThreadTestHelpers.WaitForConditionWithoutRelinquishingTimeSlice(
+                                () => Interlocked.CompareExchange(ref completeWork, 0, 0) != 0);
+                    }
+                    catch (Exception ex)
+                    {
+                        Interlocked.CompareExchange(ref backgroundEx, ex, null);
+                    }
+                    finally
+                    {
+                        if (Interlocked.Decrement(ref queuedWorkCount) == 0)
+                        {
+                            allWorkCompleted.Set();
+                        }
+                    }
+                };
+                WaitCallback threadPoolGlobalWork = data => work();
+                Action<object> threadPoolLocalWork = data => work();
+                WaitCallback scheduleThreadPoolLocalWork = data =>
+                {
+                    try
+                    {
+                        int n = (int)data;
+                        for (int i = 0; i < n; ++i)
+                        {
+                            ThreadPool.QueueUserWorkItem(threadPoolLocalWork, null, preferLocal: true);
+                            if (waitForWorkStart)
+                            {
+                                workStarted.CheckedWait();
+                            }
+                        }
+                    }
+                    catch (Exception ex)
+                    {
+                        Interlocked.CompareExchange(ref backgroundEx, ex, null);
+                    }
+                    finally
+                    {
+                        localWorkScheduled.Set();
+                    }
+                };
+
+                var signaledEvent = new ManualResetEvent(true);
+                var timers = new List<Timer>();
+                int totalWorkCountToQueue = 0;
+                Action scheduleWork = () =>
+                {
+                    Assert.True(queuedWorkCount < totalWorkCountToQueue);
+
+                    int workCount = (totalWorkCountToQueue - queuedWorkCount) / 2;
+                    if (workCount > 0)
+                    {
+                        queuedWorkCount += workCount;
+                        ThreadPool.QueueUserWorkItem(scheduleThreadPoolLocalWork, workCount);
+                        localWorkScheduled.CheckedWait();
+                    }
+
+                    for (; queuedWorkCount < totalWorkCountToQueue; ++queuedWorkCount)
+                    {
+                        ThreadPool.QueueUserWorkItem(threadPoolGlobalWork);
+                        if (waitForWorkStart)
+                        {
+                            workStarted.CheckedWait();
+                        }
+                    }
+                };
+
+                Interlocked.MemoryBarrierProcessWide(); // get a reasonably accurate value for the following
+                long initialCompletedWorkItemCount = ThreadPool.CompletedWorkItemCount;
+
+                try
+                {
+                    // Schedule some simultaneous work that would all be scheduled and verify the thread count
+                    totalWorkCountToQueue = processorCount - 2;
+                    Assert.True(totalWorkCountToQueue >= 1);
+                    waitForWorkStart = true;
+                    scheduleWork();
+                    Assert.True(ThreadPool.ThreadCount >= totalWorkCountToQueue);
+
+                    int runningWorkItemCount = queuedWorkCount;
+
+                    // Schedule more work that would not all be scheduled and roughly verify the pending work item count
+                    totalWorkCountToQueue = processorCount * 64;
+                    waitForWorkStart = false;
+                    scheduleWork();
+                    int minExpectedPendingWorkCount = Math.Max(1, queuedWorkCount - runningWorkItemCount * 8);
+                    ThreadTestHelpers.WaitForCondition(() => ThreadPool.PendingWorkItemCount >= minExpectedPendingWorkCount);
+                }
+                finally
+                {
+                    // Complete the work
+                    Interlocked.Exchange(ref completeWork, 1);
+                }
+
+                // Wait for work items to exit, for counting
+                allWorkCompleted.CheckedWait();
+                backgroundEx = Interlocked.CompareExchange(ref backgroundEx, null, null);
+                if (backgroundEx != null)
+                {
+                    throw new AggregateException(backgroundEx);
+                }
+
+                // Verify the completed work item count
+                ThreadTestHelpers.WaitForCondition(() =>
+                {
+                    Interlocked.MemoryBarrierProcessWide(); // get a reasonably accurate value for the following
+                    return ThreadPool.CompletedWorkItemCount - initialCompletedWorkItemCount >= totalWorkCountToQueue;
+                });
+            }).Dispose();
+        }
+
+        public static bool HasAtLeastThreeProcessors => Environment.ProcessorCount >= 3;
     }
 }
index f5d3d45..2ae79c7 100644 (file)
@@ -3,6 +3,7 @@
     <BuildConfigurations>
       netcoreapp;
       netstandard;
+      uap;
     </BuildConfigurations>
   </PropertyGroup>
 </Project>
\ No newline at end of file
index d4b8386..6575f1b 100644 (file)
@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
   <PropertyGroup>
     <ProjectGuid>{ac20a28f-fda8-45e8-8728-058ead16e44c}</ProjectGuid>
-    <Configurations>netcoreapp-Debug;netcoreapp-Release;netstandard-Debug;netstandard-Release</Configurations>
+    <Configurations>netcoreapp-Debug;netcoreapp-Release;netstandard-Debug;netstandard-Release;uap-Debug;uap-Release</Configurations>
     <TestRuntime>true</TestRuntime>
     <IncludeRemoteExecutor>true</IncludeRemoteExecutor>
   </PropertyGroup>
     <Compile Include="TimerChangeTests.cs" />
     <Compile Include="TimerFiringTests.cs" />
   </ItemGroup>
-  <ItemGroup Condition="'$(TargetGroup)' == 'netcoreapp'">
+  <ItemGroup Condition="'$(TargetGroup)' != 'netstandard'">
     <Compile Include="TimerDisposeTests.cs" />
   </ItemGroup>
-  <ItemGroup>
-    <Compile Include="$(CommonTestPath)\System\Threading\ThreadTestHelpers.cs">
-      <Link>CommonTest\System\Threading\ThreadTestHelpers.cs</Link>
-    </Compile>
-  </ItemGroup>
 </Project>
\ No newline at end of file
index 375df33..00006f6 100644 (file)
@@ -10,7 +10,6 @@ using System.Linq;
 using System.Text;
 using System.Threading;
 using System.Threading.Tasks;
-using System.Threading.Tests;
 using Xunit;
 using Xunit.Sdk;
 
@@ -338,57 +337,4 @@ public partial class TimerFiringTests
             await tcs.Task.ConfigureAwait(false);
         }
     }
-
-    [Fact]
-    public void TimersCreatedConcurrentlyOnDifferentThreadsAllFire()
-    {
-        int processorCount = Environment.ProcessorCount;
-
-        int timerTickCount = 0;
-        TimerCallback timerCallback = _ => Interlocked.Increment(ref timerTickCount);
-
-        var threadStarted = new AutoResetEvent(false);
-        var createTimers = new ManualResetEvent(false);
-        var timers = new Timer[processorCount];
-        Action<object> createTimerThreadStart = data =>
-        {
-            int i = (int)data;
-            var sw = new Stopwatch();
-            threadStarted.Set();
-            createTimers.WaitOne();
-
-            // Use the CPU a bit around creating the timer to try to have some of these threads run concurrently
-            sw.Restart();
-            do
-            {
-                Thread.SpinWait(1000);
-            } while (sw.ElapsedMilliseconds < 10);
-
-            timers[i] = new Timer(timerCallback, null, 1, Timeout.Infinite);
-
-            // Use the CPU a bit around creating the timer to try to have some of these threads run concurrently
-            sw.Restart();
-            do
-            {
-                Thread.SpinWait(1000);
-            } while (sw.ElapsedMilliseconds < 10);
-        };
-
-        var waitsForThread = new Action[timers.Length];
-        for (int i = 0; i < timers.Length; ++i)
-        {
-            var t = ThreadTestHelpers.CreateGuardedThread(out waitsForThread[i], createTimerThreadStart);
-            t.IsBackground = true;
-            t.Start(i);
-            threadStarted.CheckedWait();
-        }
-
-        createTimers.Set();
-        ThreadTestHelpers.WaitForCondition(() => timerTickCount == timers.Length);
-
-        foreach (var waitForThread in waitsForThread)
-        {
-            waitForThread();
-        }
-    }
 }
index 66bb5f5..b65709a 100644 (file)
@@ -228,6 +228,7 @@ namespace System.Threading
         public static void Enter(object obj, ref bool lockTaken) { }
         public static void Exit(object obj) { }
         public static bool IsEntered(object obj) { throw null; }
+        public static long LockContentionCount { get { throw null; } }
         public static void Pulse(object obj) { }
         public static void PulseAll(object obj) { }
         public static bool TryEnter(object obj) { throw null; }
diff --git a/src/libraries/System.Threading/src/ApiCompatBaseline.uapaot.txt b/src/libraries/System.Threading/src/ApiCompatBaseline.uapaot.txt
new file mode 100644 (file)
index 0000000..8db229d
--- /dev/null
@@ -0,0 +1,2 @@
+Compat issues with assembly System.Threading:
+MembersMustExist : Member 'System.Threading.Monitor.LockContentionCount.get()' does not exist in the implementation but it does exist in the contract.
index febffb0..9018550 100644 (file)
@@ -3,6 +3,7 @@
     <BuildConfigurations>
       netstandard;
       netcoreapp;
+      uap;
     </BuildConfigurations>
   </PropertyGroup>
 </Project>
\ No newline at end of file
index 2d37c4b..f7fab6e 100644 (file)
@@ -10,7 +10,7 @@ using Xunit;
 
 namespace System.Threading.Tests
 {
-    public static class MonitorTests
+    public static partial class MonitorTests
     {
         private const int FailTimeoutMilliseconds = 30000;
 
diff --git a/src/libraries/System.Threading/tests/MonitorTests.netcoreapp.cs b/src/libraries/System.Threading/tests/MonitorTests.netcoreapp.cs
new file mode 100644 (file)
index 0000000..48eb758
--- /dev/null
@@ -0,0 +1,23 @@
+// 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;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace System.Threading.Tests
+{
+    public static partial class MonitorTests
+    {
+        [Fact]
+        public static void Enter_HasToWait_LockContentionCountTest()
+        {
+            long initialLockContentionCount = Monitor.LockContentionCount;
+            Enter_HasToWait();
+            Assert.True(Monitor.LockContentionCount - initialLockContentionCount >= 2);
+        }
+    }
+}
index 5054c23..d95b7b7 100644 (file)
@@ -3,7 +3,7 @@
     <ProjectGuid>{18EF66B3-51EE-46D8-B283-1CB6A1197813}</ProjectGuid>
     <TestRuntime>true</TestRuntime>
     <IncludeRemoteExecutor>true</IncludeRemoteExecutor>
-    <Configurations>netcoreapp-Debug;netcoreapp-Release;netstandard-Debug;netstandard-Release</Configurations>
+    <Configurations>netcoreapp-Debug;netcoreapp-Release;netstandard-Debug;netstandard-Release;uap-Debug;uap-Release</Configurations>
   </PropertyGroup>
   <ItemGroup>
     <Compile Include="AsyncLocalTests.cs" />
     <Compile Include="EtwTests.cs" />
     <Compile Include="EventWaitHandleTests.cs" />
     <Compile Include="InterlockedTests.cs" />
-    <Compile Include="InterlockedTests.netcoreapp.cs" Condition="'$(TargetGroup)'=='netcoreapp'" />
+    <Compile Include="InterlockedTests.netcoreapp.cs" Condition="'$(TargetGroup)' != 'netstandard'" />
     <Compile Include="HostExecutionContextTests.cs" />
     <Compile Include="HostExecutionContextManagerTests.cs" />
     <Compile Include="ManualResetEventTests.cs" />
     <Compile Include="ManualResetEventSlimCancellationTests.cs" />
     <Compile Include="ManualResetEventSlimTests.cs" />
     <Compile Include="MonitorTests.cs" />
+    <Compile Include="MonitorTests.netcoreapp.cs" Condition="'$(TargetGroup)' != 'netstandard'" />
     <Compile Include="MutexTests.cs" />
     <Compile Include="SemaphoreSlimCancellationTests.cs" />
     <Compile Include="SemaphoreSlimTests.cs" />
@@ -30,7 +31,7 @@
     <Compile Include="ReaderWriterLockTests.cs" />
     <Compile Include="ReaderWriterLockSlimTests.cs" />
     <Compile Include="SpinWaitTests.cs" />
-    <Compile Include="SpinWaitTests.netcoreapp.cs" Condition="'$(TargetGroup)'=='netcoreapp'" />
+    <Compile Include="SpinWaitTests.netcoreapp.cs" Condition="'$(TargetGroup)' != 'netstandard'" />
     <Compile Include="ThreadLocalTests.cs" />
     <Compile Include="XunitAssemblyAttributes.cs" />
     <Compile Include="ExecutionContextTests.cs" />
index abf089a..f703ed3 100644 (file)
@@ -368,6 +368,75 @@ namespace System.Threading.Tests
             }
         }
 
+        [Fact]
+        [OuterLoop]
+        [SkipOnTargetFramework(TargetFrameworkMonikers.NetFramework)]
+        public static void ValuesGetterDoesNotThrowUnexpectedExceptionWhenDisposed()
+        {
+            var startTest = new ManualResetEvent(false);
+            var gotUnexpectedException = new ManualResetEvent(false);
+            ThreadLocal<int> threadLocal = null;
+            bool stop = false;
+
+            Action waitForCreatorDisposer;
+            Thread creatorDisposer = ThreadTestHelpers.CreateGuardedThread(out waitForCreatorDisposer, () =>
+            {
+                startTest.CheckedWait();
+                do
+                {
+                    var tl = new ThreadLocal<int>(trackAllValues: true);
+                    Volatile.Write(ref threadLocal, tl);
+                    tl.Value = 1;
+                    tl.Dispose();
+                } while (!Volatile.Read(ref stop));
+            });
+            creatorDisposer.IsBackground = true;
+            creatorDisposer.Start();
+
+            int readerCount = Math.Max(1, Environment.ProcessorCount - 1);
+            var waitsForReader = new Action[readerCount];
+            for (int i = 0; i < readerCount; ++i)
+            {
+                Thread reader = ThreadTestHelpers.CreateGuardedThread(out waitsForReader[i], () =>
+                {
+                    startTest.CheckedWait();
+                    do
+                    {
+                        var tl = Volatile.Read(ref threadLocal);
+                        if (tl == null)
+                        {
+                            continue;
+                        }
+
+                        try
+                        {
+                            IList<int> values = tl.Values;
+                        }
+                        catch (ObjectDisposedException)
+                        {
+                        }
+                        catch
+                        {
+                            gotUnexpectedException.Set();
+                            throw;
+                        }
+                    } while (!Volatile.Read(ref stop));
+                });
+                reader.IsBackground = true;
+                reader.Start();
+            }
+
+            startTest.Set();
+            bool failed = gotUnexpectedException.WaitOne(500);
+            Volatile.Write(ref stop, true);
+            foreach (Action waitForReader in waitsForReader)
+            {
+                waitForReader();
+            }
+            waitForCreatorDisposer();
+            Assert.False(failed);
+        }
+
         private class SetMreOnFinalize
         {
             private ManualResetEventSlim _mres;