[英]Thread.Join method does not always return the same value when the thread has already terminated (.NET 5 / Core)
Thread.Join
方法具有三個重載: Join()
、 Join(Int32)
和Join(TimeSpan)
。 對於這三個重載中的每一個, Microsoft doc 中有以下語句:
如果調用 Join 時線程已經終止,則該方法立即返回。
雖然此語句對Join()
重載有意義,但它沒有指定為Join(Int32)
和Join(TimeSpan)
返回哪個值,因此我在兩個不同的環境中測試了Int32
重載:
請注意,如果調用Join
時線程仍在運行並且在調用后終止,則 Linux/Docker 實現將返回true (如 Windows 之一)。 如果線程在調用之前終止,它只返回false 。
在我看來,無論平台如何, Join
都應該始終返回 true,那么如何解釋這種不一致的行為呢? 我錯過了什么還是 .NET 5 錯誤?
更新
正如@txtechhelp 所建議的,這里是一個 .NET Fiddle與我正在測試的確切代碼。
如果我在 Windows 10(或 .NET Fiddle)上運行此代碼,我會得到以下結果:
Starting..
Sleeping 1200..expect T1 end before join
In T1
Leaving T1
Join(100)..expect success
Join(100) success!
Done..
然后,如果我在 Docker 桌面(v. 3.1.0)上使用mcr.microsoft.com/dotnet/runtime:5.0運行此代碼,則會得到以下結果:
Starting..
Sleeping 1200..expect T1 end before join
In T1
Leaving T1
Join(100)..expect success
Join(100) failed
Done..
更新 2
實際上,經過進一步測試后,我意識到只有在Join
應用程序正在卸載時(即收到AssemblyLoadContext.Default.Unloading
事件,這是 Docker 發送的信號通知它將關閉應用程序)。
public class Program
{
public static void Main()
{
System.Runtime.Loader.AssemblyLoadContext.Default.Unloading += (arg) => { OnStopSignalReceived("application unloading"); };
}
public static void T1()
{
System.Console.WriteLine("In T1");
System.Threading.Thread.Sleep(1000);
System.Console.WriteLine("Leaving T1");
}
private static void OnStopSignalReceived(string stopSignalSource)
{
System.Threading.Thread t1 = new System.Threading.Thread(T1);
System.Console.WriteLine("Starting..");
t1.Start();
System.Console.WriteLine("Sleeping 1200..expect T1 end before join");
System.Threading.Thread.Sleep(1200);
System.Console.WriteLine("Join(100)..expect success");
if (t1.Join(100))
{
System.Console.WriteLine("Join(100) success!");
}
else
{
System.Console.WriteLine("Join(100) failed");
}
t1.Join();
System.Console.WriteLine("Done..");
}
}
此問題似乎是AssemblyLoadContext
class的底層 .NET 5 代碼中的錯誤,或者可能是文檔尚未指定的一些未定義行為; 也就是說, AssemblyLoadContext.Unloading
事件的文檔僅說明:
在卸載 AssemblyLoadContext 時發生。
鑒於您遇到的問題,這是唯一的句子,並沒有提供太多的上下文。
話雖如此,經過一番挖掘,我編寫了您提供的代碼的 2 個版本,並發現了一些處理AssemblyLoadContext.Unloading
和線程的有趣行為。
此代碼重現了您提到的錯誤:
越野車.cs
using System;
using System.Diagnostics;
using System.Threading;
using System.Runtime.Loader;
public class Program
{
static Thread t1 = new Thread(ThreadFn);
static Stopwatch sw = new Stopwatch();
public static void Main()
{
AssemblyLoadContext.Default.Unloading += ContextUnloading;
sw.Start();
Console.WriteLine("{0}ms: leaving main", sw.ElapsedMilliseconds);
}
public static void ThreadFn()
{
Console.WriteLine("{0}ms: in ThreadFn, sleeping 1s", sw.ElapsedMilliseconds);
Thread.Sleep(1000);
Console.WriteLine("{0}ms: leaving ThreadFn", sw.ElapsedMilliseconds);
}
private static void ContextUnloading(AssemblyLoadContext context)
{
Console.WriteLine("{0}ms: unloading '{1}', thread state '{2}'", sw.ElapsedMilliseconds, context, t1.ThreadState);
// possible bug/UB with t1.Start() in this function
Console.WriteLine("{0}ms: starting thread", sw.ElapsedMilliseconds);
t1.Start();
Console.WriteLine("{0}ms: calling Sleep(1200); expect thread in state '{1}' to end before join called", sw.ElapsedMilliseconds, t1.ThreadState);
Thread.Sleep(1200);
Console.WriteLine("{0}ms: calling Join(100) on thread in state '{1}'; expect 'succeeded!'", sw.ElapsedMilliseconds, t1.ThreadState);
Console.WriteLine("Join(100) {0}", (t1.Join(100) ? "succeeded!" : "failed"));
Console.WriteLine("{0}ms: done", sw.ElapsedMilliseconds);
}
}
運行該代碼會給我以下結果:
0ms: leaving main
12ms: unloading '"Default" System.Runtime.Loader.DefaultAssemblyLoadContext #0', thread state 'Unstarted'
13ms: starting thread
14ms: calling Sleep(1200); expect thread in state 'Running' to end before join called
14ms: in ThreadFn, sleeping 1s
1014ms: leaving ThreadFn
1214ms: calling Join(100) on thread in state 'Stopped'; expect 'succeeded!'
Join(100) failed
1214ms: done
您會注意到在這個有缺陷的版本中,每個點的線程 state 和時間間隔都與提供的代碼相匹配。 該錯誤發生在調用Join(Int32)
方法時; 即使文檔聲明返回值為Boolean
,其中值為:
如果線程已終止,則為
true
; 如果經過了millisecondsTimeout
參數指定的時間量后線程還沒有終止,則返回false
。
鑒於線程已Stopped
,根據ThreadState
文檔,這意味着線程要么響應了Abort
調用(在上面的代碼中沒有調用),要么如果
一個線程被終止。
並閱讀文檔Understanding System.Runtime.Loader.AssemblyLoadContext ,他們甚至注意到
注意線程競賽。 加載可以由多個線程觸發。 AssemblyLoadContext 通過將程序集自動添加到其緩存來處理線程競爭。 比賽失敗者的實例被丟棄。 在您的實現邏輯中,不要添加無法正確處理多個線程的額外邏輯。
結合所有這些,我們假設調用Join(Int32)
應該在上面的代碼中給出預期的結果true
。
所以,是的,這似乎是一個錯誤。
然而
如果將線程開始移動到Main
function 中,而不是在卸載事件處理程序中,則Default
上下文上的AssemblyLoadContext.Unloading
在線程完成之前不會被調用,然后調用Join(Int32)
當然會返回預期的結果。
在線程完成之前不會調用Unloading
事件是有道理的,因為它可以被認為是當前程序集上下文的“一部分”,但它沒有解釋為什么上面代碼中的錯誤仍然發生。
因此,雖然Join(100)
調用在下面的代碼中確實如預期那樣成功,但似乎是因為在Main
退出后沒有像預期的那樣調用AssemblyLoadContext.Unloading
,而是在線程完成后調用它,這使得上下文意義,但不一定在任何文檔中注明。
“成功”代碼:
同步錯誤.cs
using System;
using System.Diagnostics;
using System.Threading;
using System.Runtime.Loader;
public class Program
{
static Thread t1 = new Thread(ThreadFn);
static Stopwatch sw = new Stopwatch();
public static void Main()
{
AssemblyLoadContext.Default.Unloading += ContextUnloading;
sw.Start();
// Get expected result starting thread, but Unloading isn't called until AFTER the thread
// finishes, which is not the expected result according to the .NET documentation
Console.WriteLine("{0}ms: starting thread", sw.ElapsedMilliseconds);
t1.Start();
Console.WriteLine("{0}ms: leaving main", sw.ElapsedMilliseconds);
}
public static void ThreadFn()
{
Console.WriteLine("{0}ms: in ThreadFn, sleeping 1s", sw.ElapsedMilliseconds);
Thread.Sleep(1000);
Console.WriteLine("{0}ms: leaving ThreadFn", sw.ElapsedMilliseconds);
}
private static void ContextUnloading(AssemblyLoadContext context)
{
Console.WriteLine("{0}ms: unloading '{1}', thread state '{2}'", sw.ElapsedMilliseconds, context, t1.ThreadState);
Console.WriteLine("{0}ms: calling Sleep(1200); expect thread in state '{1}' to end before join called", sw.ElapsedMilliseconds, t1.ThreadState);
Thread.Sleep(1200);
Console.WriteLine("{0}ms: calling Join(100) on thread in state '{1}'; expect 'succeeded!'", sw.ElapsedMilliseconds, t1.ThreadState);
Console.WriteLine("Join(100) {0}", (t1.Join(100) ? "succeeded!" : "failed"));
Console.WriteLine("{0}ms: done", sw.ElapsedMilliseconds);
}
}
運行該代碼會給我以下結果:
0ms: starting thread
11ms: leaving main
12ms: in ThreadFn, sleeping 1s
1012ms: leaving ThreadFn
1013ms: unloading '"Default" System.Runtime.Loader.DefaultAssemblyLoadContext #0', thread state 'Stopped'
1014ms: calling Sleep(1200); expect thread in state 'Stopped' to end before join called
2214ms: calling Join(100) on thread in state 'Stopped'; expect 'succeeded!'
Join(100) succeeded!
2215ms: done
您會注意到直到線程完成后才會調用Unload
事件。
在撰寫此答案時, AssemblyLoadContext
class和346 個已關閉存在 82 個未解決的錯誤。 因此,您的問題可能已經以某種方式被注意到,但是粗略的搜索並沒有產生任何可能與您的問題相關的內容。
由於這似乎是一個合法的錯誤,並且由於您對代碼和正在發生的事情有更深入的了解,我建議您訪問他們的問題頁面並提交一個新問題。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.