All streams
Search
Write a publication
Pull to refresh
28
0
Send message
В «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 в его деструкторе.

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

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

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

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

4. Это — под задачу. Серверы, с которыми я имел дело, написаны на Си, с трудом справляются с нагрузкой, и расширения Mt там пригодились бы.
Через аннотации. Определять условия, которые должны быть соблюдены при доступе к элементу данных (например, должен быть захвачен 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
}
Смотря что называть декларатором. В терминологии C++ деклараторы в объявлении «int a, *b, * const &c» — это «a», "*b" и «const &c». В этом смысле ничего не меняется.
В своих проектах я такую схему использовал весьма успешно. Это как раз и есть схема синхронизации, к которой всё пришло в результате продолжительных попыток и размышлений.
Для больших объёмов данных — отдельные высокоскоростные стораджи, доступные через сеть, плюс система кэширования, чтобы снизить задержки до минимума. На локальный диск можно логи писать разве что.
Смысл обработчиков событий при асинхронном обслуживании в том, что обработка конкретного события должна происходить достаточно быстро, чтобы не задерживать обработку других событий в том же потоке. Поэтому подразумевается, что автоматические блокировки не слишком долгие. Если нужно выполнять тяжёлые расчёты, то мы выпадаем из асинхронной модели. Например, запускаем новый тред и считаем в нём.

О взаимоблокировках: я использую очень простое правило, позволяющее о них забыть: одновременно захвачен только один мьютекс состояния какого-либо объекта. Перед вызовом асинхронного метода объекта B из асинхронного метода объекта A необходимо освободить мьютекс на состояние A. Это позволяет методу B обращаться к методам A без ограничений с любой вложенностью. Оператор call подчёркивает, что состояние объекта A до и после вызова не синхронизировано. Это усложнение — плата за отсутствие взаимоблокировок. Автоматическое соблюдение этого правила — одна из задач Mt.
Если действительно не найдётся альтернативного подхода, то можно будет расширить язык. Но гнаться за STL я бы не стал: из посылки, что шаблоны C++ — слишком мощный инструмент, следует, что и STL слишком мощна.
Я как раз питаю надежды сделать простым и надёжным использование модели threads&locks, не используя новые необычные подходы. Пишем как раньше, делегируем большую часть работы транслятору, и получаем от него дополнительные статические проверки на MT-safety.
Потому что большинство проектов, которые могут выиграть от использования Mt, написаны на C/C++. Потому что я использую C/C++ в своих проектах, и хочу улучшить проверенный, надёжный инструмент. Я читал про Go, и он мне не приглянулся: не увидел в его фичах настоящей изюминки, а текущая реализация, судя по тестам, слишком заметно проигрывает по производительности чистому C. При выборе базы хочется надёжности. В C я уверен, в Go — нет.

Information

Rating
Does not participate
Location
Redmond, Washington, США
Registered
Activity