可以安全地从同一线程的多个并行任务中使用Activity.Current吗?

huangapple go评论54阅读模式
英文:

Is it safe to use Activity.Current from multiple parallel tasks on the same thread?

问题

在尝试实现使用变量的W3C跟踪时,例如Activity.Current.SpanId,出现了以下问题。

对于静态属性Activity.Current官方文档如下所示:

获取或设置当前线程的当前操作(Activity)。这会在异步调用之间传递。

我不完全确定如何解释在异步调用之间传递当前线程的关系。使用现代C#,在使用任务时,我们应该不再关心线程。因此,我期望在多个工作任务内执行using workActivity = new Activity($"Work {id}").Start()时,该活动实例应该始终由Activity.Current返回,即使多个任务在同一(线程池)线程上运行。

这是否始终成立?

实际上,这涉及到线程上下文和任务上下文之间的区别(如果存在这样的概念)。Activity.Current是线程上下文的一部分还是任务上下文的一部分?或者如何才能使Activity.Current始终反映每个并行任务的正确值?

我还使用以下示例代码进行了一些快速调查:

using System.Diagnostics;

namespace DummyConsoleApp
{
    internal class Program
    {
        static void PrintTraceInfo(string msg, int? workerId = null)
        {
            Debug.Assert(Activity.Current != null, "Activity.Current != null");
            Console.WriteLine($"Worker {workerId} on thread {Thread.CurrentThread.ManagedThreadId} with trace {Activity.Current.TraceId} {Activity.Current.ParentSpanId} --> {Activity.Current.SpanId}: {msg}\n");
        }

        static async Task Work(int id)
        {
            using var workActivity = new Activity($"Work {id}").Start();
            PrintTraceInfo("Work started", id);
            await Task.Delay(3000);
            PrintTraceInfo("Work ended", id);
        }

        static async Task Main(string[] args)
        {
            using var activity = new Activity("Main").Start();
            PrintTraceInfo("Main started.");
            var tasks = new List<Task>();
            for (int i = 0; i < 100; i++)
            {
                var id = i;
                var task = Task.Run(() => Work(id));
                tasks.Add(task);
            }
            await Task.WhenAll(tasks).ConfigureAwait(false);
            PrintTraceInfo("Main ended.");
        }
    }
}

一切似乎按预期工作,即使在相同线程上的任务也会输出其各自的ID。但再次强调,这只是一个小测试。

有人有更多关于这个问题的信息吗?Activity.Current是否可以安全地从并行任务中访问?

英文:

When trying to implement W3C tracing which makes use of variables, such as e.g. Activity.Current.SpanId, the following question came up.

The official documentation for the static property Activity.Current says:
> Gets or sets the current operation (Activity) for the current thread. This flows across async calls.

I am not entirely sure how to interpret flows across async calls in view of current thread. With modern C#, we should not care about threads anymore when using tasks. So I would expect that when I do a using workActivity = new Activity($"Work {id}").Start() inside multiple worker tasks, this activity instance should always be returned by Activity.Current, even when multiple tasks are run on the same (thread pool) thread.

Is that always true?

It actually boils down to the difference between thread context and task context (if such a thing even exists). Is Activity.Current part of the thread context or part of the task context? Or how can Activity.Current always reflect the right value for each parallel task?

I also did some quick investigation using this sample code:

using System.Diagnostics;

namespace DummyConsoleApp
{
    internal class Program
    {
        static void PrintTraceInfo(string msg, int? workerId = null)
        {
            Debug.Assert(Activity.Current != null, "Activity.Current != null");
            Console.WriteLine($"Worker {workerId} on thread {Thread.CurrentThread.ManagedThreadId} with trace {Activity.Current.TraceId} {Activity.Current.ParentSpanId} --> {Activity.Current.SpanId}: {msg}\n");
        }

        static async Task Work(int id)
        {
            using var workActivity = new Activity($"Work {id}").Start();
            PrintTraceInfo("Work started", id);
            await Task.Delay(3000);
            PrintTraceInfo("Work ended", id);
        }

        static async Task Main(string[] args)
        {
            using var activity = new Activity("Main").Start();
            PrintTraceInfo("Main started.");
            var tasks = new List<Task>();
            for (int i = 0; i < 100; i++)
            {
                var id = i;
                var task = Task.Run(() => Work(id));
                tasks.Add(task);
            }
            await Task.WhenAll(tasks).ConfigureAwait(false);
            PrintTraceInfo("Main ended.");
        }
    }
}using System.Diagnostics;

namespace DummyConsoleApp
{
    internal class Program
    {
        static void PrintTraceInfo(string msg, int? workerId = null)
        {
            Debug.Assert(Activity.Current != null, "Activity.Current != null");
            Console.WriteLine($"Worker {workerId} on thread {Thread.CurrentThread.ManagedThreadId} with trace {Activity.Current.TraceId} {Activity.Current.ParentSpanId} --> {Activity.Current.SpanId}: {msg}\n");
        }

        static async Task Work(int id)
        {
            using var workActivity = new Activity($"Work {id}").Start();
            PrintTraceInfo("Work started", id);
            await Task.Delay(3000);
            PrintTraceInfo("Work ended", id);
        }

        static async Task Main(string[] args)
        {
            using var activity = new Activity("Main").Start();
            PrintTraceInfo("Main started.");
            var tasks = new List<Task>();
            for (int i = 0; i < 100; i++)
            {
                var id = i;
                var task = Task.Run(() => Work(id));
                tasks.Add(task);
            }
            await Task.WhenAll(tasks).ConfigureAwait(false);
            PrintTraceInfo("Main ended.");
        }
    }
}

Everything seems to work as expected and even tasks on the same thread will output their individual id. But then again this is only a little test.

Does anybody have more information on this? Can Activity.Current always be safely accessed from parallel tasks?

答案1

得分: 3

Activity.Current 使用 AsyncLocal<T> 作为存储:

public partial class Activity
{
    private static readonly AsyncLocal<Activity?> s_current = new AsyncLocal<Activity?>();

来自 AsyncLocal 备注:

因为基于任务的异步编程模型 tends to abstract the use of threads,AsyncLocal 实例可以用于在不同线程之间持久化数据。

AsyncLocal 类还在当前线程的关联值发生更改时提供可选通知,无论是因为通过设置 Value 属性进行了显式更改,还是因为线程遇到了 await 或其他上下文切换而发生了隐式更改。

因此,它使用 "任务上下文"。要查看 "线程上下文" 的等效部分,请参阅 ThreadLocal<T>

英文:

If we do a bit of decompiling we can see that Activity.Current uses AsyncLocal&lt;T&gt; as the storage:

public partial class Activity
{
        private static readonly AsyncLocal&lt;Activity?&gt; s_current = new AsyncLocal&lt;Activity?&gt;();
...

And from the AsyncLocal remarks
> Because the task-based asynchronous programming model tends to abstract the use of threads, AsyncLocal<T> instances can be used to persist data across threads.
>
>The AsyncLocal<T> class also provides optional notifications when the value associated with the current thread changes, either because it was explicitly changed by setting the Value property, or implicitly changed when the thread encountered an await or other context transition.

So it uses "task context". For the "thread context" equivalent, see ThreadLocal&lt;T&gt;

huangapple
  • 本文由 发表于 2023年3月3日 17:53:12
  • 转载请务必保留本文链接:https://go.coder-hub.com/75625535.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定