Какой из них я должен использовать в Clojure? перейти к блоку или потоку?

#multithreading #clojure

#многопоточность #clojure

Вопрос:

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

Я понимаю, что если кто-то создает блок перехода, то он управляется для запуска в так называемом пуле потоков, размер по умолчанию равен 8. Но thread создаст новый поток.

В моем случае существует входной поток, который откуда-то принимает значения, и значение принимается в качестве входных данных. Выполняются некоторые вычисления, и результат вставляется в канал результатов. Короче говоря, у нас есть канал ввода и вывода, и вычисления выполняются в цикле. Чтобы добиться параллелизма, у меня есть два варианта: либо использовать блок перехода, либо использовать thread .

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

 (go-loop []
  (when-let [input (<! input-stream)]

  ... ; calculations here

  (>! result-chan result))
  (recur))

(thread
  (loop []
    (when-let [input (<!! input-stream)]

    ...  ; calculations here

    (put! result-chan result))
    (recur)))
  

Я понимаю, что количество потоков, которые могут выполняться одновременно, в точности равно количеству ядер процессора. Тогда в этом случае это go-block и thread не показывает различий, если я создаю более 8 thread или go-блоков?

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

Кстати, вычисления не такие уж тяжелые. Если входные данные не такие большие, за 1 секунду можно выполнить 8000 циклов.

Еще одно соображение заключается в том, повлияет ли go-block vs thread на производительность GC.

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

1. Инструменты, а не правила, но если вы настаиваете на правиле: используйте блок перехода.

2. Потоки проще и более пуленепробиваемы. JVM может поддерживать более 1000 потоков на современном оборудовании без проблем. Основная сила core.async заключается в том, что он может имитировать многопоточность в браузере, который имеет только один поток, который должен быть общим. Я бы редко рассматривал это как первый выбор в JVM.

3. @AlanThompson серьезный вопрос: для потребностей параллелизма, которые не требуют больших вычислительных затрат (например, простой ввод-вывод), не предпочтительнее core.async? Что касается простоты, это несколько в глазах наблюдателя: я нахожу CSP довольно интуитивным, но YMMV. Каждый раз, когда я когда-либо использовал потоки в приложении (предостережение: делал это только на Python и C), это оказывалось более сложным, более подверженным ошибкам и менее тестируемым, чем был бы CSP.

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

5. Если вы говорите о thread (со строчной буквы t ) из core.async , это не запускает новый поток. Он использует тот же исполнитель, что и go блоки. Вы можете использовать java.lang.Thread для запуска нового потока. Однако есть и другие варианты. То, что вы описываете, похоже на то, для решения чего были разработаны преобразователи. Если бы вы писали вычисления в менее императивном стиле, вместо использования цикла / recur вы могли бы использовать что-то вроде core.async/pipeline .

Ответ №1:

Здесь следует отметить несколько моментов.

Во-первых, пул потоков, в котором потоки создаются через clojure.core.async / thread, известен как кэшированный пул потоков, что означает, что, хотя он будет повторно использовать недавно использованные потоки внутри этого пула, он по существу неограничен. Что, конечно, означает, что это потенциально может занять много системных ресурсов, если оставить его непроверенным.

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

Вы также не упомянули, откуда вы получаете входные значения, являются ли входные данные каким-то фиксированным набором данных, который остается постоянным при запуске программы, или входные данные непрерывно поступают во входной поток из какого-либо источника с течением времени?

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

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