четверг, 19 февраля 2009 г.

Многопоточное программирование превращается в кошмар?

В последнее время много читаю о проблемах, которые можно получить в многопоточных программах на современных многоядерных архитектурах (см. мегасообщения Димы Вьюкова на RSDN: "Многопоточность сегодня" и "3 базовых вещи относительно параллельных вычислений"). Страшно становится. Вот, кстати, одна из последних страшилок на эту тему от Герба Саттера - "Sharing Is the Root of All Contention" (статья интересная и, имхо, полезная - рекомендую).

Вот чего я не могу себе представить, так это как программисту учитывать все эти факторы в своей повседневной работе. Ведь ситуация такова, что нам приходится делать все больше и больше функциональности за меньшее время. В связи с чем постоянно идет переход к новым высокоуровневым языкам, библиотекам и технологиям. Взять тот же C++. Хочешь, чтобы программисту было удобно, чтобы он писал быстро и надежно -- используй STL. Вот тот же std::string -- удобно? Удобно! Но насколько эффективно? Особенно, если пишешь что-то вроде:

std::string make_some_params(
         const std::string & a,
         const std::string & b )
{
	return a + "::" + b;
}

А ведь пишешь так как раз для того, чтобы не возиться с ручным выделением памяти, ручным вызовом strcpy, strcat, чтобы не забыть освободить выделенную память. Ну да ладно std::string. Его еще хоть можно заменить на char*. А что делать с map или hash_map? И такие классы в больших прикладных программах всего лишь самый нижний слой.

Это я все к тому, что пока сложность разрабатываемой тобой системы позволяет задумываться над эффективностью операций с std::string/std::map, тогда еще можно как-то держать в уме вопросы размещения данных по разным линиям кэша. Но что делать, если сложность системы гораздо больше? Вот мне сейчас нужно сделать небольшое распределенное приложение, состоящее из двух процессов, связанных каналом связи. Приложение на SObjectizer, один из процессов при этом еще и работает с БД. Самой прикладной логики, которую мне предстоит написать с нуля, там очень немного. Зато сколько всего оказывается скрыто под капотом! Внутри SObjectizer, внутри ACE, внутри OTL, внутри ODBC... Какие там операции выполняются, насколько они дружелюбны и ориентированны на современные Core Duo, QuadCore и прочее? Тайна сия велика есть... Как и границы моих возможностей в случае необходимости увеличить производительность всего этого комбайна :(

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

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

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