Pull to refresh

Comments 9

Пример состояния гонки, как не надо создавать потоки в С++. По мотивам найденного в реальном софте. Воспроизводилось только на определённой архитектуре.

Будет печатать "111 -1", а не как предполагается "111 999", "X() finished" напечатает после чисел. Потому что поток стартует раньше чем завершится инициализация класса. Вместо задержки в 1мс была инициализация кучи других классов.

#include <stdio.h>
#include <chrono>
#include <thread>
#include <cstring>
#include <new>

struct S {
    S() {
        std::this_thread::sleep_for(std::chrono::milliseconds(1));
    }
};

struct X {
    void worker() { printf("%d %d\n", first, last); }
    X() : t(&X::worker, this) { puts("X() finished"); }
    int first = 111;
    std::thread t;
    S s;
    int last = 999;
};

int main() {
    alignas(X) char buf[sizeof(X)];
    memset(buf, -1, sizeof(buf));
    new(buf) X();
}

Потому что поток стартует раньше чем завершится инициализация класса.

Не, ну голова-то дана не только чтобы в нее есть...

Потоки создаем в состоянии suspended, завершаем всю инициализацию и только потом переводим его в состояние active. Как-то так...

Это не про гонки и не про синхронизацию разделяемых ресурсов. Это просто про понимание того что когда и как там внутри происходит.

Давайте представим, что мы работаем над созданием интернет-банкинга, предоставляющего клиентам доступ к своим финансовым данным и операциям через веб-интерфейс. 

Вы бы хоть другой пример выбрали. Потому что там все это происходит ну совсем не так. И никаких гонок на уровне клиентов там нет и быть не может.

Второй момент. Есть разделяемые ресурсы, совместное использование которых приходится регулировать разработчику. Например, разделяемая память. И есть ресурсы, за совместное использование которых отвечает система. Например, очереди, сокеты, файлы.

Строго говоря, к С++ все это вообще никакого отношения не имеет, это вопрос общей архитектуры.

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

И вот здесь нет смысла связываться с разделяемой памятью - это слишком сложно и оправданно только тогда когда требуется предельная скорость именно обмена данными.

Можно использовать системные средства. Например, именованные локальные сокеты (UNIX socket). Или майлслоты (Win). Или очереди (*usrq, *dtaq в as/400). Каждый поток (процесс) создает свой объект с уникальным именем который является "почтовым ящиком" для него. Он оттуда будет читать все, что ему посылают другие. Все остальные могут туда писать все что нужно передать данному потоку.

И здесь все вопросы синхронизации отдаются на откуп системе, она за всем этим следит. Лично использовал такой подход в ситуации когда было три потока - один коммуникация с удаленными контроллерами по UDP, второй коммуникация с интерфейсными клиентами по TCP и третий - обработка и маршрутизация посылок от клиента к контроллеру и от контроллера к клиенту. И никаких гонок, никаких проблем с синхронизацией при минимальных затратах.

Второй пример, с чем часто сталкиваюсь сейчас - параллельная обработка больших объемов данных. Есть головное задание, которое производит первичный отбор данных для обработки, есть несколько (5-10) заданий-обработчиков которые уже занимаются обработкой каждого блока данных. И есть конвейер (у нас - *usrq). Головное задание выкладывает отобранные данные на конвейер, обработчики их оттуда разбирают и обрабатывают. И опять - вся синхронизация на системе. На разработчике только "положить в очередь" и "взять из очереди".

Может стоит добавить в обе статьи про UB дисклеймер: "Приведённые в статье фрагменты кода служат сугубо для демонстрации, не стоит использовать их в реальных проектах (ну, по крайней мере берите фрагменты, которые помечены как исправленный код)"?

Ну, а если серьёзно, то большое спасибо за такой развёрнутый фидбек. Очень интересный и полезный комментарий.

Пример, как было описано, сугубо синтетический и не несёт в себе не то, что близости к продовскому коду, а даже и особой осмысленности:) Как-то раз, я ревьювил смарт-контракт на Solidity и думал: "Ё моё, в этот писец бы ещё многопоточку добавить". Ну, время пришло и я реализовал что-то похожее. Интернет-банком это стало по остаточному принципу.

Я просто знаю как мобильные клиенты устроены (точнее, как они связаны с реальными данными) - от реальных данных они бесконечно далеко. У них есть REST API - они просто дергают запросы. А дальше это длинным путем идет до центрального сервера, где уже выполняется. Там даже нет понятия "аккаунт". Есть ПИН клиента и все запросы идут по нему.

И перевести деньги со счета на счет - это не просто переложить их из одного аккаунта в другой. Это создать платежный документ и отправить его в очередь на обработку. Т.е. никаких "гонок" и конфликтов тут в принципе не бывает.

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

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

Это даже без учета блокировок чтение-запись.

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

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

Но, повторюсь, все это не имеет отношения к конкретному языку, все это из области общей архитектуры IPC (InterProcess Communications).

Пример уб с семантикой перемещений неверный, в c++ для классов с user-defined деструктором не генерятся move-конструктор и move-оператор= -> resource просто копируется, никакого уб там нет

А первый пример вообще валидный? В упор не понимаю, как два хранящихся по значению std::string могут указывать на один и тот же ресурс

Sign up to leave a comment.