Pull to refresh

Comments 123

Когда я был ещё молод и зелен достался мне в наследство код, где в главном ui потоке выполнялись синхронные операции с очень большими файлами, которые сильно фризили gui приложения. Тогда я как следует разобрался с многопоточностью, вынес работу с ФС в фоновый поток и сделал достаточно неплохой обмен сообщениями между потоками. Время шло, код работал (и работает до сих), но изменился способ обработки файлов, да и сами файлы: их стало много больше, но сами они стали меньше по размеру. И сейчас получается, что gui снова фризится, но причина теперь в том, что сообщений между потоками перебрасывается просто бешенное количество. При этом, gui "единомоментно" фризится на совсем чуть-чуть, но таких фризов очень много. Попытка вернуть обработку обратно в один поток показала, что общее время выполнения всех операций становится значительно меньше, но превращается в "один гигантский фриз".

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

Скоро передам эту задачу студенту, посмотрим, что предложит :)

То, что он вообще может предложить, сильно зависит от среды выполнения: ОС, языка прогрограммирования, архитектуры GUI...

А варьироваться это "может предложить" от "элементарно" (например, асинхронный запуск IO в главном UI-потоке с проверкой, когда надо, завершения IO в этом потоке или же отправкой события завершения в цикл обработки событий UI, если UI имеет событийную архитектуру; вся эта благодать доступна, например, в Windows NT и ее потомках в виде современных версий Windows) до "невозможно" (это тоже было в Windows, но той, которая не NT, и там приходилось в такие моменты созерцать песочные часы вместо курсора).

А уже потом студент, в меру своего знания, выберет то, что он предложит.

Вы знаете много студентов 3 курса, которые знают что такое async i/o и умеют с ним работать? Я пока что ни одного) Да что там, я знаю достаточно много состоявшихся программистов, которые не умеют писать обычный thread-safe код (о чем, собственно, и была данная статья, в комментариях к которой мы находимся)

Дайте им в руки thread-safe язык программирования и они сразу научатся.

Это какой такой язык thread-safe? Какой из существующих компиляторов сам, без помощи разработчика, определяет какие участки кода и области памяти нужно защищать а какие нет, да ещё и с учётом того что ситуация меняется в процессе выполнения в зависимости от того какие данные поступили на вход?

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

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

Может, если речь о чём-то очень прямолинейном на уровне "Hello, world!" и можно сделать всё почти автоматически, но на любом сложном проекте вся эта автоматика либо сломается, либо замедлит всё в разы.

Впрочем, даже это всё (если и будет когда-то реализовано) не спасёт от потенциальных dead-lock которые в принципе невозможно обойти не зная что должен делать код, т.е. не читая и не понимая мыслей разработчика или архитектора - но в ближайшие 20 лет это маловероятно.

На данном этапе пока всё ещё проще делать синхронизацию почти вручную (используя примитивы синхронизации и прочие подобные механизмы) и пытаться "думать как процессор", но это всё же чуток сложнее чем "распознавание лиц в 10 строк на питоне".

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

У меня вообще сейчас нет знакомых студентов 3 курса. А по себе я судить не могу - когда я на 3 курсе учился, у меня доступ к ЭВМ был только через перфокарты - так давно это было. Впрочем, асинхронный ввод-вывод имелся и тогда - но об этом я узнал немного позже.

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

Работу с ФС следует не выносить в фоновый поток, а реализовывать через асинхронные вызовы. Но я не знаю что у вас была за платформа и АПИ, поэтому мне сложно судить.

У нас в конторе своё кроссплатформенное SDK почти на все случаи жизни, которое появилось ещё в середине 90-х. Чтобы использовать что-то другое, тот же boost-asio, надо прям очень сильно поспорить с начальником и аргументировать необходимость внедрения этого чего-то стороннего. Собственно, в те времена я не умел спорить с начальником) Да и сейчас не уверен, что стоит, ибо:

  1. Это очень маленькая тулзина для внутренних нужд;

  2. Там сейчас под капотом libarchive, а он не умеет в асинхронные операции.

    Ну и последнее: ввиду старости нашего SDK у нас практически никто не умеет в асинхронщину. Даже коды работы с сетью в нас имеют синхронное API.

У нас в конторе своё кроссплатформенное SDK

Ой-вэй. Когда мне на интервью говорят что-нибудь типа: "Для разработки мы используем свой собственный фреймворк (ORM, DI контейнер, MQ, RDBMS, etc - нужное подчеркнуть).", то у меня в голове сразу начинает звучать сигнал тревоги :)

Ага, я тоже плююсь. Поэтому в пет-проектах я активно использую что-то более популярное, чтобы быть в тонусе :)

Если у вас ещё и старый стандарт плюсов — то могу посоветовать только бежать подальше.


А на новом стандарте можно написать свой примитив "асинхронная операция, выполняющаяся в UI потоке", после чего всё значительно упростится.

Файберы прекрасно абстрагируют прикладного программиста от всей этой асинхронщины.

Асинхронные вызовы точно также приведут к обмену сообщениями между потоками (если команды на чтение файлов приходят из ui треда) - просто это будет происходить "под капотом". Никакой магии в них нет.

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


А магии там и правда нет.

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

Насколько мне известно единственный нормальный файловый асинк есть только в винде. В линксе вроде худо-бедно запилили io_uring, но его поддержка ещё все же хромает во многих языках и платформах.

Учитывая, что они там упирались в количество сообщений — асинхронный файловый ввод-вывод и не нужен, всё равно отдельный поток заводить придётся.

Попытка вернуть обработку обратно в один поток показала, что общее время выполнения всех операций становится значительно меньше, но превращается в "один гигантский фриз".

Достаточно было просто сократить количество обменов сообщений, не посылая их по одному а пачками скажем по 100 или 1000. Менять дизайн системы практически не нужно, просто вставить батчинг в 1 месте и цикл в другом, общее количество изменений — десяток строк.

стесняюсь спросить, а очереди не для этого придуманы?

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

«Хорошо» или «плохо» выясняется не с помощью присказок и тайного знания, а путем бенчмарков и профайлинга

в контексте обсуждаемого вопроса это одно и тоже

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

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

а два -- когда я говорю "одно и тоже" я имею ввиду, что мы говорим о примитиве исключительного доступа и тут не важно, как он фактически реализован

Все эти очереди потокобезопасны и грин треды тут вообще ни при чем. Тем более что зачастую грин треды вполне себе на обычных потоках делаются (шедулеры в эрланге или какой нибудь тредпул в токио в расте).

Ну и там нет примитива исключительного доступа как правило, в этом и смысл lock-free. Ну то есть нет такого, чтобы какой-то поток залочил себе запись, а все остальные ждут, когда он закончит.

Атомики и мьютексы вообще несравнимые вещи и то как они реализованы очень сильное значение имеет.

смысл в том, что в реальных сценариях все эти lock-free подходы (даже с учетом прорывного открытия disruptor pattern в 11-м году) обычно работают с производительностью, сравнимой с прямолинейным кодом на основе мютекса (и кроме lock-free в названии еще и содержать спинлоки в коде)

https://youtu.be/_qaKkHuHYE0

Так я нигде и не утверждал, что они быстрее мьютексов. Более того я прекрасно понимаю, что оно в негативном сценарии в отличие от мьютекса еще и проц будет жрать как не в себя. Разница только в том, что в одном случае все будет намертво висеть, если ресурс занят, а в другом случае возможны варианты.
Реальный сценарий реальному сценарию рознь. Если втупую взять какую-то общую структуру, которая шарится на много потоков и поменять мьютексы на lock-free, то почти наверняка оно хуже и будет. Все системы с обменом сообщениями, с которыми я работал и работаю подразумевают, что таких глобальных данных под мьютексом, в которые ломятся все кому не лень, нужно избегать и заменять на отдельные сообщения между отдельными потоками. В некоторых случаях у вас даже возможности такой не будет пошарить данные, например в эрланге(ну формально, конечно, можно и там извратиться, но это надо прям очень сильно захотеть). Но благодаря этому многопоточная разработка становится в разы проще и понятнее и помогает избежать кучи проблем.
Об этом (как мне показалось) и было оригинальное сообщение в треде. Ну и да, даже очереди с мьютексами (а то и вообще с RWLock) под капотом с точки зрения отсутствия возможности отстрелить себе ногу в разы лучше, чем голые мьютексы. Да, оно будет очевидно медленнее, но избавит от кучи головной боли, о которой в частности и написана статья.

