Как стать автором
Обновить

Комментарии 12

Тест тестирует что-то другое.

используется Task.Yield(), чтобы задачи не блокировали поток.

Какой поток?

Или же это "костыль" для очистки стека при рекурсии всё ж?

Моё имх далее:

А рекурсия там прям от души звфигачена... Я всю жизнь избегаю её и другим советую, а тут прям мелкомягкие дали неокрепшим умам инструмент для выключения мозга. Хотя нормально на цикле решается и контролируется, и читается понятнее.

А рекурсия там прям от души звфигачена

Нету ее там. По крайнейе мере - той, что стек жрёт: после UnsafeQueueUserWorkItem пул вызывает делегат, который был передан с новым, сведим стеком для того потока, в котором делегат будет выполняться. Так что этот делегат может смело вызывать снова TryExecuteTask(), который опять вызовет UnsafeQueueUserWorkItem: ни один стек при этом не пострадает.

Или вы где-то в другом месте рекурсию нашли?

Начнём с основы. В .NET Task — это удобная абстракция над потоками.

Ну, давайте начнем с основы: задача (Task) и поток (Thread) - это две совершенно разные абстракции, берущие свое начало из разных вариантов реалиизации многозадачности: коперативной (задача) и вытесняющей (поток). Например, если бы мы писали программу на C# для Windows 3.x (или Novell Netware, или CIsco IOS), то возможностью воспользоваться потоком у нас бы не было - там многозадачность на уровне системы кооперативная, вытесняющей многозадачности нет, а поток - один на всю систему. А вот задачами вполне можно было бы пользоваться. И ими пользовалисьво всю: обработка сообщения - это именно задача.

Так что IMHO при написании статей на прикладные темы с основы начинать лучше не пытаться.

Хотите сказать, что Thread - это абстракция потока процессора в операционной системе? Минуя планировщик ядра ос?

Если нет, то абстракция чего именно тогда?

Как-то странно вы меня поняли. Поток - это абстракция уровня операционной системы. Как именно его поддерживает ядро на аппаратном уровне - внутреннее дело ядра. Но, как во всякой абстракции, в этой тоже есть дыры, и потому иногда про это внутреннее дело полезно знать.
А ещё понятие потока есть и у исполняющей системы .NET (CLR). Но ее потоки (managed thread) обычно напрямую отображаются на потоки ОС.

я поэтому и задал уточняющий вопрос.

Именно thread и именно потоки ОС - не процессора (hyper threading). Тогда согласен с вами.

На удивление простая реализация. Я думал, что для планировщику придётся как-то хитро потокам жонглировать, а тут как-то не страшно выглядит.

https://github.com/microsoft/referencesource/blob/master/mscorlib/system/threading/Tasks/ThreadPoolTaskScheduler.cs

Планировщик на тред-пуле тоже довольно простой. Большая часть логики планировщика либо в базовом классе, либо в целом не требуется, т.к. всё это просто очередь задач, которые надо выполнить.

Наконец-то нашел время прочитать статью поподробнее. Общее впечатление: как и ожидалось от рекламного материала курсов, статья демонстрирует внешне глубокое знание темы, но таковым достоинством не обладает.

Начать надо с путаницы в заголовке: статья обещает рассказать про "управление потоками в .NET", но по факту рассказывает об управлении задачами - а это таки две большие разницы (кстати, интересно было бы знать, что не понравилось тем, кто мне минусы за комментарий об этом молча понаставил). Управлением потоками .NET занимаются совсем другие компоненты: Thread позволяет управлять потоками вручную, SynchronizationContext управляет потоками, реализующими этот контекст синхронизации, ThreadPool - имеющимся в исполняющей системе .NET пулом потоков. Об этом ничего в статье не сказано - ибо она вообще на другую тему. Я бы предложил автору просто исправить заголовок: статья рассказывает про планировщик задач - значит, в заголовке должно быть "управление задачами в .NET".

Далее, собственно о содержимом статьи. Она пытается рассказать о том, что

иногда нужно больше контроля над задачами. Например:

  • Ограничить число одновременно выполняемых задач.

  • Установить приоритет выполнения.

  • Выполнить задачи в специфическом контексте (например, в UI-потоке).

