Ескертпе
Бұл бетке кіру үшін қатынас шегін айқындау қажет. Жүйеге кіруді немесе каталогтарды өзгертуді байқап көруге болады.
Бұл бетке кіру үшін қатынас шегін айқындау қажет. Каталогтарды өзгертуді байқап көруге болады.
Эта статья относится к: ✔️ .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, поэтому сообщение об ошибкеuh-oh было связано с запросом A. Тем не менее, параллельная обработка в приложениях становится все сложнее. Теперь обычно используют async и await, чтобы один запрос мог частично обрабатываться в различных потоках до полного завершения работы. Идентификаторов потоков больше недостаточно для корреляции всех сообщений, созданных для одного запроса. Идентификаторы действий решают эту проблему. Они предоставляют более точный идентификатор зерна, который может отслеживать отдельные запросы или части запросов независимо от того, распространяется ли работа по разным потокам.
Замечание
Понятие идентификатора действия, называемое здесь, не совпадает с именем 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' в окне немедленных вычислений, чтобы обойти проблему прежде чем продолжить работу.
Используя идентификаторы действий, вы можете увидеть, что все сообщения для рабочего элемента A имеют идентификатор 00000011-..., а все сообщения для рабочего элемента B имеют идентификатор 00000012-.... Оба рабочих элемента сначала работали над потоком 21256, но затем каждый из них продолжал свою работу над отдельными потоками threadpool 11348 и 14728, поэтому попытка отслеживать запрос только с идентификаторами потоков не работала.
EventSource имеет автоматическую эвристику, при которой определение события с именем _Something_Start, сразу следующим за событием с именем _Something_Stop, считается началом и завершением единицы работы. Ведение журнала события начала для новой единицы работы создает новый идентификатор действия и начинает регистрирование всех событий в одном потоке с этим идентификатором действия до тех пор, пока событие остановки не будет зарегистрировано. Идентификатор также автоматически следует асинхронным потокам управления при использовании async и await. Хотя мы рекомендуем использовать суффиксы именования "начало/остановка", вы можете называть события как вам угодно, явно аннотируя их с помощью свойства 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 — это одно средство, которое знает, как сопоставлять события, аннотированные с этими идентификаторами.