По поводу видео, его я посмотреть не успел, но заголовок говорит о том, что там описывается mpmc, мои примеры выше работают либо в варианте mpsc, либо в варианте spsc вообще.

lock-free queue не подразумевает исключительного доступа. Lock-free структуры данных вообще не связаны с примитивами синхронизации.

Предлагаю подумать над парой фактов:

  • Мьютекс может быть реализован через lock-free spin-lock.

  • live-lock может заблокоровать ваш поток на неограниченный срок.

А потом глянуть этот материал:

универсальная lock-free queue с поддержкой multi-producer и multi-consumer, без спинлоков и с (хотя бы) 2x производительностью от примитивной версии на мютексе это что-то из фантазий на тему вечного двигателя

все известные мне и широко используемые очереди сообщений, которые используются для общения именно между «тредаии», мьютексов не используют

А это что? std::sync::mpmc::waker::SyncWaker

Предварительное уточнение, я с std::sync давно не работал напрямую и использовал в основном из tokio асинхронные варианты. Относительно недавно в основной раст добавили каналы из crossbeam, которые считались лучшей версией, нежели основные. Старые были без мьютексов, но думаю это не актуально.
https://doc.rust-lang.org/1.66.0/src/std/sync/mpsc/mpsc_queue.rs.html

По поводу того, что вы скинули. я бегло глянул, где оно там используется.
Во первых там есть 2 варианта каналов: bounded и unbounded
В bounded случае канал ограничен размером и используется array::Channel (ну или zero::Channel, если ограничить в ноль размер),
и SyncWaker там используется, чтобы разбудить тех отправителей, которые были добавлены в очередь, когда канал был забит.
В обоих случаях SyncWaker используется, чтобы разбудить получателей, если канал был пустым и кто-то туда что-то прислал.
То есть это некая оптимизация для пустых (или полных в bounded случае) каналов.

Сама отсылка в очередь (и забор из нее) делается через cas, спинлок и вот это вот все.
это вы можете посмотреть в start_send(start_recv) методах соответствующих Channel (array, list и zero).

Заодно глянул как оно там в tokio. Eсли в std(после мерджа crossbeam) все сделано на основе mpmс, просто при создании создается всего один получатель и mpmc превращается в mpsc, то в токио оно сразу идет как mpsc. Поэтому там нет таких же Waker для получателей, там идет один AtomicWaker и он без мьютексов, а на атомиках (как можно понять по названию). А для отправителей, да, идет семафор, но исключительно для задач ограничения размера канала. Сама же отправка точно так же идет через cas.

Итоги: мьютексы (и семафоры) используются, тут вы правы. Используются ли они для реализации самой очереди и разрешения конфликтов в ней при вставке или заборе очередного сообщения? Нет, не используются. Для чего они используются? Для оптимизации случаев пустых или переполненных каналов. Можно ли без них? Я не знаю.

Спасибо за то, что указали на это и дали повод чуть подробнее изучить.

По вашей же ссылке, первая же строчка:


A mostly lock-free multi-producer, single consumer queue.

Не совсем этичный вопрос, но я его задам: вы когда-нибудь писали на расте за деньги? Ну, так, чтобы в команде, то-се?

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

А почему вы не пишете тексты сюда? Тут довольно мало профессионалов в последнее время, все больше теоретики. Я бы подписался.

У меня нет такого количества экспертизы, чтобы прям туториалы для других писать. Я исходники упоминаемые в вышеприведенном комменте только сегодня посмотрел, чтобы убедиться, не ошибаюсь ли я. Было бы неловко, если бы наврал)

Пф. Кому нужны туториалы?

Я бы почитал про опыт.

Спасибо за предложение. Я подумаю над этим. Никогда не пробовал)

В данном контексте уместнее wait-free queue. Легко реализуется через циклический буфер. Работа парного треда при этом вообще никак не влияет на твой.

Lock-free queue

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

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

"Ждать сообщения из очереди" обычно считается антипаттерном. Кошерный подход это обрабатывать приходящие сообщения через евенты/коллбеки.

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

Зависит от реализации самой очереди. Если чтение из неё реализовано как неблокирующий вызов, то отдельный "ждущий" поток будет не нужен.

Вы потеряли изначальную задачу: очередь-то нужна чтобы передать данные в другой поток.


Ну и я не видел ещё lock-free очередей с асинхронным чтением.

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

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

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

Лучшая защита критических секций с данными, которые нельзя изменять параллельно, — это полная иммутабельность данных.

Лучшая защита - это всё же гарантии компилятора на уровне типов. Вирусная иммутабельность - одна из таких. Но не единственная, и не самая практичная. Например, есть move semantic, есть shared objects, есть 1p1c.

Как мне типы помешают вместо 42 записать 43 в базу из-за гонки?

А иммутабельность вам как помешает ерунду в базу писать?

В принципе — никак не помешает, а данном конкретном частном случае гонки, обсуждаемом в данном треде, — очень даже помешает.

А вот типы помогаю примерно никогда, а уж в случае высокой конкурентности — и подавно.

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


Если же вернуться к ситуации "вместо 42 записать 43 в базу из-за гонки", то здесь возможны две ситуации:


  1. у программы есть внутренняя гонка, из-за чего в БД попадает некорректно вычисленное значение;
  2. у программы есть внешняя гонка (гонка в БД), из-за чего значение в БД некорректно изменяется.

Так вот, в первом случае у вас упоминание БД в описании проблемы лишнее. И против гонки могут помочь как иммутабельность (иногда), так и типы (иногда).


Во втором случае вам против гонки не помогут никакие языковые средства, поскольку проблема за их пределами. Ну, разве что в каком-нибудь Идрисе найдётся пакет, позволяющий доказывать корректность транзакций БД, и то сомневаюсь.

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

Напомню, что оригинальный (осмысленный) комментарий звучал так:

Лучшая защита критических секций с данными, которые нельзя изменять параллельно, — это полная иммутабельность данных.

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

иммутабельность помогает всегда

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


Простейший пример:


static int shared_var;

shared_var = shared_var + 1;

Тип int является иммутабельным? Да, является.
Помогла ли иммутабельность? Нет, не помогла, инкремент потоконебезопасен.


Вот если бы вы сказали "Атомарные CAS-операции над иммутабельными ссылочными структурами в языке с GC помогают всегда" — это было бы более похоже на правду. Но и тут легко накосячить, достаточно лишь положить рядом две такие независимые структуры.


а типы — никогда

Это тоже спорно. Начну с того, что ваша иммутабельность — свойство типа, т.е. "иммутабельноть помогает" является частным случаем "типы помогают".


А закончу вот таким примером на Rust: std::sync::Mutex.

ваша иммутабельность — свойство типа

А моя иммутабельность в нетипизированных языках — она свойство чего?

Атомарные CAS-операции над […]

Не, я бы так сказать не мог, «атомарные CAS-операции» — это плеоназм.

А моя иммутабельность в нетипизированных языках — она свойство чего?

Свойство подразумеваемых типов.


Не, я бы так сказать не мог, «атомарные CAS-операции» — это плеоназм.

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

Во у меня какой примерчик из личных случаев: https://github.com/NickDoom-IDKFA/UnpLibr

Полюбуйтесь/попугайтесь :-D Сколько я с этими атомарками возился, жесть как она есть :-D

И всё равно рандомизированные тесты с рандомизированными задержками до последнего раза (который и пошёл в релиз) мне выявляли всё новые и новые сочетания стейтов, которые я не догадался предусмотреть.

Либа интереса не получила (и, соответственно, не получила развития по причине отсутствия смысла), но я надеюсь, что хотя бы гол престижа забил, в смысле в релизной версии у меня таки предусмотрены все сочетания состояний со всеми вариантами задержек %)

И да, sleep там тоже есть, для критических ошибок, когда надо аварийно вырубиться и ждать, когда клиентское ПО соизволит принять нашу отставку (это может быть хоть через сутки, когда оператор нажмёт «ОК» и отпишется руководителю, что архив битым оказался).

Что-то я там с ходу так и не нашёл никакой многопоточности, где её искать-то?

Тредов там два, один обеспечивает апи и полностью синхронен с приложением, пользующимся библиотекой, а второй через пачку атомарок получает его хотелки и через пачку атомарок сигнализирует о готовности. Окно распаковки у алгоритма в любом случае одно, поэтому если нужно независимо читать из нескольких мест архива — создаём столько таких «парочек», сколько нужно (но вот тут я не помню, дописал до этого места или нет; даже если нет, то там уже тривиально — обернул все глобалы и вперёд). Я вообще уже не очень хорошо этого пета помню :( Если память не врёт, эти два треда разнесены по двум группам исходников.

Смысл в этом простой (там вообще вся задача сферовакуумная настолько, что хоть в учебники помещай) — архив распаковывается вовсе не такими кусками, как нужно приложению, да и распаковка, особенно с поиском, может занять вечность. Библиотека кэширует распаковку и выводит её в отдельный тред, чтобы приложение тем временем занималось своими делами. В итоге апи очень похож на istream::open istream::read istream::close (с лёгким налётом FindNext, заради solid-архивов), а тред распаковки пытается обеспечить максимальную готовность для самого базового режима — последовательного чтения требуемого файла.

Либа вообще во многом «школьная», меня просто (на тот момент) удивило отсутствие аналогичных решений и я написал основу, то есть сам синхронизатор-буферизатор. С прицепленной к нему unrar.dll оно даже могло практически где-то использоваться :)

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

Что делает второй поток если у него нет задачи? Надеюсь, не активно ждёт работы?


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

Я тоже надеюсь, что не протупил до такой степени, но в точности не помню уже ?

Скорее всего, какой-то костыль типа «спи один квант времени»…

Чёрт, набрехал я за давностью лет, там SetEvent / ResetEvent / WaitForSingleObject.

А взяли бы толковый язык с толковыми абстракциями, и не пришлось бы мучаться:

import core.time;
import std.stdio;
import jin.go;

// provide periodical signals while consumed
static auto tick( Output!bool signals, Duration dur ) {
	while( signals.available >= 0 ) {
		dur.sleep;
		signals.put( true );
	}
}

// provide one signal after timeout
static void after( Output!bool signals, Duration dur ) {
	dur.sleep;
	signals.put( true );
}

// start tasks on thread-pool
auto ticks = go!tick( 100.msecs );
auto booms = go!after( 450.msecs );

for( ;; ) {
	
	// consume all pending ticks
	while( ticks.pending > 0 ) {
		write( "tick," );
		ticks.popFront;
	}
	
	// consume boom
	if( booms.pending > 0 ) {
		writeln( "BOOM!" );
		break;
	}
	
	// sleep or do other work
    1.msecs.sleep;
}

И нет, это не $mol.

Такие "абстракции" есть на любом современном ЯП, язык тут менять необязательно.


Кстати, мне показалось, или ваш код делает активное ожидание на пустом месте?

Только не любой язык умеет передавать сообщения без блокировок проверяя thread-safety на этапе компиляции.

Тогда почему на твоём языке не пишут все?

Это не мой язык. А большинство вообще на JS пишут, это делает из него отличный язык для многопоточной разработки?

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

Elixir, как и другие языки на базе BEAM.

В плане многопоточной разработки - все остальные языки на уровне «детский сад, штаны на лямках». Причём разве что Rust c Actix в правильном направлении идёт.

Остальные только костыли разной степени ебанутости придумывают. Потому что невозможно писать адекватно многопоточный код там, где может в любой момент прийти GC и начать общую кучу чистить ?

Тут согласен, в нем очень сложно (хоть и не невозможно) себе прострелить ногу при работе с потоками. Скорее всего просто не скомпилится.

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

Rust уже начал выстраивать, но до уровня BEAM ему ещё лет 10 упорной разработки.

По сути это «ос внутри процесса». Там создаются внутренние виртуальные полностью изолированные друг от друга процессы, которые общаются друг с другом только посредством передачи сообщений. У вас физически нет разделяемой памяти и не нужно ничего синхронизировать. Это дает определенные преимущества в определенных программах, но это явно не применимо для всех возможных задач.

В общем-то ничего общего нет. И как бы вас thread-local storage вообще никто не заставляет использовать. А тут нет выбора в принципе, у вас буквально процессы изолированные, передавать можно только копированием (с некоторыми исключениями, но это уже не особо важные в контексте этого разговора подробности).

В thread-safe языках компилятор не просто заставляет использовать TLS, а сам хранит данные именно там.

Я слабо себе представляю, как оно должно работать в случае всяких thread pool с постоянным переключением green threads между разными физическими потоками. Сам я на практике такое не использовал, но если вы мне объясните, я буду не против.

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

Вот вообще нифига не так, я буквально на днях это проверял. То есть делается функция с авейтом внутри и после авейта все делается в другом физическом потоке. Это буквально весь смысл всей этой схемы. Даже в том же расте чтобы что-то запустить в асинхронщине - оно должно быть Send + Sync, чтобы рантайм мог этим жонглировать.
По поводу дорогого удовольствия. Ничто не дается бесплатно. В том же эрланге куча оверхеда и тормозов, но зато меньше вариантов прострелить себе ногу. И тут надо выбирать, что тебе важнее.

Не помню, где читал про это, но сейчас сходу не нашёл. Возможно я пререпутал с привязкой нитей к ядрам.

В том же эрланге куча оверхеда и тормозов

Иммутабельность безусловно накладывает определенный оверхед на все функциональные языки.

Но, например, для разработки backend это практически никакой роли не играет и никаких торомозов нет. Даже, наоборот, я например переписывал как-то сервис с Go на Elixir. Получилось на пару процентов лучше по throughput, а по latency так вообще стало гораздо лучше - выбросы все ушли.

А там, где нужна числодробилка или мутабельность, для этих 0.01% случаев можно NIF на Rust написать. Как, например, Discord сделал: https://discord.com/blog/using-rust-to-scale-elixir-for-11-million-concurrent-users

Я пишу на эрланге профессионально уже больше 13 лет мне не надо его рекламировать)

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

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

Ну, для них обычно dirty-cpu scheduler-ы используют, чтобы на основные влияния не было. А так, нюансы есть конечно.

Я слабо себе представляю как работает thread local storage и beam тут ни при чем вообще.

Подозреваю, что из-за путаницы в нитях различных цветов, тут речь идет про process dictionary.

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

Да и опять же я не уверен, что именно thread local storage там используется, а это не просто схожая по концепциям вещь.

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

И как в такой схеме мне делать разделяемые данные, если у каждого свой скоуп недоступный? То, что вы описали - вообще какая-то сугубо специфичная для определенных задач штука, но никак не механизм, на котором весь thread-safe построен.

Передать ссылку на них в другой поток. А чтобы вы не передали туда какой-нибуь небезопасной ерунды, и нужен thread-safe на уровне типов.

А причем тут тогда thread-local storage? И как он помогает в достижении thread-safe, что это прям обязательное условие для всех thread-safe языков, как вы писали выше?

Стесняюсь спросить, что возникло раньше: read-only mode, или типы? И если не типы, то как древние умудрялись реализовать этот самый read-only режим?

Кажется вы перепутали readonly и immutale - это разные вещи. Да и безопасно работать из разных потоков можно и со вполне мутабельными структурами.

чтобы вы не передали туда какой-нибудь небезопасной ерунды, и нужен thread-safe на уровне типов

Переформулирую вопрос: а чем read-only хуже, почему вместо того, чтобы использовать железобетонную проверенную десятилетиями концепцию — вдруг потребовались какие-то типы?

EvilBlueBeaver отчасти правильно ответил. Но помимо полностью изолированных друг от друга легковесных процессов, там ещё очень богатый инструментарий по управлению ими, называемый OTP, плюс средства удаленной интроспекции, отладки и горячей замены.
Представьте себе Kubernetes только раз 5 навороченее и в рамках одного инстанса приложения, где вместо подов у вас легковесные потоки, десятки тысяч потоков.

P.S. Могу посоветовать вам 2 книги для краткого ознакомления с этой темой: The Little Elixir & OTP Guidebook и Concurrent Data Processing in Elixir

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

В расте тоже есть каналы:

use std::sync::mpsc;

fn main() {
    let (tx, rx) = mpsc::channel();
}

Спасибо, я в курсе) вы можете выше найти комментарий, где я про их внутреннее устройство немного рассказывал)

Как из моего комментария вообще следует, что я actix использую? к слову, я его не использую.

Да, вы всё правильно поняли. Плюс вытесняющая многозадачность для зеленых потоков позволяет более равномерно использовать все доступные ядра и иметь предсказуемый latency.

Sign up to leave a comment.

Articles