Являются ли сопрограммы упреждающими или просто блокируют поток, который выбрал запускаемый?

#multithreading #kotlin #kotlin-coroutines

Вопрос:

покопавшись немного в реализациях диспетчеров сопрограмм, таких как «По умолчанию» и «Ввод-вывод», я вижу, что они просто содержат Java-исполнитель (который представляет собой простой пул потоков) и очередь исполняемых файлов, которые являются логическими блоками сопрограммы.

давайте рассмотрим пример сценария, в котором я запускаю 10 000 сопрограмм в одном и том же контексте сопрограммы, например, диспетчер «По умолчанию», который содержит исполнителя с 512 реальными потоками в своем пуле.

эти сопрограммы будут добавлены в очередь диспетчера (в случае, если количество сопрограмм в полете превысит максимальное пороговое значение).

предположим, например, что первые 512 сопрограмм, которые я запустил из 10 000, действительно медленные и тяжелые.

будут ли остальные мои сопрограммы заблокированы до тех пор, пока не завершится хотя бы 1 из моих реальных потоков, или в этих «потоках пользовательского пространства»есть какой-то механизм сокращения времени?

Ответ №1:

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

Но, как вы заметили, у него есть недостатки. При выполнении длительных вычислений с интенсивным использованием процессора рекомендуется время от времени вызывать функцию yield (). Это позволяет освободить поток для других сопрограмм. Другим решением является создание отдельного пула потоков для наших вычислений, чтобы отделить их от других частей приложения. Это имеет тот же недостаток, что и упреждающее планирование-это заставит сопрограммы/потоки бороться за доступ к ядрам процессора.

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

1. спасибо за ответ. в конкретном примере, который я привел, функция yield() не поможет, так как все потоки в конкретном контексте уже выбрали тяжелое выполнение, поэтому лучший вариант-вместо этого создать выделенный пул

2. Обратите внимание , что это не Thread.yield() так, но yield() функция специфична для сопрограмм. Я также не уверен, знаете ли вы/понимаете ли вы, что сопрограмме не нужно заканчивать, чтобы освободить поток. Достаточно, чтобы он приостановился, а затем поток переключится на выполнение другой сопрограммы. Что значит, что ваши задачи «тяжелые»? Они требуют больших затрат процессора или просто длительных задач, связанных, например, с операциями ввода-вывода и т. Д.?

3. под тяжелым я подразумеваю, что внутри нет точек подвеса, просто блок кода с интенсивным использованием процессора

4. @broot Я бы предложил перефразировать «Это на самом деле по замыслу» , потому что при чтении кажется, что вы имеете в виду совместное планирование, но я думаю, что вы имеете в виду упреждающее здесь.

5. @Dev93 Посмотрите, можно ли разделить эту интенсивную работу с процессором на блоки, и вы вызовете yield после завершения каждого блока

Ответ №2:

Как только сопрограмма начнет выполняться, она будет продолжать это делать до тех пор, пока не достигнет точки приостановки, которая вводится вызовом suspendCoroutine или suspendCancellableCoroutine .

Приостановка-это фундаментальная идея

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

Без приостановки вы теряете большую часть прироста производительности

Поэтому, чтобы идентифицировать переключатель в вашем конкретном случае, вам нужно будет определить термин slow and heavy . Задача, требующая больших ресурсов процессора, такая как генерация простого числа, может быть медленной и тяжелой, а вызов API, который выполняет сложные вычисления на сервере, а затем возвращает результат, также может быть медленным и тяжелым. если у 512 сопрограмм нет точки приостановки, то другим придется подождать, пока они завершатся. что фактически сводит на нет весь смысл использования сопрограмм, поскольку вы эффективно используете сопрограммы в качестве замены потоков, но с дополнительными накладными расходами.

Если вам нужно параллельно выполнять множество операций, не приостанавливающих выполнение, вместо этого вам следует использовать такую службу, как Executor, поскольку в этом случае сопрограммы не делают ничего, кроме добавления бесполезного уровня абстракции.

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

1. спасибо за ваш ответ. Я все еще не понимаю одной простой вещи, поправьте меня, если я ошибаюсь, пожалуйста. отодвигая в сторону все эти приятные уровни абстракций, которые Котлин вводит под капотом, если есть реальный блокирующий вызов (например, сеть / ввод-вывод). реальный поток ОС, как только он выберет запускаемый экземпляр с такой логикой, будет заблокирован при выполнении этой операции низкого уровня, мы просто не сможем запустить его. я прав или я что-то здесь упускаю?