Намерение полезное - но до конца оно не выполнено.А именно - последний пункт. Под "специфическим контекстом", судя по написанному дальше, подразумевается контекст синхронизации (SynchronizationContext), и по этому, на практике, местами - очень важному - вопросу в статье нет ничего. Хотя, казалось бы, чего сложного: написать пару предложений (а то и парой слов ограничиться)про метод TaskScheduler.FromCurrentSynchronizationContext - и всё, вопрос закрыт. А так получилось, что тема выполнения задачи в UI-потоке не раскрыта.

Ну и далее - по примерам. Точнее - по первому из них (второй не смотрел - одного хватило). Пример, на мой взгляд, написан всеьма неаккуратно. Во-первых: что делает в делегате, передаваемом в ThreadPool.UnsafeQueueUserWorkItem, операция await Task.Yield() - из-за которой делегат пришлось сделать асинхронным? В статье это оправдывается фразой "используется Task.Yield(), чтобы задачи не блокировали поток", но ведь этот поток специально только что выбран именно для того, чтобы выполнить именно этот делегат, и никакой другой работы у него нет. Так почему бы ему сразу не заняться выполнением делегата, запускающего и выполняющего задачу? Я причин не вижу. Так что операция await Task.Yield() выглядит излишней. Ведь await что делает, если пренебречь оптивизациями? Проверяет что ожидаемое ещё не выпонилось, и если не выполнилось - а для Task.Yield() оно точно не выполнилось - кидает в пул потоков (ибо контекста синхронизации у нас тут нет) делегат, продолжающий выполнение после await, и быстренько возвращается. То есть в отутствии оптимизации эта конструкция просто повторно передает тот же самый по сути рабочий элемент в пул потоков на выполнение. В реальности сейчас await на стандартных операциях сильно оптимизирован, и вряд ли (проверять, честно говоря, лень, так это или нет) дело дойдет до повторного помещения в пул путоков, то есть вред, скорее всего, окажится минимизирован - но ведь и пользы-то с этого никакой. Ну и, все эти возвраты в пул потоков после того, как await встал на ожидание - они ведь тоже какого-то времени на обработку требуют, создавая заведомо лишнюю нагрузку (пусть и небольшую). Короче, неаккуратно получается.

Ну и вообще, само по себе использование ThreadPool.UnsafeQueueUserWorkItem вместо QueueUserWorkItem вызывает вопрсы: а нам точно не нужно передавать задаче текущий контекст выполнения (ExecutionContext)? Я понимаю, чем это могло быть плохо в .NET Framework - там в ExecutionContext входил, в частности, SynchronizationContext, передавать который в задачу было не всегда желательно (ибо можно было нарваться на deadlock), но сейчас-то это не так. Не передавая же ExecutionContext, мы лишаемся всех AsyncLocal переменных - а ведь там могут быть такие полезные вещи, как ссылка на HttpContext для реализации IHttpContextAccessor. И разработчик, делая часть обработки запроса HTTP в ASP.NET Core как задачу под таким планировщиком, вряд ли обрадуется, что эта задача не будет иметь ожидаемого доступа к контексту запроса. В общем - тоже неаккуратно сделано.

Короче, в статье автор прошелся по верхам и продемонстрировал из обсуждаемой темы кое-что (и кое-как).
Так что, если бы это была обычная техническая статья, я поставил бы ей минус за низкий технический уровень материала. Но для рекламной статьи такой недостаток не критичен. У нее - другая задача: привлечь на курсы платежеспособных слушателей. Я таковым заведомо не являюсь, а потому не буду мешать людям мутить лавэшку.

Но если автор считает мою критику технических аспектов несправедливой - я готов обсудить это с ним в комментариях.

PS Вообще-то, в том, что касается потери ExecutionContext, я, возможно, сгустил краски: все-таки рабочий элемент, который выполняется пулом потоков, не выполняет тело задачи напрямую, а вызывает для выпонения задачи метод инфраструктуры TPL, (в конечном итоге, Task.ExecuteEntry), который, возможно(так это или нет - я не смотрел), восстанавливает ExecutionContext , и ничего страшного не происходит. Но, в любом случае, я бы в своей статье не стал обходить этот момент молчанием.

Не пробовали глубину параллельности с помощью Семафоров сделать? Может красивее бы получилось.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий