英文:
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<T>
as the storage:
public partial class Activity
{
private static readonly AsyncLocal<Activity?> s_current = new AsyncLocal<Activity?>();
...
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<T>
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论