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

Пользуясь случаем хочу сказать большое спасибо комментаторам! Написали много полезного и интересного, статья была бы неполной без этого.

Терминология

Как многие отметили, термины, которые я использовал, не отличаются однозначностью. Попробую подробно объяснить, что я имел в виду. Я не настаиваю на своём прочтении этих терминов, они в принципе ещё совсем не устоялись.

Самое общее понятие — это параллелизм. Оно означает любое параллельное исполнение кода. Причём как "честное" — на нескольких процессорах/ядрах, так и имитируемое программно на одном процессоре. Способов его организовывать может быть несколько. Мой общий посыл относительно параллелизма как такового состоит всего-навсего в том, что теперь мы от него никуда не денемся.

Один из способов делать параллелизм — это детерминированный параллелизм (не знаю более короткого термина). Я совсем не касался его в прошлый раз, и поэтому сошлюсь на хорошо объясняющий пост "Parallelism /= Concurrency", из которого я понял, что речь идёт о случаях, когда у вас есть функция типа f(x, y), где x и y друг от друга не зависят, вследствие чего вы можете считать аргументы параллельно, и тогда значение f будет посчитано быстрее. Штука в том, что в императивном языке невозможно предсказать независимость x и y только на основании их аргументов, потому что каждый из этих процессов может менять состояние мира, в котором работает, и это может влиять на результат другого процесса. Зато в функциональных языках, где мир вокруг внезапно не меняется, эту параллелизацию, по идее, возможно делать автоматически.

Весь остальной — недетерминированный — параллелизм принято называть словом concurrency, которому я вменяемого перевода на русский язык не знаю. Недетерминированность concurrent-процессов происходит от того, что они обмениваются данными в непредсказумые моменты времени. Это могут быть разделяемые даные в общей памяти и может быть передача сообщений с данными между процессами. Причём как именно организованы эти процессы, не уточняется: unix-процессы, треды, процессы виртуальной машины, сервисы ядра и т.д.

В связи с этим стоит сказать про не очень удачное слово тред, под которым часто понимаются две разные вещи:

Я очень стараюсь использовать слово "тред" только в первом смысле. Второе пусть называется "шаренные данные и локи".

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

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

Посыл

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

Вообще, ключевое слово там — не "асинхронно", а "придётся" :-).

Разные виды concurrency

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

Именно поэтому мне интересны языки, которые реализуют concurrency по-другому.

Передача сообщений

В Erlang это независимые процессы с передачей сообщений между ними. Главный плюс независимости процессов — возможность запускать их на разных физических машинах, и значит масштабироваться практически бесконечно. Главный минус — потери производительности на копирование данных и на обслуживание этой самой независимости. Erlang'исты меня поправят если что, но кажется, даже для инкремента счётчика нужно запрограммировать отдельный процесс, который примет на вход текущее значение и вернёт на выходе увеличенное на единицу. Это накладно.

Ещё примерно тот же подход с передачей сообщений используется в Go, если я правильно читаю их доку по concurrency. Для физической реализации параллельных процессов используются треды ОС. Причём кажется, что тред создаётся под каждую goroutine, никакого готового пула под них нет. Что не очень хорошо.

Разделяемая транзакционная память

В Clojure же всё наоборот. Там данные лежат в общей памяти, но никогда не изменяются. Вместо этого новая версия данных строится над старой, и шарит с ней все неизменённые куски. Раз данные не изменяются, доступаться к ним можно вообще без использования локов. А конфликты при записи новых данных решаются реализованной в языке software transactional memory, которая прозрачна для программиста и гарантирует корректность (хоть и недетерминированную) новых версий данных. Это очень хорошо видно в примере с эмулятором муравьёв, в видео, на которое я уже ссылался. Там в районе времени 83:45.

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

Главный минус — это работает только на одной машине. Соответственно, если вам нужно масштабировать систему на Clojure на несколько машин, это будут отдельные системы на Clojure, которые уже будут обмениваться данными не средствами языка, а так, как вы напишете.

"Архаичный" Erlang

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

Откровенно говоря, я все эти вещи назвал "архаичными", потому что мне кажется очевидным, что в последнее время языки движутся в другую сторону: меньше церемоний, меньше синтаксиса как такового, меньше обязательных декларации чего бы то ни было. То есть по шкале выразительности языка от, скажем, Си до Питона, Erlang, придуманный в конце 80-х, таки кажется мне архаичным.

Впрочем, я не претендую хоть на какую-то объективность тут. Больше того, очень хорошо понимаю, что моё ворчание про Erlang из той же области, что ворчания не-лисперов про скобки, не-паскалистов про индексирование массивов с единицы, не-питонистов про отступы пробелами. Каждому — своё :-).

Неэффективность веб-модели

Ещё одна вещь, про которую я забыл. Я хочу не согласиться с Тимом Бреем, который считает, что на вебе проблемы с недостаточной параллелизацией обработки в целом решены. Да, у нас почти нет проблем с доступом к общим данным, и мы действительно обычно занимаем все доступные CPU, но только делаем это не особенно эффективно. Например мы плодим процессы/треды по количеству клиентов, которых пока что на обозримое будущее сильно больше, чем процессоров. А это значит, что процессы будут драться за ресурсы и мешать друг другу.

Представьте себе, если бы то же самое, что сейчас делают nginx и lighttpd, используя неблокирующий IO, удалось обобщить на все остальные вычисления. Веб-фреймворк мог бы принимать и держать у себя тысячи запросов пользователей, обрабатывая их задачи в пуле потоков, не превышающим количества процессоров.

Существующих библиотечных подходов типа питоньих eventlet и Twisted для этого не хватает. На них можно делать очень ээфективную асинхронность на неблокирующих IO-вызовах. Но к сожалению, кроме ввода-вывода существуют и другие вычислительные задачи.


Ну вот... Кажется, всё должно стать яснее.

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

  1. ag

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

    Должно быть, если скрестить twisted и psycopg2 (который давно умеет асинхронное выполнение запросов), можно полностью уйти от GIL и все выполнять в одном потоке. Один такой поток сможет сервить много клиентов, даже если это comet.

    Будущее уже наступило :)

  2. Alex Ott

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

  3. Alex Ott

    по поводу тредов. erlang и другие языки не всегда делают треды уровня OS, а имеют свои, легковесные треды, которые управляются самой виртуальной машиной.

  4. Ivan Sagalaev

    Но это можно сделать и без нового процесса.

    Но ведь всё равно с копированием данных? Я так понимаю, такой красивой штуки, как persistent data structures из Clojure, в Erlang нет?

  5. Ivan Sagalaev

    Будущее уже наступило :)

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

  6. Alex Ott

    В смысле с копированием? в Erlang копируются данные сообщений между процессами, но это сделанно исключительно для устойчивости - стоит почитать History of Erlang, почему это так сделано...
    У Erlang есть другая интересная/полезная функциональность - pattern matching, которого нет в clojure

  7. Qrilka

    А что ты скажешь про асинхронный http://nodejs.org/ ?
    Хотя до Эрланга ему...

  8. Денис Баженов

    Я понимаю что AIO эффективен по используемым ресурсам. Но какой резон мне переписывать скажем функцию шифрования данных в асинхронном стиле? Какой profit я получу?

    Я считаю что асинхронность имеет смысл только в случаях дорогого IO. Но не для всех задач IO является bottleneck'ом. Есть задачи для которых ограничивающим фактором является CPU. Что даст асинхронность в этом случае?

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

    В целом, я согласен с вами в том, что параллелизм будет очень важен в ближайшее время (я сам об этом писал: http://dotsid.blogspot.com/2009/01/blog-post.html)). Но есть процессы которые не смогут выполнятся параллельно. Параллелить то же IO нет смысла до тех пор пока у вас одно устройство ввода/вывода (например, один жесткий диск). Хочу подчеркнуть что распараллеливание и неблокирующее IO служит разным целям.

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

    Я бы не скидывал со счетов текущий инструментарий и языки. Brian Goetz в одном из своих speech'ей (http://www.infoq.com/presentations/brian-goetz-concurrent-parallel) недвусмысленно заявил что они работают над имплементацией STM в Java (сторонние имплементации STM для java есть уже давно). И те же actor'ы давно есть в java (kilim) причем, по заявлению авторов, throughput они обеспечивают больше чем erlang (за достоверность этой информации я ответственности не несу, erlang'овцам просьба не паниковать :) ). А в интернете можно найти немало недовольных STM'ом, поэтому сам profit этого подхода для меня лично еще не очевиден.

    То в какой ситуации, с какими языками и парадигмами мы окажемся скажем через десять лет, - это еще очень большой вопрос. Я не исключаю варианта что принципиально ничего не поменяется. Erlang, Haskell и прочие языки очень важны, как полигон для обкатки идей, которые могут спасти параллельные вычесления. Я сам сейчас с удовольствием читаю книгу по erlang, а в reading queue у меня стоит haskell. Но станут ли эти языки доминирующими? Что ж, поживем увидим.

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

  9. Андрей Светлов

    IO в общем случае всегда довольно дорогой.

    Смотрим на современный комп: CPU с несколькими ядрами, память, GPU, HDD, сетевая карта. Последние два зачастую во множественном числе. И на каждой паре - IO. С разной "ценой вопроса", разумеется. А еще есть cloud computing вообще и кластеры в частности.

    Почему нельзя рассматривать обращение к GPU равноценно с запросом по сети - только с разной латентностью?

    А взаимодействие двух ядер CPU - это просто очень быстрый IO.

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

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

    Питоновский GIL - пожалуй, самое больное место для разработчиков. И когда-нибудь его уберут наконец - но это непросто. В Python 3.2 будет "изрядно более быстрая версия" - не более. Но даже когда долгожданный момент наступит - Питон станет всего лишь таким же многопоточным, как C, С++, Java или C#. Ничего принципиально нового с этой точки зрения в языке не появится.

    А так - да. В интересное время живем. Только, если оглянуться и вспомнить - а когда оно было не интересное? Пять, десять, двадцать, пятьдесят лет назад в программировании тоже были очень интересные времена и революционные изменения. Парадигмы рушились, создавая новые. И тоже публика бурлила, ожидая чуда. Чудеса происходили, да только каждое изменение открывало новые горизонты и поднимало планку - что просто замечательно. Думаю, интересные времена закончатся очень не скоро.

  10. Денис Баженов

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

    Абсолюнто с вами согласен. Но что я имел ввиду, так это то, что для первого приведенного вами случая достаточно nonblocking IO (это, надо признаться, тоже своеобразная форма параллелизма, но я склонен отделять зерна от плевел), а также то что нет смысла параллелить задачи отдаваемые одному жесткому. У него один шпиндель и совмещенные головки. Одновременно два файла читать/писать он не будет. Вполне очевидно что распараллеливание доступа к ресурсами которые не обладают конкурентным исполнением приведет к деградации производительности системы.

    В общем случае, разные исполнители обладают разным уровнем конкурентности (процессоры могут делать несколько задач одновременно, жесткие нет, сетевые карты только две задачи одновременно: прием, отправка и т.д.). И это надо брать в расчет когда мы нагружаем исполнителей работой. Pipeline'инг позволяет эффективно решать эту проблему, а это то где как раз во всей красе могут проявить себя асинхронное программирование и его производные, такие как SEDA.

  11. Ivan Sagalaev

    Есть задачи для которых ограничивающим фактором является CPU. Что даст асинхронность в этом случае?

    Эм-м... Кажется вы снова привязываете слово "асинхронный" к IO. Причём тут IO? Шифрование в асинхронном стиле это вот так:

    for chunk in chunks(big_data):
        async_encrypt(chunk)
    

    Каждый кусок данных шифруется параллельно (как их потом складывать вместе — другой вопрос).

    А в интернете можно найти немало недовольных STM'ом, поэтому сам profit этого подхода для меня лично еще не очевиден.

    В интернете можно найти немало недовольных чем угодно :-). STM очень вкусен тем, что выполняет ту же функцию, что локинг, только сильно эффективней (не блокирует чтение) и надёжней (не вызывает dead lock'ов и race condition'ов). Как по мне, так это очень круто.

  12. Денис Баженов

    Каждый кусок данных шифруется параллельно (как их потом складывать вместе — другой вопрос).

    Я не имею ничего против кода который вы привели, он прекрасен! Но непонятки с терминологией я считаю все же остались. Что вы сделали, так это поставили равно между асинхронностью и map-reduce (ну или devide&conquer, как вам будет угодно). Мне кажется, вы подменяете понятия.

    Брр... Вообще, не люблю все эти терминологические споры, простите меня.

    STM очень вкусен тем, что выполняет ту же функцию, что локинг, только сильно эффективней

    Как раз этот аспект и ставится под вопрос. Существует мнение, что для некоторых задач STM обеспечивает меньшую пропускную способность чем обыкновенные mutex'ы. И вообще, это не удивительно. В основе STM лежат принципы оптимистической блокировки, и потому она страдает от всех тех же проблем что и все другие реализации этой методики. Одна из таких проблем (раз уж мы заговорили об эффективности) заключается в том, что оптимистическая блокировка, и следственно STM, более прожорливы в отношении трафика шины памяти. На некоторых задачах в это можно стукнутся. По-крайней мере, на текущем железе. Non-uniform memory access машины я в расчет не беру, так как, насколько я знаю, коммерчески успешных имплементаций этого подхода еще нет.

  13. Марко Кевац

    Мне не понятно преимущество immutable объектов в параллельном программировании.

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

    Помоему тут какая-то путаница в терминах "изменяемы", "не изменяемы", "будут изменены обязательно" и ставится равенство между неизменяемыми объектами и функциональными ЯП. Неизменяемые объекты существуют и в императивных ЯП.

  14. fuwaneko

    У Erlang синтаксис поаккуратнее некоторых будет. Он чем-то похож на синтаксис Pascal: строгий. Всё нужно делать аккуратно. Это плохо? Не думаю. Вот разобраться в чужой писанине на Erlang без комментариев действительно очень сложно, но это уже другой вопрос.

    Теперь касаемо процессов. Оверхед на создание оных очень маленький. Он на первый взгляд кажется нереальным :) Приведу пример из известной книги самого Армстронга (Programming Erlang): «[...]2.40GHz Intel Celeron with 512MB of memory running Ubuntu Linux.[...] Spawning 20,000 processes took an average of 3.5 μs/process of CPU time and 9.2 μs of elapsed (wall-clock) time.» Создавать процессы в Erlang вовсе не накладно. И конечно никто не заставляет этого делать, так что для счётчика не нужен отдельный процесс :) А самое прикольное тут в том, что пересылка сообщений между процессами тоже очень быстрая. OTP оптимизирован для этого, он создавался для этого. А заявление о накладных расходах на копирование данных выглядит довольно странно, если учесть, что данные немутабельные. И в Clojure тоже.

    Persistent data structures это конечно круто, но я в таких случаях вспоминаю 20-гиговую базу данных Firebird :) Хотя CouchDB, например, работает по такому же принципу.

    В общем я хочу вам посоветовать поплотнее познакомиться с Erlang :)

    P.S. Жаль, что у вас нет кнопки «предпросмотр».

  15. Ivan Sagalaev

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

    О, нет. Если A читает какой-то составной объект, то нам надо обеспечить, чтобы в момент этого чтения другой тред не изменил частично этот объект. Именно поэтому в программировании с локами приходится лочить объекты в том числе при чтении. Immutability даёт гарантию, что этого не нужно делать никогда.

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

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

    В случае immutable-данных у треда всегда есть какая-то консистентная (возможно устаревшая) версия данных.

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

    Аргумент не столько про какие-то конкретные языки, сколько про concurrency. Давай переформулирую. Из-за того, что параллельные задачи работают непредсказуемо по временио и должны обмениваться данными, при concurrent-программировании надо как-то эти моменты синхронизировать. Это проблема. Идеальное её решение — не обмениваться данными между процессами. И тогда действительно неважно, какой там язык, и какие данные. Но ведь есть и другие задачи, которые повязаны взаимодействиями, но которые тоже хочется параллелить. И вот тут важен язык, который организует concurrent-программирование с минимальными затратами со стороны программиста.

  16. Марко Кевац

    О, нет. Если A читает какой-то составной объект, то нам надо обеспечить, чтобы в момент этого чтения другой тред не изменил частично этот объект. Именно поэтому в программировании с локами приходится лочить объекты в том числе при чтении. Immutability даёт гарантию, что этого не нужно делать никогда.

    Я тут понаписал кучу всего, а потом еще раз вчитался в цитируемый фрагмент. Упустил совсем фразу "лочить объекты в том числе при чтении". Да. Согласен. Упустил этот элемент из виду.

    В случае immutable-данных у треда всегда есть какая-то консистентная (возможно устаревшая) версия данных.

    Какие способы используются для того, чтобы обновленные данные треда А смерджить с обновленными данными треда B и т.д. и положить на место оригинальных данных?

    Зачем? Ведь когда будем создавать тред C, то он будет копировать оригинальные данные. И у него должна быть последняя версия.

  17. russian-knight.livejournal.com

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

    Попробую сам ответить на последний свой вопрос. Наверное одно из самых лучших тут решений - транзакционная память. Для работы транзакционной памяти тоже требуются свои средства для обеспечения безопасности concurrency. Тогда получается что единственное преимущество immutable объектов - отсутствие необходимости лочить при чтении. Но таким же образом можно копировать не immutable объект. И получается что immutable - это практически синтаксический сахар. Не так?

  18. Несказов Александр

    Единственный принципиальный плюс Erlang-a перед другими системами - это то, что это детерминированная система (если конечно вы сами это дело не рушите). Почему? Читаем про network process Kahn. + основы функциональной парадигмы в бек енде этого всего.

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

  19. Andrey Popp

    Я бы не сказал, что Erlang так уже детерминирован — асинхронная передача сообщений как раз добавляет неопределённости, из-за этого возникают проблемы переполнения mailbox'а у процессов. Более детерминированным была бы синхронная передача сообщений как в CSP например.

  20. Несказов Александр

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

    Спасибо за ссылку на CSP, раньше знаком не был, посмотрел немного начало статьи "Communicating Sequential Processes" Hoare, и сходу наткнулся на вот эту строчку:
    "In general, an input or output command is delayed until the other process is ready with the corresponding output or input. Such delay is invisible to the delayed process." Вероятно из-за моего отвратительно английского, я мог не правильно понять, но это значит, что CSP блокирует поток при передаче сообщения, а следовательно возможен deadlock, а если так, какой уж тут детерминизм... + это достаточно сильно сковывает параллельные процессы.

    Поправте если ошибся.

  21. coffeesnake

    Главный минус — это работает только на одной машине. Соответственно, если вам нужно масштабировать систему на Clojure на несколько машин, это будут отдельные системы на Clojure, которые уже будут обмениваться данными не средствами языка, а так, как вы напишете.

    На самом деле для Java Runtime (которую Вы отнесли к недостаткам:) есть ещё один вариант - существуют довольно популярные последнее время решения вроде Terracotta, позволяющие приложениям прозрачно расползаться на кластер и шарить память между тысячами нодов.

  22. Дмитрий Чаплинский

    Я может глупость сморожу, но вроде у Go Lang эти проблемы решены, это такой себе компилируемы пайтон с CSP concurrency, как у Erlang:
    http://golang.org/doc/go_lang_faq.html#concurrency

  23. Andrey Popp

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

    Я как раз и говорил с мат. точки зрения.

    следовательно возможен deadlock, а если так, какой уж тут детерминизм...

    Да, deadlock возможен, но детерминизм тут непричём — как раз в силу него есть инструменты стат. анализа, которые могут искать deadlock'и в CSP программах. Кстати deadlock'и возможны и в Erlang.

    это достаточно сильно сковывает параллельные процессы.

    Что имеется ввиду?

  24. Andrey Popp

    Я может глупость сморожу, но вроде у Go Lang эти проблемы решены, это такой себе компилируемы пайтон с CSP concurrency, как у Erlang

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

  25. Powerman

    Мне тоже кажется, что CSP плюс обычный императивный язык решает имеющиеся проблемы ("шаренные данные и локи" ;-)) лучше всего.

    В этом смысле лучше всего смотреть не на Go, а на OS Inferno и Limbo. Go это попытка реализовать Limbo без Inferno, и хотя это упрощает установку и использование языка (и может сделать его куда более популярным чем Limbo) но без некоторых фишек виртуальной машины Inferno Dis он до Limbo пока не дотягивает.

    Функциональные языки и immutable данные это здорово, но большинство задач всё-таки проще и естественнее записывать в императивном стиле, и с mutable данными. Я думаю, что уделом Erlang останется довольно узкая ниша, примерно та предметная область, для которой он и разрабатывался.

    Что касается асинхронного I/O... Наш текущий веб-фреймворк как раз базируется на асинхронном I/O. Есть один FastCGI-процесс, который обслуживает весь веб-сайт. Все блокирующие операции (вроде доступа к данным) делаются через RPC-запросы к отдельным (локальным или сетевым - у нас кластер) сервисам. Таким образом, этот процесс выполняет только неблокирующие операции - I/O к юзерам(веб-серверу), I/O к нашим сетевым сервисам, и простые вещи вроде обработки html-шаблонов (когда от сетевых сервисов приходят все данные, необходимые для отправки ответа пользователю). Пишется всё на Perl, так что тредов нет. Одни callback-и. Да, синтаксического сахара намешано достаточно, чтобы сильно это не раздражало. Да, используется только одно ядро/проц, но ведь другие ядра загружены остальными сетевыми сервисами, маськой, etc. - а для AIO и обработки шаблонов одного ядра/проца выше крыши хватает. И да, работает всё это нереально быстро (простой запрос где-то за 0.0004 sec, запрос включающий в себя несколько запросов к сетевым сервисам - проверка авторизации, запрос данных из базы и ещё пара RPC - где-то за 0.0024 sec - но нужно учитывать что параллельно с этим запросом благодаря AIO в то же время может быть выполнено ещё несколько запросов). НО! Линейный императивный код плюс CSP позволить распараллелить всё не хуже, а писать и понимать его намного проще, чем код, где обработка одного CGI-запроса разбита на 5 callback-ов.

  26. Powerman

    BTW, почитал про node.js - в нашем фреймворке всё абсолютно аналогично, даже код выглядит примерно так же, хоть это и Perl а не JS. Только надо отметить, что встроенные в середину кода анонимные функции красиво выглядят в примерах уровня helloworld. А в реальных проектах так код не попишешь, иначе там дикая мешанина начинается - приходится выносить их в отдельные функции... и сразу становится заметно, что код таки не линейный, а сплошные callback-и. И писать и воспринимать его намного тяжелее, чем линейный код.

    Ещё любопытно - пример с helloworld на node.js работает раз в 6-7 быстрее нашего фреймворка, хотя оба используют libev и epoll. Неужели V8 насколько шустрее perl? %-[] Или это тормоза из-за цепочки apache-fastcgi (у node свой веб-сервер и никакого fastcgi не нужно)?

  27. Google user

    Сразу маленьку поправку про Go.

    Причём кажется, что тред создаётся под каждую goroutine, никакого готового пула под них нет. Что не очень хорошо.

    Нет, треды создаются по необходимости в пределах пула. Размер пула контроллируется environ переменной GOMAXPROCS (кажется, точно не помню название). По-умолчанию она равна 1, то есть в любой момент будет ровно один OS thread.

  28. xonix

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

  29. Google user

    @Alex Efros, никак не могу понять. У вам Perl. Один процесс. Зачем вы снаружи тормозите перл threaded/prefork апачом? Какую функцию там апач якобы выполняет? Ограничивает количество одновременных соединений? :)

  30. Google user

    xonix, если программа просто работает асинхронно (eventlet/Twisted+inlineCallbacks/etc), а не состоит из макарон колбеков, то для разработчика это самая обычная однопоточная программа. И отлаживается соответственно.

    Что колбеки - это зло тут, кажется, все согласны. А что AJAX пишется в стиле "old school Twisted без inlineCallbacks" не бросает тень на асинхронный ввод-вывод в целом. Это недостатки конкретных языков/библиотек.

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

  31. Несказов Александр

    2 Andrey Popp

    Да, deadlock возможен, но детерминизм тут непричём — как раз в силу него есть инструменты стат. анализа, которые могут искать deadlock'и в CSP программах. Кстати deadlock'и возможны и в Erlang.

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

    Да, deadlock возможен в erlang, но только в том случае, если мы создаём поверх его асинхронной системы передачи сообщений синхронную. Если не секрет, в чём недетерминированность сетей процессов Кана (переполнение mailbox - ограничение реализации, но не идеи)?

    Что имеется ввиду?

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

    2 xonix

    К примеру если сравнивать отладку многопоточного приложения на java и erlang, то для эрланга это намного проще, и, как правило, не нужен отладчик. Это связано с тем, что данные каждого процесса только его. А следовательно, единственные возможные вмешательства в процесс его работы - посылка сообщений, и ничего не запрещает вам почитать его "письма".

    Но в случае если мы имеем асинхронную систему с общими ресурсами, вы правы, это превращается а ад.

  32. Andrey Popp

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

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

    Если не секрет, в чём недетерминированность сетей процессов Кана (переполнение mailbox - ограничение реализации, но не идеи)?

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

  33. Powerman

    Ограничивает количество одновременных соединений? :)

    Угу. :) Переключил на nginx - скорость выросла почти в два раза на helloworld и почти в полтора на реальных задачах. Это я просто дома игрался, переключал между апачем и nginx-ом, ну и бросил его на апаче. Пока сегодня ab не запустил, разница между апачем и nginx себя, на домашней-то машине, никак не проявляла.

    Раза в 3 отставание от node.js всё ещё есть, но если учесть накладные расходы на протокол FastCGI между nginx и perl она может и нивелироваться - ведь речь по сути идёт о десятитысячных долях секунды.

  34. Google user

    @Денис Баженов, я с вами полностью согласен кроме одного пункта.

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

  35. Google user

    просто дома игрался, переключал между апачем и nginx-ом, ну и бросил его на апаче.

    Вот так в неизвестной фирме следят за деплоем. :)

  36. Денис Баженов

    Про "синхронность диска". Вобще-то диск не обязательно (хотя и очень часто, да) весь такой блокирующий. Один раз прочитал сектор с блинов, десять раз мгновенно отдал его из кеша.

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

    А новые SSD диски и вовсе не имеют "одной головки", то есть потенциально могут параллелить свою работу.

    Как показывает практика, к сожалению, нет, - не могут. Любой девайс имеет шину адреса в единственном экземпляре доступ к которой не может быть распараллелен. Единственная возможность распараллелить иметь два жестких. В случае с SSD все еще сложней. Например, Intel'овые SSD-шки серии X мейнтейнят внутри таблицу хранящую информацию о количестве циклов перезаписи секторов. Это необходимо чтобы жесткий "устаревал равномерно", так как у SSD ограниченное кол-во циклов перезаписи. Я подозреваю, что у других производителей то же самое, я просто читал только про Intel'овые иксы (у нас лежит пара X25E и мы незнаем куда их засунуть :) ). Проблема в том, что даже если распараллелить шину адреса/данных, то запись на девайс будет испытывать contention на этой таблице. Это ведь такой же самый shared state, только внутри девайса. Или мы внутрь жесткого запихнем erlang VM с message passing'ом и actor'ами? :)

    Конечно, ничего не мешает SSD предоставлять implicit параллелизм. Быть снаружи sequential device'ом, но внутри параллелить запись (что-то типа embedded RAID 0). Но давайте посмотрим на аналоги. Так называемая, dual/tripple channel память предоставляет implicit параллелизм. Контроллер предоставляет наружу более широкий канал и сам дробит данные между планками (при этом сам контроллер параллельно использоватся не может). По некоторым оценкам, (http://www.tomshardware.com/reviews/PARALLEL-PROCESSING,1705-15.html) разница в производительности между single-channel и dual-channel на современных процессорах не превышает 5%. Вот вам и параллелизм.

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

  37. Сергей Шепелев

    Если у вас есть много производительности, то может вам и не нужна масштабируемость?

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

    Только вот, словоблудие всё это. В реальной жизни есть куча разных факторов, типа того же кеша HDD. Никто не мешает ему отдавать из кеша одно и при этом перемещать головку в другое место. Действительно в одно и то же время. И если файловая система снаружи будет думать, что он способен выполнять только одну операцию за раз, то получится медленнее и будут простои. И AIO тоже ж не магия какая-то, лишняя скорость ниоткуда не возьмётся, оно просто позволяет эффективно выжимать все существующие ресурсы. Иногда распараллеливание возможно благодаря убиранию лишних ожиданий и локов. :)

    Но это всё не про то. Вы, в самом начале, писали про "параллелить IO" и я, вроде бы, к этому придрался, что его таки да, иногда можно паралеллить. А Иван писал не о параллельном, а об асинхронном IO, что гораздо более важно и всегда (по-моему) единственно верно. Так, в последних комментариях к предыдущему посту я спрашивал какой может быть толк от синхронного (когда поток ждёт) ввода-вывода. По-моему, никаких. А от параллельного, вы совершенно правы, если и будет толк, то мизерный.

  38. Ivan Sagalaev

    А Иван писал не о параллельном, а об асинхронном IO

    Про него я особенно не писал. Просто потому, что с ним работать уже все научились, и хватает поддержки его не языком, а библиотеками. Меня как раз интересует асинхронность вычислительных процессов помимо IO.

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

    Если уж зашла о нём речь, то неблокирующий IO не блокирует не потому что из кеша отдаёт. Неблокирующий IO работает за счёт того, что вы можете сказать ОС: "вот из этого файлового дескриптора положить в этот буфер мегабайт", и потом не ждать этой операции, потому что для неё не нужен процессор. ОС организует перекладывание данных через direct memory access между диском и памятью, продолжая загружать процессор остальной вашей программой. А уж сработает там кеш у HDD или нет — детали реализации.

  39. Денис Баженов

    Я рад что мы пришли к консенсусу.

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

    Железо будет менятся (посмотрите, http://www.youtube.com/watch?v=BBMeplaz0HA)). А вы предлагаете переписать все. Переписать все под железо, которое лет черяз пять будет совсем иным?

  40. Кирилл Маврешко

    У меня сложилось чувство, никто из комментаторов так полностью и не поддержал автора (в обеих статьях). Большинство пытается доказать, что никакие Clojure — не нужны, и обычных императивных языков со стандартной моделью разработки на локах — более чем достаточно для наступления счастливого будущего.
    Хочется спросить их, а как они себе представляют это самое будущее? Лично у меня есть все основания представлять его так:

    1. У процессоров будет не просто 2-8 ядер. Их будет 200-800, пусть даже большинство из этих ядер будет достаточно туповатыми.
    2. Современные HDD вымрут, как динозавры. На смену придут либо SSD, либо иная очень быстрая твердотельная технология. Весьма вероятно, что грань между ОЗУ и ПЗУ вообще исчезнет. Кроме того, транзакционная память начнёт поддерживаться аппаратно (процесс уже идёт).
    3. Пропускная способность сетей будет совершенно дикой.
    4. В далёком будущем, "вычислительная мощность" может стать частью инфраструктуры, как сейчас ею являются электроэнергия, водопровод, отопление, телефон... Это значит, что на руках у людей будут сравнительно простые и тупые машинки (тонкие, очень тонкие клиенты с копеешной стоимостью), а вся реальная нагрузка будет приходиться на ближайший Центр Обработки Данных (вплоть до рендеринга графики в играх). За это мы будем платить примерно также, как сейчас за киловатты платим (или за Amazon Services/Google App Engine). И писать программы придётся с расчётом на их выполнение именно в таком ЦОД'е.

    Иными словами, нас ждёт не просто "параллельное будущее". Уровень параллелизма будет просто зашкаливающим, зубодробительным. Лично я считаю, что первые три пункта мы точно увидим в ближайшие 10 лет. Не знаю как вы, а я на пенсию через 10 лет ещё не собираюсь.

    Потому-то нам как воздух необходим более-менее универсальный подход к разработке параллельных приложений. В том числе, с асинхронным I/O.

    Чем плох современный подход? Я не буду повторять доводы автора и других комментаторов, приведу лишь ещё одну аналогию, из мира Windows. Файловая система (скажем, FAT) разбита на логические единицы, именуемые кластерами. Кластеры имеют определённую длину (скажем, 32Кб), которая является минимальной длиной, допустимой для хранения файла. Поэтому, если ваш файл имеет длину 2 Кб — он всё равно займёт 32Кб. Если у вас 1024 файла по 2Кб, то вы уже впустую потеряли 30 мегабайт. И такая потеря происходит на каждом файле, записанном на диск, т.к. он всегда займёт целое число кластеров, и последний будет в 99% случаев заполнен не до конца. Потери, в итоге, исчисляются гигабайтами!

    Мне кажется, что аналогичная проблема возникает сейчас при использовании многоядерных процессоров. Подобно кластерам файловой системы, ядра оказываются загруженными очень неравномерно — где-то "пусто", а где-то "густо". А что дальше? Каковы будут потери на 128-ядерном процессоре? 20%? 30%? Не слишком ли много? И никакие планировщики ОС не исправят эту ситуацию, т.к. сами программы и библиотеки написаны бестолково. И параллельность в них — это исключительные случаи, особые части алгоритмов, но никак не естественное состояние бытия.

    И тут, похоже, действительно придётся менять язык. Мне нравятся функциональные языки, вроде Clojure/Haskell, тем что они допускают ещё и динамическое распараллеливание, основанное на решении VM о том, что "данный участок исполняется достаточно долго, при этом поддаваясь распараллеливанию". Например, VM может сама попробовать выполнить цикл в OpenMP-стиле. Аналогичным образом работают СУБД, автоматически выбирая оптимальный план исполнения запроса, на основании собранной статистики. Словом, параллелизм тут "в крови" — то самое, естественное состояние бытия...

    И да, всё действительно придётся переписать :)

    P.S. js/showdown.js возвращает 404-ю ошибку. Видимо поэтому не работает предпросмотр комментариев. Всем могу посоветовать пока использовать http://attacklab.net/showdown/

  41. Сергей Шепелев

    Если уж зашла о нём речь, то неблокирующий IO не блокирует не потому что из кеша отдаёт.

    Конечно, это я ушёл в сторону параллельного IO. Когда, например, два файла читаются действительно одновременно.

  42. Сергей Шепелев

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

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

    А вот модель разработки на локах к языкам не относится. Кто сегодня пишет на локах, тот и завтра будет писать на локах (и железо ему будут покупать с 16 ядрами вместо 96, потому что бесполезны ему ядра.

  43. Сергей Шепелев

    И тут, похоже, действительно придётся менять язык. Мне нравятся функциональные языки, вроде Clojure/Haskell, тем что они допускают ещё и динамическое распараллеливание, основанное на решении VM

    Это отличная мысль, Кирилл! Только вот очень грустно чем именно она отличная. Функциональные языки, чистые вычисления, статический анализ... столько шансов. И никто не использует их для динамического или автоматического распараллеливания. Казалось бы, хацкель-то всё может, всё умеет, а параллелить руками надо. Эрланг, кстати, в этом плане смотрится очень интересно, потому что практически навязывает создавать множество потоков, а уж дальше они на ядра-то расползутся.

  44. Сергей Шепелев

    @Иван

    Меня как раз интересует асинхронность вычислительных процессов помимо IO.

    Так мы увлеклись этим IO; только теперь я чётко понял о чём были посты. :)

  45. Никита

    Иван, в Erlang можно указать модулю атрибут -compile(export_all) для экспортирования всех функций.

  46. Валерий Замараев

    Возьмем, к примеру, тот же XML парсер. Допустим, у нас есть парсер, в который можно запихнуть полностью документ, и либо получить DOM на выходе, или отловить кучу callback'ов в SAX стиле. Парсер работает с одним CPU. Я думаю, это как раз случай существующей библиотеки, которая не готова ни к асинхронности, ни к multicore.

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

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

    Поэтому, переписывание для асинхронной модели и для multicore - это практически ортогонально. И собственно выбор, на каком языке это лучше делать, как раз зависит от того, в какую сторону относительно стандартной модели one-thread-per-client, или thread-pool мы хотим отклониться. Хочешь делать библиотеку, которую легко использовать в контексте twisted или node - это одно. Хочешь задействовать множество ядер даже для небольших задач - привет функциональным языкам.

  47. Ivan Sagalaev

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

    Нет такого термина — "асинхронная модель". Асинхронность как таковая никакой модели обработки не подразумевает. Вот асинхронный I/O — действительно подразумевает модель обработки, когда один поток обработки реагирует на события появления в сокете данных.

    Так вот, я не об этом :-).

    Цитата из предыдущего поста:

    Почему бы лексинг разных кусков не делать в несколько потоков (map), которые потом объединять в единый парсинг (reduce), причём возможно тоже в несколько потоков?

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

  48. Всеволод Дёмкин

    Для concurrency есть очень хороший, на мой взгляд, перевод — конкуренция.
    (Соответственно и concurrency-oriented programming хорошо переводится :)

  49. Powerman

    Поэтому, переписывание для асинхронной модели и для multicore - это практически ортогонально.

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

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

    Но лично я думаю, что асинхронность реализуемая и в стиле неблокирующего I/O, и в стиле асинхронного I/O с callback-ами, и вообще любые отложенные операции с callback-ами - это плохая идея.
    Во-первых, этот подход, как правило, подразумевает работу в рамках одного процесса/нити, т.е. использование одного CPU (этого, как правило, достаточно сегодня, но вряд ли это будет приемлемо уже завтра).
    Во-вторых, этот подход заметно усложняет код. Т.е. увеличивает стоимость разработки и поддержки. И добавляет багов.
    В-третьих есть отличная альтернатива, лишённая всех упомянутых в этом и предыдущем топиках недостатков в области async/concurrent-программирования: многопоточное программирование в стиле CSP.

    На CSP можно писать обычный, простой, линейный код, делать блокирующие вызовы I/O и библиотек, и при этом нагружать все CPU. Те же библиотеки, не смотря на блокирующий внешний интерфейс, могут спокойно распараллеливать вычисления. Если нужно получать от библиотеки данные по мере обработки, не дожидаясь полной обработки данных - в библиотеку параметром передаётся не callback, а канал (из которого может читать другая нить, тоже с простым линейным кодом).

    Проблема, как я понимаю, сейчас в отсутствии поддержки CSP "из коробки". В принципе примитивы CSP (каналы, мультиплексирование каналов) не трудно реализовать традиционным образом, через shared memory и блокировки. Но стиль CSP поощряет использование громадного кол-ва нитей, так что ещё одним требованием становится поддержка очень лёгких нитей, которые можно запускать и завершать тысячами без какого либо удара по производительности. Ну и вообще, поддержка сильно типизированных каналов и сборка мусора для каналов скорее всего всё-таки потребует более основательной поддержки от языка программирования. Сейчас, насколько я понимаю, нормально всё это реализовано только в C для Plan9 и в Limbo для Inferno. :( Ну и на русском о CSP толком почитать нечего. Краткий обзор что это и зачем на английском: http://swtch.com/~rsc/thread/

  50. akzhan-abdulin@yandex.ru

    Кстати, шифрование как раз очень полезно делать асинхронно.

  51. Иван Сагалаев пишет:

Добавить комментарий