Pull to refresh

Comments 40

Что-то не улавливаю. Поясните, плс?

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


Во-вторых, значение status::up может "застрять" в кеше процессора, из-за чего цикл сделает кучу холостых операций.


Если один поток записал в переменную некоторое значение — это ещё не означает, что другой поток именно это значение и прочитает.

Во-вторых, значение status::up может «застрять» в кеше процессора, из-за чего цикл сделает кучу холостых операций.


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

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

Нельзя менять/читать значение неатомарной переменной из разных потоков. Если компилятор сможет доказать что в этом потоке переменная не меняется, он может выкинуть проверку нафиг или считать значение из памяти один раз, заменив проверку в цикле на if перед бесконечным циклом.
да хватит, хватит уже, первого комента достаточно.
Я уже так привык, что race condition всегда называют своим термином, что и думать забыл о нём, как об UB.

Ну, для такого и придумали volatile. Что впрочем не отменяет необходимость синхронизации чтения/записи

Так-то volatile != atomic. Но да, вы правы, использовался он чаще именно для этого

Ну нет же, не для этого придумали volatile. И даже deprecated ему собираются прикрутить в таких сценариях.
Вроде только в C++, в нормальном языке вроде оставляют, нет?
Хз что там в «нормальном» языке, но использование volatile для многопоточки — вещь, за которую надо увольнять. Да, volatile защищает от оптимизаций компилятора, но он не делает UB код меньшим UB, просто заметает баги под ковер. Ровно до тех пор пока вы не запустите свой «офигенный» «быстрый» алгоритм на volatile и reinterpret_cast'ах на каком-нибудь ARM.
К сожалению, x86 и друзья являются «strong ordered» архитектурами и поймать instruction reorder там сложно (вот хорошая статья, демонстрирующая, как его всё-таки словить), а значит ваш супер-мега «локфри» на волатайлах будет успешно «работать», возможно в продакшне и не один год.
Вы такой категоричный! Не поверите, мой код работает как раз на «каких-то» ARM'ах, и volatile там вполне достаточно. На более продвинутых процессорах — там да, нужны барьеры памяти, даже в MMIO регистр записать. Но на тех платформах, работа с которыми заполняет мои дни — нет. Поэтому я использую «нормальный язык», в котором reintrpret_cast — имя переменной. И из него работающий инструмент не выбрасывают.
Вы же сейчас о ключевом слове volatile, который вешают на переменную, а не о штуках типа asm volatile("" ::: «memory»);?
Если да, то у меня для вас плохие новости — либо у вас где-то есть барьер, который маскирует проблему, либо вы просто ещё не нашли багу.
Ключевое слово volatile не спасает от проблем многопоточности на relaxed-ordered архитектурах, потому что инструкции может переставлять процессор, а не компилятор (точнее, это могут делать оба, но volatile запрещает только второму).
Другое дело, что стандарт «нормального языка» ничего не говорит о многопоточности и memory ordering (поправьте, если ошибаюсь), а значит оставляет вас и компилятор в серой зоне — компилятор может вам помочь и напихать барьеров памяти, видя volatile, а может и не помогать и тогда ваш код будет содержать трудноотлаживаемую багу.
Я дико рекомендую вот этот цикл статей на Хабре, развеивает кучу мифов и даёт представление о том куда и на что смотреть, если поймали data race habr.com/ru/post/195770.

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

Для многопоточности этот принцип не работает. Баги, возникающие из-за гонок, "ловить" слишком тяжело, их нужно не допускать.

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


Зато я сходу вижу гонку между TcpServer::stop и TcpServer::handlingLoop...

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


Блокировки вообще не обязательны для многопоточного приложения. Достаточно CAS и других атомарных операций.

Но CAS и других атомарных операций я тоже не вижу...

Как-то раз встала задача по написанию простого и быстрого многопоточного TCP/IP сервера на C++ и при этом, чтобы работал из под Windows и Linux без требования как-либо изменять код за пределами класса самого сервера.

Можно нескромный вопрос: "встала задача" по учебе (в качестве лабораторной или курсовой) или же по работе?

bool TcpServer::Client::sendData(const char* buffer, const size_t size) const {
if(send(socket, buffer, size, 0) < 0) return false;
return true;
}


А если send вернет больше 0 или 0?
А если при этом send послал меньше чем просили?
Да. Но это ведь ошибка, вы не послали все данные в сеть и вернули, что «всё хорошо».
К сожалению, в современном мире такие сервера уже почти никому не нужны по причине отсутствия шифрования данных.

это далеко не самая боььшая проблема, плюс есть много мест, где шифрование делается на уровнях выше

for(std::list<std::thread::id>::iterator id_it = client_handling_end.begin (); !client_handling_end.empty(); id_it = client_handling_end.begin())
for(std::list<std::thread>::iterator thr_it = client_handler_threads.begin (); thr_it != client_handler_threads.end (); ++thr_it)
if(thr_it->get_id () == *id_it) {…

Может лучше map юзать? Квадратичная сложность — так себе идея

Там на самом деле надо в список client_handling_end сразу поток класть, а не его id. И из client_handler_threads поток исключать сразу же, а не в конце.

Каждый такое писал, но не каждый рискнул выложить.
Согласен, но замечу, что самое интересное начинается, кода это действительно становится рабочим проектом, и ты начинаешь смотреть в сторону pool, epool, WSAPool, разбираешься с реактором и проактором… И для следующего проекта берешь либо boost либо poco :-)
есть же libuv, где при желании можно запустить N лупов по количеству ядер и получить многопоточный сервер вместо асинхронного event-based.

100-пудово. Больше 10 лет тому назад в качестве домашнего задания при собеседовании получил подобное задание. К тому времени во всю использовал уже boost.asio.
Начал делать, думал за пару вечеров управиться. Но писать плохо не хотелось. Гляжу, сроки задания едут, а выходит что-то типа упрощенного asio. А тут и другой работодатель меня и нашел :)

while (size == 0) size = client.loadData();

->
while (!(size == client.loadData()));

А ещё, как я непрофессионально вижу, можно платформенные дефайны поставить в один конструктор, чтобы в целом меньше кода было

Небольшая поправочка:


while(!(size = client.loadData()));

Требуется запомнить размер буфера в size, иначе можно было и вовсе написать:


while(!client.loadData());
while (!(size == client.loadData()));

Зачем несколько действий совмещать в одну строчку,
"job security"?

Sign up to leave a comment.

Articles