Мой вопрос связан с этим вопросом. Код в этом вопросе генерировал несколько потоков внутри цикла, и ОП там заметил, что идентификатор потока в их журнале, похоже, со временем увеличивается. Этот вопрос был о Java, но он заставил меня задуматься: как JVM и .NET Framework вообще назначают идентификаторы потоков? Меня особенно интересуют такие случаи, как OP, описанные в его сообщении (для проверки того, что потоки действительно создаются и уничтожаются, как и ожидалось), а также для получения дополнительного контекста о том, как интерпретировать журналы средств диагностики Visual Studio. Меня также интересует мое собственное понимание того, как работает структура.
Я спрашиваю здесь в первую очередь о .NET Framework, потому что, вероятно, было бы слишком широко спрашивать об обоих сразу (хотя я определенно был бы рад услышать подробности и о JVM). Но вот пример журнала, который я получал на вкладке «События» средств диагностики Visual Studio:
Вывод программы: поток 0x44c завершился с кодом 0 (0x0).
Вот несколько идентификаторов потоков по порядку:
0xcb0
0x2c4c
0x2c5c
0x1b10
0x1a60
0x27b4
0x2b80
0x2e04
Они не кажутся особенно последовательными. Этот журнал сам по себе не является ужасно информативным без, например, получения большего контекста из окна потоков в Visual Studio, поэтому я надеюсь, что понимание того, как они назначаются в первую очередь, даст немного больше контекста для этих событий.
Вот пример кода, который я использую:
await jobs.AsyncForEach(async delegate (Job job)
{
// Do some stuff, some of which involves async/await HttpClient calls to a RESTful API
}, GlobalSettings.maxDegreeOfParallelism);
jobs
имеет тип List<Job>
, GlobalSettings.maxDegreeOfParallelism
— это const int
, указывающий максимальную степень параллелизма (из-за ограничения API от нашего поставщика), а AsyncForEach
— это метод расширения для IEnumerable<T>
:
public static async Task AsyncForEach<T>(this IEnumerable<T> enumerable, Func<T, Task> action, int degreeOfParallelism)
{
List<Task> tasks = new List<Task>();
foreach (T item in enumerable)
{
if (tasks.Count >= degreeOfParallelism)
{
await Task.WhenAny(tasks);
tasks = tasks.Where(t => !t.IsCompleted).ToList();
}
Task actionTask = action(item);
tasks.Add(actionTask);
}
await Task.WhenAll(tasks);
}
В настоящее время это может работать в одной из трех сред: приложение WPF, консольное приложение или модульный тест. Журнал, который я показал здесь, взят из запуска модульного теста, но журналы консольного приложения кажутся очень похожими.
Я понимаю, что async
/await
работает несколько иначе в подобных случаях, и что нет явных гарантий относительно того, в каком потоке (потоках) будет выполняться асинхронный код, если нет контекста синхронизации; тем не менее, я не назначаю явно какой-либо код собственному потоку с new Thread
, ThreadPool.QueueUserWorkItem
или Task.Run
в любой точке этого кода.
Когда я искал это в Google, я увидел документацию о разнице между идентификатором управляемого потока, а также о том, как получить идентификатор из потока. Однако на самом деле они не отвечают на вопрос, как .NET Framework вообще их придумала.
Я также хорошо знаком с окном Visual Studio Threads, которое показывает идентификатор, связанный процесс, управляемый идентификатор, категорию, имя и расположение потоков. Это также не совсем отвечает на вопрос о том, как структура назначила их для начала.