diff --git a/src/coreclr/jit/utils.cpp b/src/coreclr/jit/utils.cpp index 86300e47383b0f..19186932327af3 100644 --- a/src/coreclr/jit/utils.cpp +++ b/src/coreclr/jit/utils.cpp @@ -1560,7 +1560,8 @@ void HelperCallProperties::init() break; case CORINFO_HELP_GETCURRENTMANAGEDTHREADID: - isPure = true; + // In runtime async methods, execution may resume on a different thread after suspension. + // So managed thread ID is not a constant/pure value, but the helper is still no-throw. exceptions = ExceptionSetFlags::None; break; diff --git a/src/coreclr/jit/valuenum.cpp b/src/coreclr/jit/valuenum.cpp index a8ee6dd94e0a8b..95126442315bbf 100644 --- a/src/coreclr/jit/valuenum.cpp +++ b/src/coreclr/jit/valuenum.cpp @@ -14946,11 +14946,6 @@ VNFunc Compiler::fgValueNumberJitHelperMethodVNFunc(CorInfoHelpFunc helpFunc) vnf = VNF_Unbox_TypeTest; break; - // A constant within any method. - case CORINFO_HELP_GETCURRENTMANAGEDTHREADID: - vnf = VNF_ManagedThreadId; - break; - case CORINFO_HELP_GETREFANY: // TODO-CQ: This should really be interpreted as just a struct field reference, in terms of values. vnf = VNF_GetRefanyVal; diff --git a/src/coreclr/jit/valuenumfuncs.h b/src/coreclr/jit/valuenumfuncs.h index 7026e444388c72..195795669f0dfd 100644 --- a/src/coreclr/jit/valuenumfuncs.h +++ b/src/coreclr/jit/valuenumfuncs.h @@ -114,8 +114,6 @@ ValueNumFuncDef(LeadingZeroCount, 1, false, false) ValueNumFuncDef(TrailingZeroCount, 1, false, false) ValueNumFuncDef(PopCount, 1, false, false) -ValueNumFuncDef(ManagedThreadId, 0, false, false) - ValueNumFuncDef(ObjGetType, 1, false, true) ValueNumFuncDef(GetGcstaticBase, 1, false, true) ValueNumFuncDef(GetNongcstaticBase, 1, false, true) diff --git a/src/tests/async/regression/managed-thread-id.cs b/src/tests/async/regression/managed-thread-id.cs new file mode 100644 index 00000000000000..04102b536e596d --- /dev/null +++ b/src/tests/async/regression/managed-thread-id.cs @@ -0,0 +1,65 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// Regression test: Verify that Environment.CurrentManagedThreadId is not treated as a +// JIT constant/pure value across async suspension points. In runtime async methods, +// the managed thread ID can change when the continuation resumes on a different thread. + +using System; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +public class Async2ManagedThreadId +{ + // Verify that Environment.CurrentManagedThreadId == Thread.CurrentThread.ManagedThreadId + // both before and after an await that is known to suspend/resume (Task.Delay(1)). + [Fact] + public static void TestThreadIdMatchesCurrentThread() + { + TestThreadIdMatchesCurrentThreadAsync().GetAwaiter().GetResult(); + } + + private static async Task TestThreadIdMatchesCurrentThreadAsync() + { + Assert.Equal(Environment.CurrentManagedThreadId, Thread.CurrentThread.ManagedThreadId); + await Task.Delay(1); + Assert.Equal(Environment.CurrentManagedThreadId, Thread.CurrentThread.ManagedThreadId); + } + + // Verify that after an await that resumes on a specific different thread, the thread ID + // reflects the actual resumption thread, not the thread from before the await. + [Fact] + public static void TestThreadIdReflectsResumptionThread() + { + TestThreadIdReflectsResumptionThreadAsync().GetAwaiter().GetResult(); + } + + private static async Task TestThreadIdReflectsResumptionThreadAsync() + { + int threadIdBefore = Environment.CurrentManagedThreadId; + + // Switch to a brand-new dedicated thread; the continuation is guaranteed to run there. + await new SwitchToNewThread(); + + // After the await, the thread ID must always match the actual current thread. + Assert.Equal(Environment.CurrentManagedThreadId, Thread.CurrentThread.ManagedThreadId); + + // A brand-new thread has a different ID than the thread from before the await. + Assert.NotEqual(threadIdBefore, Environment.CurrentManagedThreadId); + } + + // Custom awaiter that always resumes the continuation on a brand-new dedicated thread. + private struct SwitchToNewThread : ICriticalNotifyCompletion + { + public SwitchToNewThread GetAwaiter() => this; + public bool IsCompleted => false; + public void GetResult() { } + + public void OnCompleted(Action continuation) => throw new NotImplementedException(); + + public void UnsafeOnCompleted(Action continuation) => + new Thread(_ => continuation()) { IsBackground = true }.Start(); + } +} diff --git a/src/tests/async/regression/managed-thread-id.csproj b/src/tests/async/regression/managed-thread-id.csproj new file mode 100644 index 00000000000000..3fc50cde4b3443 --- /dev/null +++ b/src/tests/async/regression/managed-thread-id.csproj @@ -0,0 +1,5 @@ + + + + +