簡體   English   中英

當線程已經終止(.NET 5 / Core)時,Thread.Join 方法並不總是返回相同的值

[英]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重載:

  1. Windows 10:返回
  2. Linux/Docker:返回false (在 Docker 桌面上使用mcr.microsoft.com/dotnet/runtime:5.0

請注意,如果調用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 發送的信號通知它將關閉應用程序)。

所以這是在 .NET Fiddle 上甚至失敗的確切測試

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);
        }
}

在 dotnetfiddle 中運行它

運行該代碼會給我以下結果:

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);
        }
}

在 dotnetfiddle 中運行它

運行該代碼會給我以下結果:

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 class346 個已關閉存在 82 個未解決的錯誤。 因此,您的問題可能已經以某種方式被注意到,但是粗略的搜索並沒有產生任何可能與您的問題相關的內容。

由於這似乎是一個合法的錯誤,並且由於您對代碼和正在發生的事情有更深入的了解,我建議您訪問他們的問題頁面並提交一個新問題

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM