Как стать автором
Обновить

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

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

Молодец. Умело обгадил большинство присутствующих.
Сам то будешь участвовать в проекте? Или тоже не знаешь программирование на достаточном уровне?
Тоже не знаю.
Обгадил? Ну это уж как вы сами себя оцениваете.
И да, если вы считаете, что программистов, способных участвовать в написании «языка Mt» больше, чем дизайнеров, менеджеров, веб-разработчиков, переводчиков и других, вместе взятых, то вы явно говорите не о Хабре.
Или большинству просто не нужны поделки. Они знают, как правильно работать с примитивами синхронизации в Си, пользуются libev и игнорируют бесполезные уровни абстракции.
Почему не взять тот же Go а не заниматься велосипедостроением?

Больше устройств поддерживают C и C++?
Ну и, конечно, опыт бесценный.
Это что за устройства такие? И причем тут С/С++?
Хм, блин, видимо в статье описан не транслятор Mt -> C :)
Больше устройств поддерживают C и C++?

Что вы имеете в виду?
Что, вероятно, Go получится запустить на тех же ОС/железяках, что и C. Может это и не так, просвятите :)
* не получится
C/C++/Go — компилируемые языки. Они хоть в код машины Тьюринга могут компилироваться. Понятия «устройства, поддерживающие C++» попросту не существует. Есть устройства в машинный код которых программы могут компилироваться. В мейнстриме на серверах сейчас не такой разброс процессорных архитектур, что бы текущие реализации Go его не покрывали.
Ага, понятно.
Я не так сформулировал конечно. Имел ввиду, что, например, нет компиляторов под какую-нибудь ОС и этой ОС оказалась винда, как гугл подсказывает.
Винда поддерживается. Более того, каждое изменение в основной ветке тестируется на всех поддерживаемых платформах, включая винду: Go Dashboard

Прямо сейчас винда в нормальном состоянии, а вот армовский билд сломали 20 коммитов назад. Тут надо понимать, что в этом нет ничего страшного, т.к. это сломана только «рабочая» версия. Раз в две недели они выпускают «релизы», в которых все зеленое.
У Go есть GCC frontend, а это значит, что он может компилироваться во все, что шевелится.
Потому что большинство проектов, которые могут выиграть от использования Mt, написаны на C/C++. Потому что я использую C/C++ в своих проектах, и хочу улучшить проверенный, надёжный инструмент. Я читал про Go, и он мне не приглянулся: не увидел в его фичах настоящей изюминки, а текущая реализация, судя по тестам, слишком заметно проигрывает по производительности чистому C. При выборе базы хочется надёжности. В C я уверен, в Go — нет.
> Я читал про Go, и он мне не приглянулся: не увидел в его фичах настоящей изюминки, а текущая реализация, судя по тестам, слишком заметно проигрывает по производительности чистому C.

Ну за то в вашей реализации изюма хоть отбавляй. Особенно повезет программисту, которому придется после вас разбираться в вашем препроцессоре без документации. В отличии от Go/Erlang/Whatever.

Что касается производительности — вы делаете запрос по сети для подгрузки данных, а это 5-10мс на tcp/ip, чтение данных с диска и пересылку по сети. Если вы очень круты, то это будет менее 1мс, но всё равно слишком медленно для того чтобы оправдать использование Си.

НЛО прилетело и опубликовало эту надпись здесь
Процессор не простаивает, а занимается… чем? Пасьянс раскладывает? В данном случае бОльшая часть работы заключается в ожидании данных. Тут не важно какой язык — C++/asm/bash. В любом случае пользовательский процесс будет торчать в wait.

Hint: вы когда-нибудь видели чтобы nginx грузил процессор?

НЛО прилетело и опубликовало эту надпись здесь
> Вы вообще знакомы с тем, как работает асинхронный ввод/вывод?

1. Вообще знаком. Кстати epoll/kqueue — это не асинхронный IO, это event notification.

2. В случае, когда вычисления занимают незначительную часть, можно использовать любой скриптовый язык с поддержкой epoll/kqueue (Python twisted, ruby event machine, итд), а не изобретать велосипеды для С++.
1-2. php с libevent :)
2. вообще говоря всё зависит от конкретных условий, в скриптовых (интерпретируемых) языках по умолчанию присутствует время на интерпретацию, если им можно пренебречь, то можно их использовать (если есть желание и возможность выучить ещё один язык и использовать его на продакшене), а если нельзя — нельзя :).

НЛО прилетело и опубликовало эту надпись здесь
Для больших объёмов данных — отдельные высокоскоростные стораджи, доступные через сеть, плюс система кэширования, чтобы снизить задержки до минимума. На локальный диск можно логи писать разве что.
2. Асинхронные цепочки обработки — очень близко к идее future/promise (уже есть в стандартно библиотеке C++).
3. Аннотации — есть в Visual C++
НЛО прилетело и опубликовало эту надпись здесь
Смысл обработчиков событий при асинхронном обслуживании в том, что обработка конкретного события должна происходить достаточно быстро, чтобы не задерживать обработку других событий в том же потоке. Поэтому подразумевается, что автоматические блокировки не слишком долгие. Если нужно выполнять тяжёлые расчёты, то мы выпадаем из асинхронной модели. Например, запускаем новый тред и считаем в нём.

О взаимоблокировках: я использую очень простое правило, позволяющее о них забыть: одновременно захвачен только один мьютекс состояния какого-либо объекта. Перед вызовом асинхронного метода объекта B из асинхронного метода объекта A необходимо освободить мьютекс на состояние A. Это позволяет методу B обращаться к методам A без ограничений с любой вложенностью. Оператор call подчёркивает, что состояние объекта A до и после вызова не синхронизировано. Это усложнение — плата за отсутствие взаимоблокировок. Автоматическое соблюдение этого правила — одна из задач Mt.
НЛО прилетело и опубликовало эту надпись здесь
В своих проектах я такую схему использовал весьма успешно. Это как раз и есть схема синхронизации, к которой всё пришло в результате продолжительных попыток и размышлений.
НЛО прилетело и опубликовало эту надпись здесь
Придумай) Потом, одно дело придумать, другое — столкнуться на практике. Мне схема неудобств не доставляет.
НЛО прилетело и опубликовало эту надпись здесь
В «call i->WorkImpl ()» i не может быть напрямую использован как аргумент в выражении (аргумент оператора -> ), потому что тогда будет обращение к данным без захваченного мьютекса. Для i будет сгенерирована временная переменная. Это есть в примере в хабратопике (tmp_num_back)

if (i != NULL) {
    FooImpl *tmp_i = i;
    state_mutex.unlock ();
    tmp_i->WorkImpl ();
}


Что означает вызов FooImpl::WorkImpl после вызова FooImpl::Free — это уже на совести программиста. По хорошему, надо бы писать

{
    i = NULL;
    call i->Free ();
}


А ещё лучше — просто «i = NULL», где i — ссылка, и чтобы это подразумевало освобождение ресурсов FooImpl в его деструкторе.

Кстати, для деструкторов есть тонкость: если деструктор вызван, когда заблокирован мьютекс на состояние какого-либо объекта, то вызов деструктора откладывается, пока не будет освобождён мьютекс.
НЛО прилетело и опубликовало эту надпись здесь
На идиому pimpl схема вообще плохо ложится. Очень нелогично иметь два мьютекса на один, по сути, объект. Я думаю, что Foo в этом случае не должен быть асинхронным объектом, а его методы
надо пометить как асинхронные (и транслятор не даст об этом забыть, потому что в теле функции используется оператор call). Проверку isInitialized при этом нужно переместить в FooImpl.

Не оглядываясь на суть pimpl в новом примере можно поставить «isInitialized = true» до вызова pimpl->Initialize(), или переместить проверку isInitialized в FooImpl::Initialize. Согласен, что это хорошая иллюстрация «подводного камня», на который напороться довольно просто. Это один из недостатков схемы синхронизации, но куда же без недостатков.

вам этот код странным не кажется?


Кажется)

FooImpl *tmp_i = i;
i = NULL;
call i->Free ();


Не очень красиво. Но, как я написал, есть способ сделать то же самое более изящно, с деструктором.
ох.
call tmp_i->Free ();
НЛО прилетело и опубликовало эту надпись здесь
НЛО прилетело и опубликовало эту надпись здесь
По замыслу, исключения в Mt не поддерживаются: и без них хватает вопросов на проработку.

Конечно, тут есть над чем подумать. Общее направление мне кажется перспективным: разбивать код на сравнительно короткие синхронизированные блоки. Проблема в том, что оператор call разбивает один блок на два разных, не синхронизированных между собой, и слишком легко об этом забыть.

Меня обнадёживает следующий момент: оператор async call делает такое же разбиение, как и call, и с ним уж точно ничего не поделать — в случае асинхронной обработки это единственно возможный путь. Поэтому есть разработчик понимает, как правильно работать с async call, то он умеет обращаться и с call. Возможно, если удастся придумать синтаксис, делающий разбиение на два блока более очевидным и лучше защищающий от ошибок, то картина станет более приятной.
НЛО прилетело и опубликовало эту надпись здесь
*косится на «Компиляторы..» Ахо-Ульмана*
про парсер: я правильно понимаю, что вы хотите оставить один декларатор для всего?
Смотря что называть декларатором. В терминологии C++ деклараторы в объявлении «int a, *b, * const &c» — это «a», "*b" и «const &c». В этом смысле ничего не меняется.
НЛО прилетело и опубликовало эту надпись здесь
Я как раз питаю надежды сделать простым и надёжным использование модели threads&locks, не используя новые необычные подходы. Пишем как раньше, делегируем большую часть работы транслятору, и получаем от него дополнительные статические проверки на MT-safety.
НЛО прилетело и опубликовало эту надпись здесь
Через аннотации. Определять условия, которые должны быть соблюдены при доступе к элементу данных (например, должен быть захвачен mutex, или должен быть захвачер r/w lock на запись), и учим анализатор проверять эти условия.

Например, пишем так:

// объявляем int a и говорим, что доступ к этой переменной
// разрешён только при захваченном my_mutex
int $mutex(my_mutex) a;

Помечаем методы lock/unlock для мьютексов как методы управления примитивом синхронизации:

void $lock     Mutex::lock ();
void $unlock Mutex::unlock ();

И дальше можем следить за использованием переменной:

void f ()
{
    my_mutex.lock ();
    a = 1; // Правильно
    my_mutex.unlock ();
    a = 2; // Ошибка
}

// При вызове этой функции my_mutex должен быть захвачен
void $mutex(my_mutex) g ()
{
    a = 3; // Правильно
    my_mutex.lock (); // Ошибка: my_mutex уже захвачен
    my_mutex.unlock ();
    a = 4; // Ошибка: не захвачен my_mutex
}
А понадобится мне массив мютексов для организации fine-grain взаимного исключения над массивом ресурсов. Причём i-у ресурсу соответствует мютекс с номером (i+42)%N. N при компиляции не известно, изменяется динамически во время выполнения. Дальше пример можно не усложнять, уже тут возникают сложности как с синтаксисом аннотаций, так и со сложностью статического анализатора.
Именно так реализована STM в Clojure.
НЛО прилетело и опубликовало эту надпись здесь
Если действительно не найдётся альтернативного подхода, то можно будет расширить язык. Но гнаться за STL я бы не стал: из посылки, что шаблоны C++ — слишком мощный инструмент, следует, что и STL слишком мощна.
Я в общем понимаю мотивацию к созданию вашего расширения к С. Синтаксис этого языка, мягко говоря, устарел. и после современных высокоуровневых языков с разными плюшками на него больно смотреть.

Вы видели язык D (от digitalmars)? Там тоже C расширен, причем, на мой взгляд, очень неплохо.

1. Что касается асинхронных вызовов — а не лучше ли реализовать все через замыкания (через создание анонимного объекта)? А потом соответственно писать:

string cookie_data = '244t4t45t45t';
int uid = 5675;

check_cookie(uid, cookie_data, /* on Error */ function () { sendResponse(BAD_AUTH) }, /* on Success */
function() use (uid) {

get_user_profile(uid,… callbacks… ) // и так далее

})

Внутри функции check_cookie тоже делают все через callbacks.

Замыкания, например успешно используются в яваскрипте именно для асинхронных операций, и вообще, более универсальны, чем этот async chain. Видели ли вы Twisted?

2. Мне лично, например, не нравятся мьютексы. По моему, с ними больше времени уйдет на синхронизацию, чем на полезную работу, плюс высокие вероятности ошибок. А вот если делать асинх-е вызовы через замыкания — мьютексы там и не нужны.

А пример с async object MyFrontend — меня вообще немного пугает, получается там на каждую запись данных в поле объекта будут создаваться/освобождаться мьютексы? Как я понимаю, выполнение кода синхронизации займет раз в 10 больше времени чем само поленое действие?

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

4. И не кажется ли вам, что лучше было бы ограничиться написанием на Си именно разных хранилищ, а «скрипты выдачи страницы с фотками» наисать на более высокоуровневом языке? При эффективном взаимодействии с хранилищами даже PHP (который явно лучше работает с шаблонами чем Си) потянет 50-100 запр/с. А ведь еще есть яваскрипт, и другие языки.
>учитывая что счетчики ссылок требуют синхронизации — получается кошмар

Это плохие счётчики ссылок. Хорошие же используют lock-free реализации, без всяких там объектов ядра.
3. Автоматическое сбалансированное размазывание данных для многих процессов (не важно локально или по сети) может оказаться задачей не менее сложной и ресурсоемкой чем синхронизации, а у ручной несбалансированной довольно много недостатков
1. Конечно, синтаксис замыканий более удобен для коротких последовательностей действий, но если их на полэкрана, то пользы нет.

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

3. Да, накладные расходы на синхронизацию — важный вопрос. Основная идея в том, чтобы делать требующими синхронизации достаточно большие блоки действий. Если удаётся это сделать, то расходы на синхронизацию должны быть достаточно небольшими. Работу в несколько процессов тоже считаю предпочтительной, но это подходит только для тех задач, которые поддаются такому распараллеливанию. Большинство, вероятно, поддаётся, но не все.

4. Это — под задачу. Серверы, с которыми я имел дело, написаны на Си, с трудом справляются с нагрузкой, и расширения Mt там пригодились бы.
Молодец.

И область весьма нетривиальная, и до практического результата довёл.
Спасибо за статью, прочитал поставил плюсик, возникли вопросы:

Первый вопрос (чем не угодил с++) пропал сам собой, т.к. мне так и не удалось написать код который был бы хотя бы «почти так же лаконичен» как ваш и гарантировал защиту от невнимательности (пример невнимательности это «lock( this )», хотя должно быть «lock guard( this )»).

А второй вопрос вот какой:
Ваш синтаксис располагает к частому использованию async (и кроме того, к использованию встроенного счетчика ссылок), а это ОЧЕНЬ медленные операции, если они рассчитаны на мультипоточность, т.к. в каждой из них присутствует асмовский префикс lock. Приведу историю по этому поводу: однажды, я обнаружил, что в некоторых весьма редких случаях у наш код по ошибки пользуется одним и тем же EventServer-ом из разных потоков, что есть недопустимо, т.к. EventServer на это не рассчитан. Недолго думая, я добавил в сервер мьютекс, гарантирующий его правильную работу в многопоточной среде, это было очень плохая идея: после этого расчеты, которыми занимается программа, замедлились на 20% (!) процентов. Разбираясь с этим непонятным замедлением я пришел к вот какому выводу:

если
xchg eax,ebx


выполняется за, скажем 1 нс, то
lock xchg eax,ebx


выполняется уже за 1000 нс минимум (возможно с порядком ошибся, но разница была колоссальной)

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

С такими большими потерями производительности я не сталкивался, хотя специально собирал свои проекты с неатомарными счётчиками ссылок (без lock) и сравнивал. Возможно, EventServer был чрезвычайно часто используемым объектом?
> С такими большими потерями производительности я не сталкивался, хотя специально собирал свои
> проекты с неатомарными счётчиками ссылок (без lock) и сравнивал. Возможно, EventServer был
> чрезвычайно часто используемым объектом?

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

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

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

Как вариант, попробуйте сделать ключевые слова, которые явно или неявно вставляют префиксы lock, В ВЕРХНЕМ РЕГИСТРЕ, чтобы было сразу видно, что оно чем-то нехорошим занимается
Префикс lock касается только атомарных операций. Для синхронизации кэшей существуют барьеры памяти. Эти барьеры устанавливают библиотечные функции (например, pthread_mutex_lock).
> Префикс lock касается только атомарных операций.
ЛЮБАЯ синхронизирующая операция неявно использует атомарные операции и префикс lock.

> Для синхронизации кэшей существуют барьеры памяти.
Я вообще-то про кэш память процессора, lock с ней делает что-то не хорошее. В плане производительности это можно прировнять к кэш-промаху.
если разные ядра, то сначала запись в память, а потом чтение из нее, что больше чем кеш промах…
в смысле группы ядер, либо процессоры… так как в некоторых моделях L2 и L3 могут разделяться между несколькими ядрами, а доступ к этим кешам в пределах сотни тактов…
Очень интересная затея.

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

Насчет подсчета ссылок — а оно сильно надо? Тут пишут что lock-free реализации быстры. Но локи все равно имеют место быть, просто на более низком уровне. Для сетевого сервера акутально использование пула памяти. Когда для обработки каждого запроса выделяется блок памяти. После завершения запроса весь блок высвобождается.

Трындец, вы сами навелосипедили парсер С или С++ кода? Полная жесть, нафига это?
Если вам нужен экстеншн языка, не проще было бы взять уже готовый компилятор? Вон в GCC 4.5 можно плагины подгружать свои, clang вообще для этих целей с самого начала придуман.
А у вас есть ссылки на какие-нибудь howto/туториалы по этим вопросам (плагины для gcc, clang`овский фронтенд для C)?
llvm.org/devmtg/2010-11/

Посмотрите для начала libclang: Thinking Beyond the Compiler
спасибо за хороший инструмент
буду тестировать

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

Вычислительная асинхронность ( без завязок на ввод/вывод ) реализована лучше в вышеупомянутом Cilk и наследнике его идей Intel Threading Building Blocks ( это т.н. task-stealing scheduler ) Асинхронность ввода/вывода на основе примитивов платформы — другое и немного сложнее.

Мьютексы как краеугольный камень синхронизации — слишком наивно и грубо.

Платформенная независимость в на основе того что в основе С — недостаточно, потому что есть еще такая штука как модель памяти, atomicity, visibility, ordering. Плюс, не будем забывать, что С плохой выбор из за того что нельзя предсказать побочные эффекты. Кстати, из за этого гугловский Go не базируется на С.

А еще есть миллион мелочей которые непосредственно влияют на параллельные вычисления, хотя бы тот же cache false sharing.

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

Удачи!
Согласен с вами написанным, но от себя добавлю.
Работа с неуправляемым кодом требует более высокой квалификации программиста, а значит и более дорогого, что сужает аудиторию которая может воспользоваться технологией.
Си как очень низкоуровневый язык заставляет думать на несколько уровней абстракции ниже, соответственно продуктивность разработчика падает, качество кода невысокое. Цена любой ошибки падение всего процесса, что в случае с многими клиентами обычно недопустимо.

> Стоит хотя бы погуглить и посоветоваться со специалистами.
Автор выложил свою работу получил критику и думаю сможет изменить концепцию в правильном направлении. Мы все учимся на ошибках и не ошибается только тот кто не пробует.

Я думаю нужно изменить позиционирование платформы. Если бы автор создал скажем генератор расширений для Python/Node.js/Erlang/Ruby/Io/… которые с одной стороны позволяли писать наиболее критичные участки кода с минимумом затрат на том же Си, хотя снова замечу это не очень хороший выбор для веба, лучше уж D, то разработку может ждать более тёплый приём.
почему бы просто не поискать нормальную библиотеку синхронизации на С++?
по моему в LOKI есть хороший базис для такой библиотеки на основе автоматических переменных…
допустим этот код
        void processRequest ()
        {
            state_mutex.lock ();
            ++ num_back;
            unsigned tmp_num_back = tmp_num_back;
            state_mutex.unlock ();

            backend->doSomething (tmp_num_back);

            state_mutex.lock ();
            ++ num_requests;
            state_mutex.unlock ();
        }

будет выглядеть так
void Process()
{
  {
    AutoLock lock(SomeLock);

    ++ num_back;
    unsigned tmp_num_back = tmp_num_back;
  }

  backend->doSomething (tmp_num_back);

  {
    AutoLock lock(SomeLock);

    ++ num_requests;
  }

для большего удобства можно завернуть в дефайн
по-моему, не особо лучше он будет выглядеть, чем оригинал :)
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации