Pull to refresh

Comments 11

Что-то странное тут написано...


Этот код называется IO-bound. Если мы будем ожидать оборудование на потоках пула, это приведёт к тому, что часть потоков в пуле перестенут быть рабочими на время блокировки. Это как минимум испортит пулу потоков статистики которые он считает

Единственная причина, по которой пул потоков считает свои "статистики" — это возможность блокирующих вызовов в потоках пула. Каким таким интересным образом условный счётчик заблокированных потоков может "испортиться" от использования по назначению?


Для нас это значит, что в делегате, работающем в ThreadPool вставать блокировку нельзя. Он для этого не предназначен.

Что значит "нельзя", когда это штатный режим работы? Что значит "не предназначен", когда блокирующие вызовы — единственная причина, по которой в пуле бывает потоков больше чем у процессора ядер?

Единственная причина, по которой пул потоков считает свои "статистики" — это возможность блокирующих вызовов в потоках пула. Каким таким интересным образом условный счётчик заблокированных потоков может "испортиться" от использования по назначению?

Вы так категоричны, будто смотрели его код. "Статистиками" ThreadPool является алгоритм предсказания уровня необходимого параллелизма исходя из данных алгоритма Hill Climbing, работающего поверх дискретного преобразования Фурье алгоритмом Гёрцеля. Соответственно данные эти используются для того чтобы понять, сколько потоков необходимо создать. Это прямо влияет на уровень загруженности CPU, что влияет на производительность всей остальной системы. Соответственно его задача -- правильно предсказывать оптимальный уровень параллелизма. Чего он не делает. На определенном уровне количества работы стандартный пул потоков работает в сумме по всем потокам -- медленнее чем собственная реализация в разы. Да и линейно также проигрывает. Блокирующие вызовы тут не при чём: они -- скорее возможность (см. статью) отделить CPU-/IO-bound операции на слое ОС.

Что значит "не предназначен", когда блокирующие вызовы — единственная причина, по которой в пуле бывает потоков больше чем у процессора ядер?

Вы правы относительно предложенной модели компанией Microsoft но абсолютно не правы в целом. Например, в Java нет "стандартного пула потоков". Там их много и у каждого свой алгоритм. То, что сделали work-around не значит, что вставать в блокировку на пуле потоков надо. Вы сталкивались с ситуацией полностью заблокированного пула потоков, когда все потоки там вошли в блокировку? Это значит что пул потоков расширяется до некоторого максимума. Это значит, что каждый последующий вошедший в блокировку поток снижает его пропускную способность и как следствие -- производительность приложения. Наличие workaround'a в ThreadPool для тех, кто не понимает, что так делать не стоит, но чтобы помочь им: чтобы приложение по прежнему отрабатывало. Аналогичный workaround в пуле с авторазблокированием пула в .NET 6. Я против таких workaound'ов. По мне так лучше пусть разработчик учит мат часть. Расплодили async/await и прочих "сахаров" да помощников. Выглядит красиво, а что это по факту никто по сути сказать не может.

ForkJoinPool в Java:

Причём тут вообще ForkJoinPool в Java?


Блокирующие вызовы тут не при чём

Вот именно, ни при чём они.


По мне так лучше пусть разработчик учит мат часть. Расплодили async/await и прочих "сахаров" да помощников. Выглядит красиво, а что это по факту никто по сути сказать не может.

А что не так с async/await? Так-то эта штука по-эффективнее RegisterWaitForSingleObject будет...

Блокирующий вызов увеличивает время работы метода, исключая возможность равномерно нагрузить пул: метод типа работает, а по факту не использует CPU.

Причём тут вообще ForkJoinPool в Java?

Алгоритмов пулинга потоков великое множество. В Microsoft сделали пул по-умолчанию и абстракцию SynchronizationContext, которая своим названием скорее путает чем помогает понять что это -- слой абстракции над неким пулом потоков. Поэтому у нас путаное знание о пулах потоков. Он у нас один и нигде не описано как он работает. Это -- проблема, порождающая заблуждения, что блокировки на пуле потоков -- это нормально.

А что не так с async/await?

Один человек из тысячи понимает как он работает. Потому что выглядит просто: как что-то, что не надо изучать, как это работает. А по факту внутри ящик пандоры.

SynchronizationContext — это абстракция не столько над пулом потоков, сколько над очередью сообщений. Абстракцией над пулом потоков можно считать скорее TaskScheduler.

Один человек из тысячи понимает как он работает. Потому что выглядит просто: как что-то, что не надо изучать, как это работает. А по факту внутри ящик пандоры.

Не стоит недооценивать важность абстракций. А абстракции как раз подразумевают абстрагирование от деталей.

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

Когда вы забиваете гвозди, никто не требует от вас знать свойства древесины рукояти молотка и его сплава.

Если бы использование абстракций не было экономически оправдано, ими бы не пользовались. Разработка ПО - это не просто игра программистов.

По мне так лучше пусть разработчик учит мат часть. Расплодили async/await и прочих "сахаров" да помощников. Выглядит красиво, а что это по факту никто по сути сказать не может.

Правильно. Только Assembler, только хардкор...

Вы написали, что создание треда — это дорогостоящая операция. Мне немного неловко спрашивать, но я не вижу у вас цифр. Не могли бы вы приложить? Например, один человек померил (может быть некрасивым способом) создание тредов, и оказалось, что цена — около 70 микросекунд. Мне бы хотелось посмотреть на ваши измерения, если можно.

ну совсем грубо -- от 0,3 до 1,0 млн итераций цикла сложения двух чисел. Это значит, что до определенного объёма работ -- когда издержки в 0,3 - 1,0 млн итераций более 5% от общего объёма работ -- это будет дорого. Это по сути значит, что чтобы %% по издержкам сравнялись с ThreadPool, длина методов должна стать больше в 300 раз (см. данные по таблицам). Иначе запускать таким образом -- через `new Thread().. Start()` станет дорого.

Спасибо за ваш комментарий. Тогда получается, что вы сравниваете время, затрачиваемое на создание треда, с вызовом одной инструкции процессора, складывающей два числа. Если так, то я с вами абсолютно согласен — это дорогостоящая операция.
Просто потом вы переходите на I/O операции, которые могут выполнятся от миллисекунд до секунд, и получается что для них создание нового потока тоже дорогостоящая операция, требующая пула с потоками.
Для CPU-bound операций вы упоминаете, что количество потоков (складывающих числа) не должно превышать число ядер процессора. Мне здесь немного не понятно, почему вы не оставляете ресурсов для операционной системы и самого приложения? Есть ли какое-либо менее размытое ограничение, допустим 80% или 70% от числа ядер?
Спасибо.

Тогда получается, что вы сравниваете время, затрачиваемое на создание треда, с вызовом одной инструкции процессора, складывающей два числа

от 0,3 до 1,0 млн итераций цикла сложения двух чисел

С миллионом.

Sign up to leave a comment.

Articles