共用方式為


EventSource 活動識別碼

本文適用於:✔️.NET Core 3.1 和更新版本 ✔️ .NET Framework 4.5 和更新版本

本指南說明活動識別碼,其為選用的識別碼,使用 System.Diagnostics.Tracing.EventSource 所產生的每個事件的識別碼,都可記錄下來。 如需取得簡介,請參閱開始使用 EventSource

記錄並行工作的挑戰

很久以前,一般的應用程式都較簡單且為單一執行緒,讓記錄能相當簡單明瞭。 您可以依序將每個步驟寫入記錄檔,然後完全以寫入記錄一樣的順序來讀取記錄檔,即可了解所發生的情況。 如果應用程式會處理要求,則一次只會處理一個要求。 要求 A 的所有記錄訊息,都會依序列印,然後列印 B 的所有訊息,依此類推。 當應用程式變成多執行緒時,該策略就不再有效,因為同時會處理多項要求。 不過,如果每個要求都指派到整個來處理它的單一執行緒,就可以藉由記錄下每個記錄訊息的執行緒識別碼,解決掉該問題。 例如,多執行緒應用程式可能會記錄:

Thread Id      Message
---------      -------
12             BeginRequest A
12             Doing some work
190            BeginRequest B
12             uh-oh error happened
190            Doing some work

藉由讀取執行緒識別碼,您知道執行緒 12 正在處理要求 A,而執行緒 190 正在處理要求 B,因此「糟了,發生錯誤了」訊息是與要求 A 相關。不過,應用程式並行作業仍然繼續變得更加複雜。 現在很常使用 asyncawait,讓單一要求可以在許多不同的執行緒上進行部分處理,直到工作完成為止。 執行緒識別碼已不足以將針對一個要求所產生的所有訊息,彼此相互關聯。 活動識別碼可解決此問題。 這些識別碼提供更精細的識別碼,可追蹤個別要求或部分要求,而不論工作是否分散到不同的執行緒。

注意

此處所說的活動識別碼概念,儘管與 System.Diagnostics.Tracing.Activity 有類似的名稱,但彼此並不相同。

使用活動識別碼追蹤工作

您可以執行下列程式碼,查看運作中的活動追蹤。

using System.Diagnostics.Tracing;

public static class Program
{
    public static async Task Main(string[] args)
    {
        ConsoleWriterEventListener listener = new ConsoleWriterEventListener();

        Task a = ProcessWorkItem("A");
        Task b = ProcessWorkItem("B");
        await Task.WhenAll(a, b);
    }

    private static async Task ProcessWorkItem(string requestName)
    {
        DemoEventSource.Log.WorkStart(requestName);
        await HelperA();
        await HelperB();
        DemoEventSource.Log.WorkStop();
    }

    private static async Task HelperA()
    {
        DemoEventSource.Log.DebugMessage("HelperA");
        await Task.Delay(100); // pretend to do some work
    }

    private static async Task HelperB()
    {
        DemoEventSource.Log.DebugMessage("HelperB");
        await Task.Delay(100); // pretend to do some work
    }
}

[EventSource(Name ="Demo")]
class DemoEventSource : EventSource
{
    public static DemoEventSource Log = new DemoEventSource();

    [Event(1)]
    public void WorkStart(string requestName) => WriteEvent(1, requestName);
    [Event(2)]
    public void WorkStop() => WriteEvent(2);

    [Event(3)]
    public void DebugMessage(string message) => WriteEvent(3, message);
}

class ConsoleWriterEventListener : EventListener
{
    protected override void OnEventSourceCreated(EventSource eventSource)
    {
        if(eventSource.Name == "Demo")
        {
            Console.WriteLine("{0,-5} {1,-40} {2,-15} {3}", "TID", "Activity ID", "Event", "Arguments");
            EnableEvents(eventSource, EventLevel.Verbose);
        }
        else if(eventSource.Name == "System.Threading.Tasks.TplEventSource")
        {
            // Activity IDs aren't enabled by default.
            // Enabling Keyword 0x80 on the TplEventSource turns them on
            EnableEvents(eventSource, EventLevel.LogAlways, (EventKeywords)0x80);
        }
    }

    protected override void OnEventWritten(EventWrittenEventArgs eventData)
    {
        lock (this)
        {
            Console.Write("{0,-5} {1,-40} {2,-15} ", eventData.OSThreadId, eventData.ActivityId, eventData.EventName);
            if (eventData.Payload.Count == 1)
            {
                Console.WriteLine(eventData.Payload[0]);
            }
            else
            {
                Console.WriteLine();
            }
        }
    }
}

執行此程式碼時,會列印輸出:

TID   Activity ID                              Event           Arguments
21256 00000011-0000-0000-0000-00006ab99d59     WorkStart       A
21256 00000011-0000-0000-0000-00006ab99d59     DebugMessage    HelperA
21256 00000012-0000-0000-0000-00006bb99d59     WorkStart       B
21256 00000012-0000-0000-0000-00006bb99d59     DebugMessage    HelperA
14728 00000011-0000-0000-0000-00006ab99d59     DebugMessage    HelperB
11348 00000012-0000-0000-0000-00006bb99d59     DebugMessage    HelperB
11348 00000012-0000-0000-0000-00006bb99d59     WorkStop
14728 00000011-0000-0000-0000-00006ab99d59     WorkStop

注意

有一個已知問題,Visual Studio 偵錯工具可能會導致產生不正確的活動識別碼。 可以不要在偵錯工具下執行此範例,或是在 Main 的開頭設定中斷點,然後在目前的視窗中,評估運算式 'System.Threading.Tasks.TplEventSource.Log.TasksSetActivityIds = false',然後再繼續,即可變通解決此問題。

使用活動識別碼可以看到識別碼為 00000011-... 的工作項目 A 之所有訊息,以及識別碼為 00000012-... 的工作項目 B 之所有訊息。 這兩個工作項目都會在執行緒 21256 上執行一些工作,但之後兩者會在不同的執行緒集區執行緒 11348 和 14728 上繼續它們的工作,因此嘗試只使用執行緒識別碼來追蹤要求,並不實際。

EventSource 有自動啟發學習法,其定義名為 _Something_Start 的事件,後面緊接著另一個名為 _Something_Stop 的事件,可被視為工作單位的開始和停止。 記錄新工作單位的開始事件,會建立新的活動識別碼,並會開始使用該活動識別碼,記錄相同執行緒上的所有事件,直到記錄到停止事件為止。 使用 asyncawait 時,識別碼也會自動遵循非同步控制流程。 雖然建議您使用開始/停止命名尾碼,但您可以使用 EventAttribute.Opcode 屬性明確地標註事件,隨意命名事件。 將第一個事件設定為 EventOpcode.Start,並將第二個設定為 EventOpcode.Stop

記錄執行平行工作的要求

有時候,單一要求可能會以平行方式執行其工作的不同部分,而您希望將所有記錄事件和子元件能歸在一起。 下列範例即模擬平行執行兩個資料庫查詢,然後對每個查詢的結果進行一些處理的要求。 您想要隔離開每個查詢的工作,但也了解執行許多並行要求時,哪些查詢屬於相同的要求。 這會模型化為樹狀結構,其中每個最上層的要求都是一個根,然後工作的不同部分是分支。 樹狀結構中的每個節點,都會取得自己的活動識別碼,而第一個記錄了新的子活動識別碼的事件,會記錄下一個稱為「相關活動識別碼」的額外欄位,以描述其父代。

執行下列程式碼:

using System.Diagnostics.Tracing;

public static class Program
{
    public static async Task Main(string[] args)
    {
        ConsoleWriterEventListener listener = new ConsoleWriterEventListener();

        await ProcessWorkItem("A");
    }

    private static async Task ProcessWorkItem(string requestName)
    {
        DemoEventSource.Log.WorkStart(requestName);
        Task query1 = Query("SELECT bowls");
        Task query2 = Query("SELECT spoons");
        await Task.WhenAll(query1, query2);
        DemoEventSource.Log.WorkStop();
    }

    private static async Task Query(string query)
    {
        DemoEventSource.Log.QueryStart(query);
        await Task.Delay(100); // pretend to send a query
        DemoEventSource.Log.DebugMessage("processing query");
        await Task.Delay(100); // pretend to do some work
        DemoEventSource.Log.QueryStop();
    }
}

[EventSource(Name = "Demo")]
class DemoEventSource : EventSource
{
    public static DemoEventSource Log = new DemoEventSource();

    [Event(1)]
    public void WorkStart(string requestName) => WriteEvent(1, requestName);
    [Event(2)]
    public void WorkStop() => WriteEvent(2);
    [Event(3)]
    public void DebugMessage(string message) => WriteEvent(3, message);
    [Event(4)]
    public void QueryStart(string query) => WriteEvent(4, query);
    [Event(5)]
    public void QueryStop() => WriteEvent(5);
}

class ConsoleWriterEventListener : EventListener
{
    protected override void OnEventSourceCreated(EventSource eventSource)
    {
        if (eventSource.Name == "Demo")
        {
            Console.WriteLine("{0,-5} {1,-40} {2,-40} {3,-15} {4}", "TID", "Activity ID", "Related Activity ID", "Event", "Arguments");
            EnableEvents(eventSource, EventLevel.Verbose);
        }
        else if (eventSource.Name == "System.Threading.Tasks.TplEventSource")
        {
            // Activity IDs aren't enabled by default.
            // Enabling Keyword 0x80 on the TplEventSource turns them on
            EnableEvents(eventSource, EventLevel.LogAlways, (EventKeywords)0x80);
        }
    }

    protected override void OnEventWritten(EventWrittenEventArgs eventData)
    {
        lock (this)
        {
            Console.Write("{0,-5} {1,-40} {2, -40} {3,-15} ", eventData.OSThreadId, eventData.ActivityId, eventData.RelatedActivityId, eventData.EventName);
            if (eventData.Payload.Count == 1)
            {
                Console.WriteLine(eventData.Payload[0]);
            }
            else
            {
                Console.WriteLine();
            }
        }
    }
}

此範例會列印輸出,如下:

TID   Activity ID                              Related Activity ID                      Event           Arguments
34276 00000011-0000-0000-0000-000086af9d59     00000000-0000-0000-0000-000000000000     WorkStart       A
34276 00001011-0000-0000-0000-0000869f9d59     00000011-0000-0000-0000-000086af9d59     QueryStart      SELECT bowls
34276 00002011-0000-0000-0000-0000868f9d59     00000011-0000-0000-0000-000086af9d59     QueryStart      SELECT spoons
32684 00002011-0000-0000-0000-0000868f9d59     00000000-0000-0000-0000-000000000000     DebugMessage    processing query
18624 00001011-0000-0000-0000-0000869f9d59     00000000-0000-0000-0000-000000000000     DebugMessage    processing query
18624 00002011-0000-0000-0000-0000868f9d59     00000000-0000-0000-0000-000000000000     QueryStop
32684 00001011-0000-0000-0000-0000869f9d59     00000000-0000-0000-0000-000000000000     QueryStop
32684 00000011-0000-0000-0000-000086af9d59     00000000-0000-0000-0000-000000000000     WorkStop

此範例只會執行一個最上層要求,指派給它的活動識別碼為 00000011-...。 然後,每個 QueryStart 事件都會以活動識別碼 00001011-...00002011-...,分別開始一個要求的新分支。 因為這兩個開始事件在 [相關活動識別碼] 欄位中,都記錄了其父 00000011-...,所以您可以識別出這些識別碼是原始要求的子系。

注意

您可能已經注意到,識別碼的數值在父代和子系之間,有一些明確的模式,而且並非隨機的。 雖然在簡單的案例中,它有助於一眼就看出關聯性,但其最適合不依賴此識別碼,並將識別碼視為不透明識別碼的工具。 當巢狀層級變深時,位元組模式將會變更。 使用 [相關活動識別碼] 欄位是確保無論巢狀層級如何,工具都能可靠運作的最佳方式。

因為具有子工作項目複雜樹狀結構的要求,會產生許多不同的活動識別碼,因此這些識別碼通常最好由工具進行剖析,而不是嘗試手動重新建構樹狀結構。 PerfView (英文) 是知道如何將標註有這些識別碼的事件,彼此相互關聯的一種工具。