A Few Words on Task.Id (and TaskScheduler.Id)
• CommentsThere are some Id
properties in TPL types (notably Task.Id
and TaskScheduler.Id
); these “identifiers” follow the same pattern. I believe their primary use case is for ETW events, though they may have other uses.
Generated On-Demand
Identifiers are generated on-demand. So if you don’t read the properties and don’t have ETW tracing on, then your tasks (and task schedulers) won’t actually have identifiers. They generate them right when they need them.
Invalid/Unassigned Value
The value 0
is never used. This is technically undocumented, but it’s pretty safe to assume. The ETW events all produce plain int
s for task and task scheduler identifiers, and some ETW events need a value for “none” (e.g., OriginatingTaskId
needs to support a value meaning “there was no originating task”).
This means you’ll never actually see a zero value as an identifier. A task (or task scheduler) can internally have a zero identifier (meaning “unassigned”) but will generate an actual identifier if that value is ever read.
Incidentally, Task.CurrentId
is a bit different than TaskScheduler.Current.Id
. Task.CurrentId
will return null
when there is no task currently executing. TaskScheduler.Current.Id
will return the (real) identifier of the “current” TaskScheduler
; if there’s no task executing, the “current” scheduler is the default (thread pool) scheduler.
But either way, you won’t see a zero value.
Per-Type
Identifiers have meaning only for a particular type. For example, the first assigned Task
identifier is one, and the first assigned TaskScheduler
identifier is one. So the identifier “one” has no meaning by itself; the identifiers are not allocated from a shared pool or anything like that.
Not Quite Unique
The “identifiers” are not unique. They’re pretty close (they’ll repeat very rarely), but they’re not actually unique.
The MSDN documentation states the identifiers are unique. The MSDN documentation is wrong.
This can be easily proven with a simple test (also on gist) where we first create one Task
and then repeatedly create additional Task
instances until we find one where the identifiers are the same (though the task instances are different):
This program takes about 3 minutes on my machine to observe an identifier collision. The output is:
Id collision!
task.Id == other.Id: True
task == other: False
Note that Saw Id of 0!
is not in the output; the task identifiers worked their way through all possible int
values but skipped over zero.
Probably no one will write a program that has four billion Task
instances simultaneously, but it’s not uncommon for a few Task
instances to be long-lived and most of them short-lived. So if you have a long-lived Task
instance in a long-running program, be aware that its identifier may be reused while the long-lived task is still alive. Note that this is the common case! The example program above illustrates this: it only has one long-lived task; all the other tasks are eligible for garbage collection shortly after they’re created.
So, be aware that identifiers are not strictly unique. Some developers have attempted to “attach” data to a task using a concurrent dictionary with task identifiers as the key. But this will not work for most long-running programs.
When developers try to attach data to tasks, they’re usually trying to figure out some kind of “ambient context” for asynchronous operations. I cover the correct way to do that in a separate post. If you really, seriously do need to attach data to tasks and you can’t derive from Task
for whatever reason, you can use Connected Properties.
Identifiers in Nito.AsyncEx
I have a number of types in my AsyncEx library where I need a similar sort of semi-unique identifier (primarily for logging purposes). So I follow the same pattern as the built-in framework identifiers: generated on demand, zero as an invalid/unassigned value, and allocated by-type. I use a helper class called IdManager (not exposed in the public API) to satisfy this pattern.
You’re welcome to use this type in your own code if you need to. The design may appear a little unusual to .NET developers because it uses a generic tag type. Conceptually, IdManager<Tag>
actually defines a set of types, each with their own “namespace” for identifiers. The generic parameter Tag
is completely unused by IdManager<Tag>
; its only purpose is to partition the static members.
This is a common code pattern in C++, a language which has much greater support for generic programming than C#. This kind of pattern is not at all common in the .NET world, and in fact StyleCop will complain about this class. But it makes perfect sense if you think about generic arguments as actual arguments that you pass to the type.