В данной статье я опишу свои самые свежие и яркие впечатления от многопоточного программирования. Это мои впечатления, мой опыт  и я буду рад, если он будет полезен другим программистам.

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

Предсказанное сбылось, как говорится, по полной программе. Думаю, что озвученные далее проблемы для кого-то не станут новостью, но будут и те, кто о них не подозревает, как не подозревал и я. А потому захотелось их зафиксировать и поделиться, с чем пришлось столкнуться.  Ну, и рассказать, как я выкрутился, попав в не совсем привычные для меня ситуации (в автоматном программировании, подчеркну, они не возникли бы в принципе).

Итак, создав ранее тест потоков (о нем подробнее см. [1]), гоняя его многократно и в разных режимах, я заметил, что пусть редко, но выскакивают некорректные результаты. В подобных случаях я грешу обычно на себя. А в данном случае тем более, т.к., что там скрывать, имею весьма небольшой опыт использования потоков.

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

Доступ к общему ресурсу

Ничто не предвещало беды, когда при увеличении числа потоков и достаточной длительности работы теста время от времени значение общей переменной-счетчика (далее просто счетчика) перестало быть правильным. Например, при десяти потоках и числе циклов у каждого из потоков равном миллиону (см. также первоисточник рассматриваемой задачи [2]) вместо десяти миллионов выскакивал результат на несколько единиц меньше. Ошибки, правда,  были редкими по времени, мизерными по значению и каких-то особых опасений не вызывали. Все списывалось на недостаток опыта работы с потоками и в надежде на последующую нормализацию работы теста.

Когда же игнорировать ошибки стало невозможно, то был реанимирован  аналогичный тест, но созданный вне среды ВКПа. Так исключалось ее возможное влияние. Хотя по заложенной логике код теста не был завязан с кодом ВКПа. Были лишь использованы ее диалоговые и визуальные возможности. И вдруг, работая до этого правильно, он напрочь сломался. Результаты походили на тестирование без синхронизации, хотя тест использовал мютекс. Произошло это после, казалось бы, косметических изменений кода потока, который был в данном случае просто приведен к аналогичному коду в ВКПа, мало по сути отличаясь от своего исходного варианта. Но если в ВКПа были только редкие ошибки, то в аналоге - при каждом запуске теста.

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

Запущенный поток (см. метод start() класса потока) "мгновенно" приступает к работе и, видимо, успевает отработать какое-то число циклов до старта следующих потоков. В том числе и до момента инициализации счетчика (вернемся к листингу 1). Таким образом, на момент сброса
ресурса потоки истратят какое-то число своих циклов и будут иметь их разное число по отношению к моменту сброса счетчика (см. листинг 2). При этом, когда счетчик сброшен до момента создания потоков, они его изменяют, включаясь последовательно в работу, и это не влияет на конечный результат. С новым тестом, когда ресурс сбрасывался после создания потоков, ситуация получается обратная, т.к. какое-то число циклов потоки отработают фактически бесполезно. Потому-то старый тест работает правильно, а новый - нет. Предположить иное сложно, т.к. только простое перемещение инициализации счетчика в другое место приводит к
безупречной работе теста.

Листинг 1. Код создания и запуска потоков
void MainWindow::Process() {
    bIfLoad = false;
    ui->lineEditMaxValue->setText(QString::number(nMaxValue).toStdString().c_str());
    ui->lineEditNumberOfThreads->setText(QString::number(pVarNumberOfThreads).toStdString().c_str());
    pCSetVarThread = new CSetVarThread();
    int i;
    for (i=0; i<pVarNumberOfThreads; i++) {
        CVarThread var;
        var.pQThread = new ThCounter(&nMaxValue, this);
        string str = QString::number(i).toStdString();
        var.strName = str;
        pCSetVarThread->Add(var);
    }

    timeLotThreads.start();
    pVarExtrCounter = 0;			// инициализация счетчика
    TIteratorCVarThread next= pCSetVarThread->pArray->begin();
    while (next!=pCSetVarThread->pArray->end()) {
         CVarThread var= *next;
         var.pQThread->start(QThread::Priority(0));
         pCSetVarThread->nActive++;
         next++;
    }
//    pVarExtrCounter = 0;		// инициализация счетчика
    bIfLoad = true;
    ui->lineEditTime->setText("");
    ui->lineEditCounters->setText("");
}

Листинг 2. Код потока
void ThCounter::run() {
//    while (!pFThCounter->bIfLoad);
    string str;
    int n=0;
    while (n<nMaxValue && bIfRun ) {
        bool bSm = pFThCounter->pIfSemaphoreLotThreads;
        bool bMx = pFThCounter->pIfMutexLotThreads;
        if (bSm || bMx) {
            if (bSm) pFThCounter->AddCounterSem();
            else {
                pFThCounter->AddCounterMx();
            }
        }
        else pFThCounter->AddCounter();
        n++;
    }
    pFThCounter->pCSetVarThread->nActive--;
    if (pFThCounter->pCSetVarThread->nActive==0) {
        pFThCounter->ViewConter(pFThCounter->pVarExtrCounter);
        pFThCounter->ViewTime();
    }
}

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

Доступ к общему ресурсу

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

Сделать это совсем просто. Для этого достаточно добавить одну строчку в начало кода потока, где он и будет ждать пока флаг не будет установлен (см. листинг 2). 

Ввел строку. Под отладчиком работает без вопросов. Вне - работа начинается, порой, как-то коряво, а потом тест вообще жестко виснет. Правда, если указать время ожидания завершения потока, то приложение самостоятельно завершит работу, выдав Fail Fast.  Ситуация из разряда кошмарного сна программиста. Просто потому, что отладкой ошибку "пофиксить" как-то не получается (самой-то ошибки, вроде, нет), а понять, где она кроется, только по поведению программы проблематично... И что в такой ситуации делать?

Думать...

Виснуть приложение может только, если поток не завершается.  Других вариантов, вроде, нет, т.к. все остальное прекрасно себя вело до этого - внесения цикла.. Т.е. он, пожалуй, единственный кандидат, к которому можно предъявить претензии. Решение - разорвать его, вставив выход из него по значению флага работы потока - bIfRun.  Другими словами, приводим данный цикл к виду: 

Листинг 3. Добавление цикла в начало потока
    while (!pFThCounter->bIfLoad) {
        if (!bIfRun) break;         // выход, если поток зависнет
    }

Тест виснуть перестал. Уф! - можно выдохнуть. Угадали. Теперь хотя бы тест можно перезапускать кнопкой Reset(VCPa). Это уже что-то, но радости мало, т.к. тест все равно не работает. Провал?! Думаем дальше... Напрашивается следующее решение: поскольку налицо проблема с циклом, то поступим жестче - уберем его совсем. Можно, например, перенести проверку во внутрь цикла потока? Пробуем, преобразовав основной код цикла потока к виду:

Листинг 4. Устранение внешнего цикла
    int n=0;
    while (n<nMaxValue && bIfRun ) {
        if (pFThCounter->bIfLoad) {
	...
            n++;
        }
    }
    pFThCounter->DecrementActive();

И -  о, чудо! - тест заработал (см. также видео к статье, ссылка в конце статьи). Как и чем это объяснить? Не знаю. Прямо двойная засада. Одна - это проблема добавленного цикла. Как он может стать вечным, если флаг явно установлен? Но, ведь, зараза, судя по реакции на флаг внутри него, - виснет?! Другая проблема - неэквивалентность эквивалентных преобразований. Предъявлять претензии к С++? Возможно. Но это мой взгляд дилетанта в проектировании компиляторов. Т.е. копать в эту сторону - себе, как говорится, дороже. Пусть судят специалисты в этом деле. В конце концов, есть простой проект, который эту "засаду" подтверждает и как бы устраняет. Правда, все это слабое утешение. Я же лично пока просто буду избегать подобных конструкций в потоке.

Выводы

На такой печальной ноте - жесткого виса теста, как предполагалось, будет завершена статья... Но, к счастью, решение нашлось. О нем я рассказал выше. Объяснить можно, конечно, все. В том числе и найденное решение. Оно не такое уж сложное и проблемы не было бы от слова совсем, если бы оно было изначальным (но тогда бы мы не узнали про засаду?!).

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

Поток - непредсказуемый и капризный партнер. В коллективе аналогичных "субъектов" его недостатки только множатся. При общении друг с другом - многократно.  Контролировать подобный коллектив весьма сложно (вспомним начало статьи [3]). Теория убеждает и даже доказывает, что вряд ли вообще такое возможно. По отношению к потокам, конечно.

А потому выбирайте правильных партнеров! Надежных, верных, предсказуемых. Ну, вы, наверняка, уже понимаете, куда я клоню... Однако, в любом случае, выбирая, думайте своей головой, а не видитесь на рекламу. Пусть даже многообещающую. А ее сейчас - сверх меры. Я же, например, жду, когда  искусственный интеллект от написания статей (про те же потоки) и политических выступлений (ака Вольфович) начнет качественно переводить техническую документацию. Как вы думаете, дождусь, а? Очень уж для меня это актуально. А то появились советы читать, мол, документацию...

Ну, а эту статью я написал, клянусь, сам. И даже код - не прибегая к услугам ChatGPT. А так хочется... халявы! ;) А интересно, что "генеративный интеллект" (так и подмывает сказать - дегенеративный) скажет про автоматное программирование, если ему задать такой вопрос? Задайте кто-нибудь... Или, кстати, может, он что-нибудь дельное посоветует по поводу последней засады? Пока мой интеллект здесь находится в полной отключке... Сломался то есть :(

Ссылка на видео - https://youtu.be/1-skz0PT0Zk

Литература:

1.    Все секреты многопоточности. [Электронный ресурс], Режим доступа: https://habr.com/ru/articles/818903/ свободный. Яз. рус. (дата обращения 11.06.2024).

2.    Многопоточность в Python: очевидное и невероятное. [Электронный ресурс], Режим  доступа: https://habr.com/ru/articles/764420/ свободный. Яз. рус. (дата обращения 11.06.2024).

3.    Sructured concurency в языке Go. [Электронный ресурс], Режим  доступа: https://habr.com/ru/hubs/parallel_programming/articles/ свободный. Яз. рус. (дата обращения 11.06.2024).

Комментарии (139)


  1. rsashka
    12.06.2024 12:24
    +6

    Простите, а в чем смысл этой статьи?


    1. lws0954 Автор
      12.06.2024 12:24

      Ну, так в описании самих "засад" потоков и даже, возможно, кода. Вы хоть прочитали-то статью?


      1. rsashka
        12.06.2024 12:24
        +11

        К сожалению, прочитал :-(


        1. lws0954 Автор
          12.06.2024 12:24

          Поздравляю, если у Вас подобных проблем не возникало. Или на Ваш взгляд засада с циклом - норм?


          1. rsashka
            12.06.2024 12:24
            +8

            Но кто мне объяснит, как можно допускать, чтобы какой-то код по-разному проявлял себя в разных режимах проектирования программы - отладки и выпуска?

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

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

            Других способов, (кроме тестов), я не знаю. Отпишитесь, если вы найдете какую либо альтернативу тестированию.


            1. wataru
              12.06.2024 12:24
              +2

              Есть всякие санитайзеры. tsan и valgrind, например, ловят некоторые проблемы многопоточности.

              Еще можно код размечать - все не локальные потоку переменные надо помечать, как например тут. И там уже статические анализаторы и сам компилятор заметит, если у вас какие-то проблемы есть.

              Конечно, это не спасает от всех ошибок, но позволяет многие из них отловить заранее.


              1. BugM
                12.06.2024 12:24
                +1

                Статический анализ кода сюда же. Не все, но что-то может найти.

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


                1. MiyuHogosha
                  12.06.2024 12:24
                  +1

                  Есть целая группа разработчикрв которая вообще не "верят" в такие проблемы, т.к. училь работать с компилятором или платформой, их прячущими (Майкросот очень умел в этом).

                  Такие вот слайс оф лайф я люблю кидать им в лицо, потому что один мой пример их не кчит - вместо этого у меня репутация шамана, от неверия которого код "ломается". В данном случая слайс хоррора, так как автор явно не совсем понимает что происходит.

                  Одно из замечательных фичей некоторых сборок компиляторов является то, что компилятор замечает неопределенное поведение на выходе из цикла. Неопределенного поведения не бывает, а значит цикл бесконечен. Заглядываешьв сгенерированный код и видишь откровенный jmp вместо jne или чего-то такого.

                  И я не шучу, это не какой-нибуть стартап а отделы разработки такого гиганта как "Алмаз-Антей".


                  1. wataru
                    12.06.2024 12:24

                    компилятор замечает неопределенное поведение на выходе из цикла. Неопределенного поведения не бывает, а значит цикл бесконечен. Заглядываешьв сгенерированный код и видишь откровенный jmp вместо jne или чего-то такого.

                    Я в другой ветке приводил ссылку. Там даже похожий код есть и объясняется, что происходит: https://mohitmv.github.io/blog/Shocking-Undefined-Behaviour-In-Action/


            1. petro_64
              12.06.2024 12:24
              +2

              Других способов, (кроме тестов), я не знаю. Отпишитесь, если вы найдете какую либо альтернативу тестированию.

              Попрбуйте Clang TSAN - отличная штука, как и Valgrind-helgrind (примерно тоже самое, только намного медленее - если не можете использовать clang). Они в рантайме отслеживают доступ к памяти и дают дельные отчеты - находят ошибки незащищенного доступа, потенциальные локи и т.п.

              Лучше всего прогонять такое в CI совместно с тестами, находят весьма заковыристые проблемы.


    1. AtariSMN82
      12.06.2024 12:24

      Чел не осилил многопоток и взорвался


  1. Sazonov
    12.06.2024 12:24
    +8

    А причём тут Qt? Он у вас в тэгах указан, а об особенностях многопоточности там ни слова.


    1. lws0954 Автор
      12.06.2024 12:24

      При том, что все написано на Qt. Класс потока qt-шный и соотвественно реализация потока qt-шная. А засаду с циклом кому адресовать, если не к Qt? Правда, может к компилятору? Ну, и вариант автора , конечно, не исключен. Где на Ваш опытный взгляд тут "собака порылась"?


      1. Sazonov
        12.06.2024 12:24
        +4

        Специально перечитал все листинги. Ничего про многопоточность в Qt там нет. И опережая ход ваших мыслей скажу что в Qt очень хорошая документация по многопоточности - начинать надо с неё, прежде чем изобретать велосипеды.

        Но если немного применить телепатию то можно предположить что вы отнаследовались от QThread и забыли про это написать и привести примеры. В любом случае для описанных примеров, в Qt есть более удобные инструменты.


  1. lws0954 Автор
    12.06.2024 12:24

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


    1. wataru
      12.06.2024 12:24
      +21

      Это проблемы многопоточности. Про которые написано в любой книге про многопоточное программирование. Которые вам стоит почитать. Или курсы какие-нибудь прослушать. Онлайн их очень много бесплатных. Вместо того, чтобы описывать ваше героическое сражение с ними и ваши "гениальные" их решения. Вот вам аналогия. Выхожу я такой и пишу статью:

      Представляете, если сложить 2 миллиарда и 1 миллиард, то программа выводит отрицательное число. Как такое может быть - сложили 2 положительных, а получили отрицательное?! Какие коварные эти большие числа. Я вот стал считать тысячи отдельно и единицы, вот все заработало! Пользуйтесь, кому надо. Правда, если складывать не 2 числа, а 1000, то все опять ломается, но мне это не надо, вроде работает. Вот, кстати, ссылка на мою предыдущую статью (процитированную в формате научной публикации! Ведь я Автор Статьи), где я реализовал деление на 1000 через вычитание в цикле.

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


      1. eao197
        12.06.2024 12:24
        +9

        Вы не знаете основ многопоточного программирования, а взялись писать о нем статью.

        Простите мне мой неприкрытый сарказм, но предыдущая статья автора называлась ни много, ни мало, а "Все секреты многопоточности"


        1. voldemar_d
          12.06.2024 12:24
          +2

          Лично мне не очень понятно, почему надо изучать потоки именно в qt.

          Для начала можно с std::thread поразбираться.


        1. wataru
          12.06.2024 12:24
          +2

          Ага, автор ее еще и в списке литературы указал. Фундаментальный труд, не иначе.


        1. AVaTar123
          12.06.2024 12:24

          БГГ


        1. MiyuHogosha
          12.06.2024 12:24

          Меня сейчас сильно подмывает начать писать статью в виде вредных советов "Что компилятор сотворил с моим кодом?" и показать, что получается "неправильный код" в в Compiler Explorer.

          Т.е. Мэтт Годболт, только наоборот


  1. wataru
    12.06.2024 12:24
    +11

    Например, при десяти потоках и числе циклов у каждого из потоков равном миллиону (см. также первоисточник рассматриваемой задачи [2]) вместо десяти миллионов выскакивал результат на несколько единиц меньше

    Это же в чистом виде race condition. Замените счетчик на std::atomic<int> и проблема исчезнет. Или введите критическую секцию через какие-нибудь мьютексы. Чтобы оно не влияло на производительность, пусть каждый поток считает локальный счетчик, а в конце, через критическую секцию, добавлет его к глобальному счетчику.

    Вообще, вы тут статьи про многопоточность пишите и даже их цитируете, как элктронные ресурсы, но для начала почитайте какую-нибудь книжку про многопоточное программирование на C++.

    Сделать это совсем просто. Для этого достаточно добавить одну строчку в начало кода потока, где он и будет ждать пока флаг не будет установлен

    Для этого придумали события. Писать такие ручные циклы - плохой стиль.

    Но кто мне объяснит, как можно допускать, чтобы какой-то код по-разному проявлял себя в разных режимах проектирования программы - отладки и выпуска

    Нельзя это допускать. Это обычно говорит об undefined behavior в программе. Состояние гонки, кстати - это UB. При их наличии программа может вести себя по разному даже при разных запусках, а не только при перекомпилировании в разных режимах. По возможности избегайте этого.


    1. voldemar_d
      12.06.2024 12:24
      +2

      Была вот такая статья, например:

      https://habr.com/ru/articles/443406/

      Для этого придумали события

      promise/future не подойдут? Или Вы про event в Windows?

      Состояние гонки, кстати - это UB

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

      int x = 5, y = 6;
      int z = x++ + y++; // не уточнено, будет вычислен первым x++ или y++

      Многопоточные программы надо писать так, чтобы они работали даже с учётом того, что неизвестно, с какой скоростью потоки запускаются, за какое время дойдёт исполнение до определённого места в процедуре потока и т.д. То есть, надо даже рассчитывать на некоторую неопределённость в исполнении и уметь подстраиваться под неё.


      1. wataru
        12.06.2024 12:24

        promise/future не подойдут? Или Вы про event в Windows?

        Ну да, виндовые события. Но аналогичные вещи есть, наверно, везде. В том же QT нагуглилось QTWaitCondition.

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

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


        1. voldemar_d
          12.06.2024 12:24

          Ну да, виндовые события. Но аналогичные вещи есть, наверно, везде.

          Хотелось бы опираться на что-нибудь из стандарта C++. Виндовые события - это и правда удобно: взвел событие, и на его ожидании сразу много потоков сработали.

           Вы же не можете предсказать, в каком порядке и сколько инструкций от каждого потока выполнет система? 

          Если так рассуждать, то вообще любое многопоточное приложение исполняется непредсказуемо, о чем я уже и написал.

          в зависимости от опций компиляции и фазы луны программа может вести себя совершенно по разному

          В этом проблема и заключается - при многопоточном программировании нужно иметь ввиду то, что программе нужно уметь стабильно работать вне зависимости от фазы луны, переключения между потоками, количества ядер процессора, скорости чтения/записи данных и т.д.


          1. wataru
            12.06.2024 12:24
            +1

            Если так рассуждать, то вообще любое многопоточное приложение исполняется непредсказуемо, о чем я уже и написал.

            Но если не допускать гонок, то этот порядок не влияет на результат.

            Edit:

            Хотелось бы опираться на что-нибудь из стандарта C++. Виндовые события

            std::condition_variable, оказывается, есть. Это ровно оно.


            1. voldemar_d
              12.06.2024 12:24

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


          1. wataru
            12.06.2024 12:24
            +2

            Если так рассуждать, то вообще любое многопоточное приложение исполняется непредсказуемо, о чем я уже и написал.

            И вообще, документация говорит:

            Some examples of undefined behavior are data races, memory accesses outside of array bounds, ...


      1. MiyuHogosha
        12.06.2024 12:24
        +1

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

        UB - это код, результат которого непредсказуем, поэтому компилятор вправе делать что угодно.


        1. wataru
          12.06.2024 12:24

          Я в соседней ветке уже привел: документация говорит:

          Some examples of undefined behavior are data races, memory accesses outside of array bounds, ...

          UB - это наличие в коде некоторых запрещенных конструкций. Из-за чего компилятор может генерировать машинный код, не эквивалентный вашей программе. Не обязательно из-за оптимизаций. Разрабочики гарантировали, что без UB компилятор отработает правильно. Что там с UB получится - они ответственности не несут.

          Так, например, из-за состояния гонки операция cnt++ может увеличить cnt на 0, если ее записать в машинном коде. Хотя код на C++ говорит, что увеличение всегда будет на 1.


  1. MasterMentor
    12.06.2024 12:24
    +7

    Люди правильно пишут. Ошибки детские. Прочтите это эталонное изложение (а читать там не много) и подобных ошибок у Вас не будет никогда. Проверено.

    2. Дж.Рихтер «Windows. Создание эффективных Win32-приложений с учётом специфики 64-разрядной версии Windows», 2008
    
    главы:
    
    6.  Базовые сведения о потоках
    7.  Планирование потоков, приоритет и привязка к процессорам
    8.  Синхронизация потоков в пользовательском режиме
    9.  Синхронизация потоков с использованием объектов ядра
    11. Пулы потоков


    1. voldemar_d
      12.06.2024 12:24

      Вот такой поиск несколько полезных ссылок выдает.


    1. playermet
      12.06.2024 12:24

      Еще есть потрясная книжка Anthony Williams - C++ Concurrency in Action Practical Multithreading, с вторым изданием от 2019 года. Покрывает от основ до относительно продвинутых нюансов многопоточностей.


      1. MiyuHogosha
        12.06.2024 12:24

        Только желательно с оригиналом сравнивать редактор или переводчик во втором издании местами накосячили. В терминах в основном, но где-то был пропуск еррат. Оригинал был доступен просто так на Manning


  1. Kotofay
    12.06.2024 12:24

    Если хотите играться с неатомарными глобальными флагами ожидания, то используйте:

    while ( !pFThCounter->bIfLoad )   
       QThread::currentThread()->sleep( 0 ); // "0" заставляет планировщик переключиться на другой поток

    И будет всё нормально работать.


  1. skovoroad
    12.06.2024 12:24
    +6

    Про проблемы многопоточности, изложенные в этой статье, можно почитать отличные труды Даннинга и Крюгера.


  1. lws0954 Автор
    12.06.2024 12:24

    Господа, стоило мне отвлечься на какое-то время, как меня тут смотрю помножили на ноль? Прям, печалька... А советов почитать - просто уйма. Но я не про почитать, а про конкретно - почему не работает код (Выпуск) во второй засаде...

    Вы все тут умные, ну, очень умные, начитавшиеся множества книжек и советы ваши полезные, но ... только скажу, что в основном совсем не в тему. Похоже, вам нужно (может быть, правда, не всем) начать с элементарных основ и вспомнить и, возможно, по новой усвоить, что такое алгоритм, забыв на мгновение про свои самые крутые языки (в прямом и переносном смысле) . Но это я так - брюзжу в силу возраста. Думаю, что подавляющее большинство из вас еще не родилось, когда я начал программировать. Так что опыт есть. Это так - для справки. Все это время только и занимался программированием и много-много читал. Понимаю, возраст - не аргумент. А с какого-то момента даже наоборот :) Но все же. Нужно заглядывать в профиль, смотреть на тематику статей, хотя бы бегло, чтобы все же понимать на кого "наезжаешь"...  ;)

    А теперь - по делу. Имеем элементарнейшую (еще раз - элементарнейшую!) параллельную задачу - узнать/угадать до чего она досчитает быстренько, ну, т.е. параллельно, изменяя одну общую переменную.  И, что тут скажешь, - полились "высокия материи" - гонки, прерывания, события, сообщения, UB и т.д. и т.п. Но если у меня ошибки "детские", то у кого-то они просто ясельного возраста, а может и меньше. Так что именно вам читать - не перечитать. Похоже, вы совсем не понимаете или не врубаетесь сходу в суть проблемы. Ведь, ваша задача - решить проблему, а не "умно породить" или "нагородить" все выше перечисленное - гонки, зависания... А потом бороться со свом же созданием, козыряя своими "скилами". 

    Дело в том, что проблема, которая, как говорится, и выеденного яйца не стоит, в переложении на потоки превращается "черти во что"! Это вы не понимаете? Если нет, то мне вас жаль. Для программиста главное - алгоритм. А на каком языке, какие библиотеки - второстепенно. Да вообще не важно даже. Но чем ближе при этом будет решение по форме к исходному алгоритму, тем больший вам респект. Это-то хотя бы понятно? Потоки искажают и извращают решение. Чтобы потом его загнать в какую-то приемлемую форму нужно, ой, как постараться! И именно этим вы гордитесь? Именно в ваши "сети" вы и хотите меня завлечь, советуя разную литературу и т.п.? Спасибо. Это уже давно пройденный этап.

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

    Итак. Хватит поучать. Если есть конкретные замечания по коду теста, по алгоритму - давайте. Проверю, скажу большое спасибо за совет, за науку... Если нет, то, как говорится "на нет и суда нет".  Код любой хорош, если он приводит к верному результату. Это замечание для особо "умных".

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


    1. skovoroad
      12.06.2024 12:24
      +10

      Безотносительно всех прочих достоинств вашего, безусловно, поучительного комментария, скажите: как вы умудряетесь в соседних абзацах писать "Хватит поучать" и "Подскажите в чем проблема?".


      1. lws0954 Автор
        12.06.2024 12:24

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


        1. MasterMentor
          12.06.2024 12:24
          +4

          Никто Вас не обнулял, и предыдущие статьи у Вас - хорошие. Просто люди думали, что здесь статья, но мы имеем просто вопрос из разряда: "мужики, не работает код, помогите, горит!" с настолько корявым "исследованием" проблемы, что да, от изложенных "методов" работы с потоками, говоря народным языком, "волосы дыбом встают".

          Если Вам нужно практическое и быстрое решение задачи "в лоб", то оно очень простое: полностью удалите весь нагороженный "огород", связанный с многопоточностью, кроме запуска потоков, заведите единственную критическую секцию и везде, где пересчитываете и меняете значение переменной пишите:

          // заводим переменную либо связанные переменные
          int i = 0;
          int j = 0;
          CriticalSection i_cs;
          
          i_cs->lock();
          
          // читаем
          // пресчитываем
          // записываем переменную
          
          i += 8;
          
          // или делаем с ней что хотим
          
          j = i - 0 + 0 + i*2;
          
          i_cs->unlock();

          То есть такие куски алгоритма дожны быть защищены критической секцией.

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

          Если где-то необходимо только читать значение любой из защищённых переменных, пишете так:

          int moment_i = 0;
          
          i_cs->lock();
          moment_i = i;
          i_cs->unlock();
          
          // используем значение moment_i, но использовать его для пересчёта и дальнейшей записи в i уже нельзя (i за время расчётов могла измениться)
          
          int d_blah_blah_blah = moment_i*moment_i;


          1. voldemar_d
            12.06.2024 12:24

            Да просто объявить переменную не как int i, а std::atomic_int i, и даже критических секций не нужно. Случай простой совсем.


            1. wataru
              12.06.2024 12:24
              +1

              Не совсем. Там помимо счетчика еще есть проблемы, вроде обновлений QLabel из всех потоков одновременно.


              1. voldemar_d
                12.06.2024 12:24
                +2

                Тогда критической секцией QLabel защитить.


                1. MasterMentor
                  12.06.2024 12:24

                  Нельзя. Представьте, сколько времени тратится на эту операцию.

                  Плюс уже было введено std::atomic_int (что вообще очень сомнительное решение, ибо на атОмиках - защиту группы переменных не построишь). Вот и пошёл расти "огород", который, поверьте, опять имеет в оконцовке "засаду".

                  PS О том и речь: без чтения литературы по основам многопоточного программирования - будет "засада" на "засаде".


                  1. voldemar_d
                    12.06.2024 12:24

                    Представить не могу, т.к. на QT не пишу.

                    ибо на атОмиках - защиту группы переменных не построишь

                    Знать бы постановку задачи целиком. Одну переменную защитить можно, а надо ли группу защищать в данном конкретном случае, я не понял.

                    который, поверьте, опять имеет в оконцовке "засаду"

                    Какую засаду и почему?

                    без чтения литературы по основам многопоточного программирования - будет "засада" на "засаде"

                    ИМХО, вряд ли можно создать литературный труд, чтение которого защитит от всех возможных подводных камней в многопоточном программировании. Например, по каким-то общим советам можно победить гонки и зависания, но получить проблемы с производительностью. А там уже и до корутин и многопоточности без блокировки недалеко.


                    1. MasterMentor
                      12.06.2024 12:24

                      Одну переменную защитить можно, а надо ли группу защищать в данном конкретном случае, я не понял.
                      Какую засаду и почему?

                      Две причины: 1) состояние объекта в 99,99% случаев не хранится в одной переменной, а атомики не позволяют блокировать сразу группу, 2) при пересчёте переменную необходимо блокировать на всё время её пересчёта, ведь другие потоки могут перезаписать её значение, поэтому такое использование атомиков даже если пересчитываемая переменная одна делает алгоритм заведомо ложным. Отсюда на атомиках конечного автомата не построишь. Они хороши в случае несвязанных переменных статистики, и даже при подсчёте ссылок на объёкт (с учётом того, что по достижении нуля ссылка больше никогда не увеличится, и объект должен быть уничтожен).

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

                      ИМХО, вряд ли можно создать литературный труд, чтение которого...

                      Так вот, чтобы не создавать и не читать "литературный труд", "литературный плод" или бесплодие, и было, как говорят в народе "не на дурку", и не ради "трёпа языком" предложено вот это. Ключевое выделю жиным:

                      (с) а читать там не много
                      https://habr.com/ru/articles/821283/comments/#comment_26926865

                      Подождём, когда в конец измученный "мы пойдём своим путём" автор вернётся к этому. А он вернётся. Без вариантов.


                      1. voldemar_d
                        12.06.2024 12:24

                         атомики не позволяют блокировать сразу группу

                        Я одного не понял: с чего Вы решили, что я где-то целую группу предлагаю атомиком блокировать.

                        В атомик можно, действительно, засунуть подсчет какой-то статистики или признак "надо завершить работу". Я вроде с этим нигде и не спорил.

                        И это не та хорошая "ненормальность", который подразумевает данный раздел

                        О каком разделе речь? О теме данной статьи?


                      1. MasterMentor
                        12.06.2024 12:24
                        +1

                        Так я не про Вас. :)

                        Я про то, что у человека принципиально неверный подход, и он на своих "шишках" хочет убедиться в этом.

                        Собственно, почему нет?! Каждый имеет на это право. :)

                        PS Так же, пользуясь случаем, разъясню, почему в комментариях мало кто оказывает квалифицированную помощь автору.

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


            1. lws0954 Автор
              12.06.2024 12:24

              Проверил. Работает. Причем в более чем в 8 раз быстрее.

              Чудны дела твои Господи ;)

              Кто еще что может предложить?


              1. wataru
                12.06.2024 12:24

                Считать в локальном счетчике. И только по завершению потока агрегировать его результат в общую переменную (тут уже не важно, с мьютексом или с атомиком).


                1. lws0954 Автор
                  12.06.2024 12:24

                  Это не то. Цель - проверить работу все же с общей переменной.

                  Кстати, QAtomicInt (см. рекламу ниже ;) тоже работает. На "соточку", правда, получается медленнее, чем std::atomic_int, но в целом тоже неплохо.

                  Так что это еще один вариант. Уже набралась коллекция :) Неплохо "засада" разрешается...


                  1. wataru
                    12.06.2024 12:24
                    +3

                    Цель - проверить работу все же с общей переменной.

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


                    1. alexdora
                      12.06.2024 12:24

                      А можно мне ответить на вопрос. Есть переменная, 10 потоков в эту переменную без мютексов начинают писать свое. Это на C++ вызывает панику или нет? Просто на golang – 100% паника. Я ради праздного любопытства для себя сравниваю C++ и Golang


                      1. Kotofay
                        12.06.2024 12:24
                        +1

                        С++ абсолютно параллельно что вы делаете со своей переменной, хоть 100500 потоков пусть её пишут читают.


                      1. alexdora
                        12.06.2024 12:24

                        Интересный нюанс, т.к получается теорию надо знать очень хорошо и понимать структуру. Если в Golang при попытке сделать данное деяние ты получишь панику с описанием что попытка перезаписать то что перезаписывается другим потоком. Но в C++ получается ты должен это сам изначально понимать и если опыта нет, то долго потом будешь как дурак искать ошибку.

                        Спасибо


                      1. wataru
                        12.06.2024 12:24

                        Но в C++ получается ты должен это сам изначально понимать и если опыта нет, то долго потом будешь как дурак искать ошибку.

                        C++, к сожаллению, полон таких вещей. При той же работе с памятью можно запросто себе в ногу выстрелить.


                      1. voldemar_d
                        12.06.2024 12:24

                        Выше уже давал ссылку на статью, где описаны штук 20 способов выстрелить себе в ногу при работе с потоками.


                      1. Kotofay
                        12.06.2024 12:24

                        Но в C++ получается ты должен это сам изначально понимать

                        С++ произошёл от С, а он от ассемблера не далеко ушёл. Поэтому если нет желания вникать во что превращается код на С/С++, то лучше писать на языках которые скрывают от разработчика любые отсылки к памяти как к ячейкам а не как к объектам, и к процессору как исполнителю конструкций языка а не машинного кода.

                        То что С++ сам что то делает, иногда без указания разработчика, например вызывает деструктор, то это можно считать допустимой помощью, но всегда надо при этом помнить во что может вылиться этот деструктор. Или обращение к памяти через разыменование указателя. Какой нибудь *(unsigned short)(0xB800) = 0x0CAB не должен приводить к нервному тику.


                      1. lws0954 Автор
                        12.06.2024 12:24

                        Хороший язык хорошо все скрывает, чтобы не было необходимости копаться в "потрохах" У меня был проект под Linux, когда я работал на разных машинах, Работа велась на windows, а один и тот же код работал и там и там. Чуть повозился с настройками среды, а потом все работало как часы. И все это благодаря кроссплатформенности Qt. Вот тогда я ее и одновременно C++ и заценил, уйдя с VS ;)


                      1. Kotofay
                        12.06.2024 12:24

                        Непонятно, почему вы считаете его нехорошим, это же переносимый ассемблер. Мне как начинавшему с машинных кодов PDP-11 и MACRO-II он очень понятен и предсказуем. Требовать от него поведения Java не стоит.
                        C Qt не всё так радужно под разными платформами, хотя после 20 лет писанины на Qt, можно сказать спасибо что он есть, но сейчас я предпочитаю Java.


                      1. ilia_reist
                        12.06.2024 12:24

                        А почему в Go будет паника? Мы же без '-race' параметра вызываем. Просто будет непонятно что в итоге записано


                      1. alexdora
                        12.06.2024 12:24

                        Вы попробуйте перезаписывать переменную и увидите панику как только будет момент когда 2 потока попытаются перезаписать её в один условный момент


                    1. voldemar_d
                      12.06.2024 12:24

                      Работа с atomic_int в C++ почему не атомарна? Это же не большая структура какая-нибудь.

                      Или Вы про просто int?


                      1. wataru
                        12.06.2024 12:24

                        Да, я про просто int и вообще про встроенные типы. Именно поэтому есть аж специальный atomic<int>.



                    1. lws0954 Автор
                      12.06.2024 12:24

                      Да конечно. Я, конечно, слушаю, но только у меня есть своя голова и какой ни есть, но опыт... В том числе и слушать других;)

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

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

                      Пока буду слушать и экспериментировать и делиться результатами этого процесса ;). В этот раз на удивление было интересно. Главное "засада" была пройдена. Пусть по началу шло туговато, но потом "разогрелись". За это огромное всем. Даже тем, кто "уперто" минусует. Но это они так завидуют ;)


                      1. alexdora
                        12.06.2024 12:24
                        +1

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

                        Можно буду душным. Какие такие ситуации когда надо сделать намеренно гонку данных. Я сидел думал и не придумал.

                        Получается программа рандомайзер...


              1. voldemar_d
                12.06.2024 12:24

                Вся программа в 8 раз быстрее заработала? Или что?


                1. lws0954 Автор
                  12.06.2024 12:24

                  Да, конечно. Здесь вся программа - это, ведь, просьто работа со счетчиком. Но ускорилась, конечно, потому, что не нужна стала работа с мютексом.


          1. lws0954 Автор
            12.06.2024 12:24

            Спасибо за совет, но пока подобной проблемы нет.


    1. wataru
      12.06.2024 12:24
      +10

      Так что опыт есть

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

      Имеем элементарнейшую (еще раз - элементарнейшую!) параллельную задачу - ... параллельно, изменяя одну общую переменную

      Ну вот вы заблуждаетесь. Это не элементарно. Вы про машинные инструкции, ассемблер, как работает процессор, с высоты вашего великого опыта - навреняка знаете отлично же, да? Ну вот один поток, чтобы увеличить переменную, загружает в регистр значение (X) из памяти. Увеличивает его и записывает назад X+1. Но! До этой записи второй поток может успеть тоже прочитать это же значение X в свой регистр. Увеличить его и потом переписать значение в памяти на X+1. В итоге оба потока сделали свои +1, но переменная изменилась с X на X+1. Вот откуда у вас первая засада.

      Но если у меня ошибки "детские",

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

      И, что тут скажешь, - полились "высокия материи" - ... UB

      Вы пишите на C++ и UB для вас - это высокая материя? Серьезно? Это основа основ, это как аквалангистам никогда не задерживать дыхание, как строителям каску носить. Вы, видимо, теоретик. Ограничтесь псевдокодом и не трогайте C++ пока.

      Для программиста главное - алгоритм. А на каком языке, какие библиотеки - второстепенно.

      Это, да, но надо уметь алгоритм реализовать на языке. Если вы криво используете язык, то даже ваш отличный алгоритм не заработает. Например, если у вас в алгоритме фигурируют квадрилионы, а вы в программе будете использовать переменные int32_t - то эта реализация не заработает, потому что туда больше 2147483647 не влезает.

      Конкретно в вашем случае: в описании алгоритма все операции выглядят атомарными. Ну есть поток. Если он делает "увеличить counter на 1", то он его на 1 и увеличит, что бы там про другие потоки не написано. Но на языке с++ - это не так, я вам выше уже описал, почему. В языке python, с его Global Interpreter Lock - это уже ближе к правде. В языке Rust гонка данных вообще невозможна, но иногда вам придется ваш алгоритм с ног на голову переворачивать, что бы его на rust написать вообще можно было.

      Я, например, абсолютно не понимаю поведение потока/потоков  во второй засаде.

      Вы же такой опытный! Мы же все сосунки тут по сравнению с вами. Как же так вам что-то может быть непонятно вообще? И как в этом случае нам, несчастным, что-то тут помыслить? Вам надо в академию наук писать какую-нибудь, а не в наш детский сад. (конец сарказма)

      Какая засада у вас там вторая по счету? Виснущие потоки? Где можно посмотреть весь код, а не отдельные его куски без контекста?


      1. lws0954 Автор
        12.06.2024 12:24

        Так что это не аргумент. Опыта параллельного программирования у вас, очевидно, нет.

        Вы не поверите, но есть ;) Просто этот опыт ни как не связан с потоками. Здесь просто один "тонкий" момент. Программирование с потоками ,строго говоря, не является параллельным. Автоматное программирование - это параллельное программирование.

        Странно, не правда ли? Но формально так оно и есть. Ну, я на это тему много уже написал ;) Скажете - "контора пишет"? Мне можете не верить, но математике не верить нельзя... В ней все базируется на формальном доказательстве. Это, чтобы все было объективно, а не наоборот.

        Докажете формально, что потоки параллельная модель - даже извинюсь ;) Нет - Ваш ход...


        1. wataru
          12.06.2024 12:24
          +1

          Докажете формально, что потоки параллельная модель - даже извинюсь ;

          Файспалм. Потоки не параллельно работают? Ну, может на каком-нибудь хламе 20-ти летнем. Если у вас процессору меньше 18 лет, то там хотя бы 2 ядра-то есть.

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

          либо же вычислительные процессы могут представлять собой набор потоков выполнения внутри одного процесса ОС. Параллельные программы могут физически исполняться либо последовательно на единственном процессоре — перемежая по очереди шаги выполнения каждого вычислительного процесса, либо параллельно

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


          1. lws0954 Автор
            12.06.2024 12:24

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

            Спасибо за уточнее... Теперь примерно то ;)

            Да я с "гонок данных" начал свою работу (начиная с задачи про RS-триггер) в параллельном программировании. См. например мою статью про Машу. В моделировании схем гонок выше крыши. И надо, чтобы это все работало. И, ведь, работает! Главное, - хорошо. На потоках такое написать - дуба дашь! ;)

            Ну, а Ваша цитата - это не определение формальной модели параллельных вычислений. Это всего лишь - структурная организация неких процессов. Но это уже надо в теорию копать... "перемежая" ;)


          1. Bender_Rodrigez
            12.06.2024 12:24

            Да ну, ерунда, и не доказательство вовсе.

            Где формулы, теоремы, леммы, вероятности, где математика?

            Мне можете не верить, но математике не верить нельзя...

            Предоставьте формальное доказательство, что в эйфории от всего этого у вас не привстал. Нет? Ну вот и все.


            1. lws0954 Автор
              12.06.2024 12:24

              Предоставьте формальное доказательство, что в эйфории от всего этого у вас не привстал. Нет? Ну вот и все.

              C Вами все понятно. Вам не до теории. Просто Вы другим озабочены... ;)


          1. MiyuHogosha
            12.06.2024 12:24
            +3

            На 286 :) Это скорее 30 лет Вот и я с такими как он работаю.. "Не бывает одновременного доступа", "достал ты со своим UB"

            Виснущие потоки могут быть результатом создания бесконечного цикла в следстие UB


        1. Kotofay
          12.06.2024 12:24

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


          1. wataru
            12.06.2024 12:24
            +2

            Да даже с одним процессором они будут выполнятся конкурнетно, а не "сначала весь один, потом весь другой". И все те же проблемы с гонками данных и дедлоками остаются точно такие же.


            1. alexdora
              12.06.2024 12:24
              +1

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


            1. Kotofay
              12.06.2024 12:24

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

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


        1. alexdora
          12.06.2024 12:24

          Это намек на то что процессор выполняет все инструкции по очереди? или я неправильно понял? Если да, то это слишком душно.

          Безусловно ядро со всеми сопроцессорами (математики и тд) работает линейно, но там скорости такие что либо называть это магией, либо все таки многопоточным исполнением.


          1. wataru
            12.06.2024 12:24
            +1

            Это намек на то что процессор выполняет все инструкции по очереди? или я неправильно понял? Если да, то это слишком душно.

            Более того, даже этот намек - наивное заблуждение.

            Это было правдой пару десятилетий назад. Сейчас, на процессорах с глубоким пайплайном и микрооперациями, даже одно ядро выполняет кучу операций параллельно. Пока одна читает данные из кеша, другая гоняет свои на АЛУ, а третья декодируется, например. Иначе как год от года растет количество операций на такт?


            1. alexdora
              12.06.2024 12:24

              Кстати, очень хороший пример это игры. Игровые процессоры имеют большую частоту на ядро, серверные имеют порой в 10-ки раз больше ядер чем в игровых, но меньшую частоту. Если пойти по математике, то серверный проц ставит на лопатки при формуле [частота] x [кол-во ядер]

              Но, когда ты запускаешь игру оказывается что игры заточены на маленькое количество ядер и зачастую там 2-4 CPU используется и это потолок (думаю это связано с тем самым параллельным вычислением и сложностью синхронизации). И при всем при этом параллельности как бэ нет (по мнению автора), но в игре происходит гиганское количество событий(вычислений) одновременно, хотя по логике их должно быть не более чем [количество ядер] в использовании. Магия видимо


              1. lws0954 Автор
                12.06.2024 12:24

                Наверное, лет десять назад написал тест - жуки (есть такая задачка в имитационном моделировании). Здесь 7 жуков гоняются друг за другом. Соединены между собой линиями, которое динамически меняют цвет. Так получился некий "скринсайвер". Можно разгонять до 300 жуков. Каждый жук - автомат. Они взаимодействуют дгуг, с другом, жрут, если догоняют другого, рождаются новые жуки и в результате создается такая картинка.


                1. voldemar_d
                  12.06.2024 12:24
                  +1

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

                  Но если Вы сделали многопоточное приложение, процедуры разных потоков на современном процессоре (с вероятностью почти 100% - любом) будут выполняться реально одновременно. Вот в таких случаях и возникают проблемы с одновременным доступом к данным из нескольких реально параллельных процессов, выполняющихся одновременно.


    1. Tyiler
      12.06.2024 12:24

      .


    1. Bender_Rodrigez
      12.06.2024 12:24

      OK. А хотели-то чего?


    1. Kotofay
      12.06.2024 12:24
      +2

      почему не работает мой код в первом варианте (с добавленной одной строчкой цикла)

      Потому что внутри цикла нет обращения к ОС, и планировщик отдаёт потоку максимум кванта времени, это глухой цикл, и он нагрузит ядро процессора на максимум. Остальным потокам в этом процессе будет тяжко получить свой квант.
      А вот внутри большого цикла есть и работа со строками, а это в Qt однозначное выделение памяти системным вызовом, так и usleep, который точно положит поток в очередь ожидания. Поэтому этот вариант работает а первый нет.
      Я уже написал, как избавляться от глухого цикла ожидания флага в потоке.
      Но ещё лучше освоить:

      это не фотошоп
      это не фотошоп


  1. Bender_Rodrigez
    12.06.2024 12:24
    +9

    мой опыт  и я

    Но одно  эти

    Ошибки, правда,  были

    реанимирован  аналогичный

    после его изменений  сделать

    со счетчиком.   Но как это

    чем  ошибки

    когда  искусственный

    Который пост подряд читаю и не пойму никак - это тренд в IT какой-то новый, двойные пробелы вставлять?

    Надо наматывать на ус, а то отстану от темы и не хайпану среди молодежи...

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

    Атец, вполне возможно, что когда нас еще не родилось, а вы начинали программировать, и потоков-то не было.

    Если вы перфокарты помните и в руках их держали, мое вам почтение, но с потоками вам, может, не надо, а?


    1. dyadyaSerezha
      12.06.2024 12:24
      +3

      Да и "крайняя статья" как-то ударила по глазам. Кто так говорит?


      1. voldemar_d
        12.06.2024 12:24

        Парашютисты ;)


        1. dyadyaSerezha
          12.06.2024 12:24

          Про свои статьи?)


          1. voldemar_d
            12.06.2024 12:24

            Про всё!


          1. MiyuHogosha
            12.06.2024 12:24

            У них есть выражение крайний прыжок, чтобы не говорить последний


  1. lws0954 Автор
    12.06.2024 12:24

    main.cpp
    #include "mainwindow.h"
    
    #include <QApplication>
    
    int main(int argc, char *argv[])
    {
        QApplication a(argc, argv);
        MainWindow w;
        w.show();
        return a.exec();
    }
    

    mainwindow
    #ifndef MAINWINDOW_H
    #define MAINWINDOW_H
    
    #include <QMainWindow>
    #include "SetVarThread.h"
    #include <QElapsedTimer>
    #include <QMutex>
    #include <QSemaphore>
    
    QT_BEGIN_NAMESPACE
    namespace Ui { class MainWindow; }
    QT_END_NAMESPACE
    
    class MainWindow : public QMainWindow
    {
        Q_OBJECT
    
    public:
        void Process();
        void Reset();
    
        void AddCounter();
        void AddCounterMx();
        void AddCounterSem();
        void DecrementActive();
    
        MainWindow(QWidget *parent = nullptr);
        ~MainWindow();
    
        int pVarNumberOfThreads{10};
        long nMaxValue{100000};   // максим. значение общего счетчика
        bool pIfSemaphoreLotThreads{false};
        bool pIfMutexLotThreads{true};
        QString pVarStrSleepLotThreads;
        QMutex  m_mutex;
        QSemaphore  m_semaphore;
    
        CSetVarThread   *pCSetVarThread;
        int pVarTimeLotThreads{0};
        long pVarExtrCounter{0};
        void ViewConter(int n);
        void ViewTime();
        QElapsedTimer timeLotThreads;
        bool bIfLoad{false};
        int nIdTimer{0};
    
        virtual void timerEvent(QTimerEvent*);
    private slots:
        void on_pushButtonReset_clicked();
    
        void on_checkBoxMutex_clicked();
    
        void on_checkBoxSemaphore_clicked();
    
        void on_lineEditMaxValue_editingFinished();
    
        void on_lineEditNumberOfThreads_editingFinished();
    
        void on_lineEditSleep_editingFinished();
    
    private:
        Ui::MainWindow *ui;
        friend class ThCounter;
    };
    #endif // MAINWINDOW_H
    
    #include "ThCounter.h"
    #include <cmath>
    #include <stdio.h>
    
    MainWindow::MainWindow(QWidget *parent)
        : QMainWindow(parent)
        , ui(new Ui::MainWindow)
    {
        ui->setupUi(this);
        nMaxValue = 100000;   // максим. значение общего счетчика
        pVarNumberOfThreads = 10;
        ui->checkBoxMutex->setChecked(pIfMutexLotThreads);
        ui->checkBoxSemaphore->setChecked(pIfSemaphoreLotThreads);
        ui->lineEditSleep->setText("");
        nIdTimer = startTimer(10);
        Process();
    }
    
    MainWindow::~MainWindow()
    {
        delete ui;
    }
    
    void MainWindow::ViewConter(int n) {
        string str = QString::number(n).toStdString();
        ui->lineEditCounters->setText(str.c_str());
    }
    
    void MainWindow::ViewTime() {
        string strTime = QString::number(timeLotThreads.elapsed()).toStdString();
        ui->lineEditTime->setText(strTime.c_str());
    }
    
    void MainWindow::Process() {
        bIfLoad = false;
        ui->lineEditMaxValue->setText(QString::number(nMaxValue).toStdString().c_str());
        ui->lineEditNumberOfThreads->setText(QString::number(pVarNumberOfThreads).toStdString().c_str());
        pCSetVarThread = new CSetVarThread();
        int i;
        for (i=0; i<pVarNumberOfThreads; i++) {
            CVarThread var;
            var.pQThread = new ThCounter(&nMaxValue, this);
            string str = QString::number(i).toStdString();
            var.strName = str;
            pCSetVarThread->Add(var);
        }
    
        timeLotThreads.start();
        pVarExtrCounter = 0;
        TIteratorCVarThread next= pCSetVarThread->pArray->begin();
        while (next!=pCSetVarThread->pArray->end()) {
             CVarThread var= *next;
             var.pQThread->start(QThread::Priority(0));
             pCSetVarThread->nActive++;
             next++;
        }
    //    pVarExtrCounter = 0;
        bIfLoad = true;
        ui->lineEditTime->setText("");
        ui->lineEditCounters->setText("");
    }
    
    void MainWindow::Reset()
    {
        pVarExtrCounter = 0;
        bIfLoad = false;
        // удалить потоки
        TIteratorCVarThread next= pCSetVarThread->pArray->begin();
        while (next!=pCSetVarThread->pArray->end()) {
            delete next->pQThread;
            next++;
        }
        if (pCSetVarThread) {
            pCSetVarThread->pArray->clear();
            delete pCSetVarThread;
        }
        pCSetVarThread = nullptr;
    
        ui->lineEditTime->setText("");
        ui->lineEditCounters->setText("");
    
    }
    
    void MainWindow::on_pushButtonReset_clicked()
    {
        Reset();
        Process();
    }
    QMutex  g_mutex;
    void MainWindow::DecrementActive() {
        g_mutex.lock();
        pCSetVarThread->nActive--;
        g_mutex.unlock();
        if (!pCSetVarThread->nActive) {
            ViewConter(pVarExtrCounter);
            ViewTime();
        }
    }
    
    void MainWindow::AddCounter() { pVarExtrCounter++; }
    
    void MainWindow::AddCounterMx() {
            m_mutex.lock();
            pVarExtrCounter++;
            m_mutex.unlock();
    }
    
    void MainWindow::on_checkBoxMutex_clicked()
    {
        pIfMutexLotThreads = ui->checkBoxMutex->isChecked();
    }
    
    void MainWindow::on_checkBoxSemaphore_clicked()
    {
        pIfSemaphoreLotThreads = ui->checkBoxSemaphore->isChecked();
    }
    
    void MainWindow::on_lineEditMaxValue_editingFinished()
    {
        QString str = ui->lineEditMaxValue->text();
        nMaxValue = str.toInt();
    }
    
    void MainWindow::on_lineEditNumberOfThreads_editingFinished()
    {
        QString str = ui->lineEditNumberOfThreads->text();
        pVarNumberOfThreads = str.toInt();
    }
    
    void MainWindow::timerEvent(QTimerEvent* t) {
        Q_UNUSED(t)
        if (t->timerId() != nIdTimer)
            return QObject::timerEvent(t);
        string strExtrCounter = QString::number(pVarExtrCounter).toStdString();
        ui->lineEditCounters->setText(strExtrCounter.c_str());
        if (pCSetVarThread->nActive != 0) {
            string strTime = QString::number(timeLotThreads.elapsed()).toStdString();
            ui->lineEditTime->setText(strTime.c_str());
        }
    }
    
    void MainWindow::on_lineEditSleep_editingFinished()
    {
        pVarStrSleepLotThreads = ui->lineEditSleep->text();
    }
    

    CSetVarThread
    #ifndef CSETVARTHREAD_H
    #define CSETVARTHREAD_H
    #include "VarThread.h"
    
    class CSetVarThread
    {
    public:
        CSetVarThread();
        virtual ~CSetVarThread();
        bool Add(CVarThread &var);
        TArrayCVarThread *pArray;
        int nActive{0};
    };
    
    #endif // CSETVARTHREAD_H
    #include "SetVarThread.h"
    
    CSetVarThread::CSetVarThread()
    {
        pArray = new TArrayCVarThread();
    }
    
    CSetVarThread::~CSetVarThread()
    {
        if (pArray)
            if (pArray->size() != 0)
                pArray->erase(pArray->begin(), pArray->end());
        delete pArray;
    }
    
    bool CSetVarThread::Add(CVarThread &var) {
        TIteratorCVarThread next=find(pArray->begin(), pArray->end(), var);
        if (next==pArray->end()) {
            pArray->push_back(var);
            return true;
            }
        else {
                return true;
        }
    }
    

    Код потока.

    ThCounter
    #ifndef THCOUNTER_H
    #define THCOUNTER_H
    #include <QThread>
    #include <QTimer>
    
    class MainWindow;
    class ThCounter: public QThread
    {
    public:
        explicit ThCounter(long *pIntMax, MainWindow *parent = nullptr);
        virtual ~ThCounter();
    
    private:
        void run();
        int nMaxValue{0};
    
        bool bIfRun{false}; // флаг работы потока
        MainWindow *pFThCounter{nullptr};
    };
    
    #endif // THCOUNTER_H
    
    #include "mainwindow.h"
    #include "ui_mainwindow.h"
    #include "ThCounter.h"
    
    ThCounter::ThCounter(long *pIntMax, MainWindow *parent):
        QThread()
    {
        pFThCounter = parent;
        nMaxValue = *pIntMax;
        bIfRun = true;
    }
    
    ThCounter::~ThCounter(void) {
        bIfRun = false;
        quit();         // корректный способ завершения потока
        wait(3000);         // ожидание завершени потока (неограниченное ожидание wait())
                        // подробнее см. Боровский А. Qt4.7... стр.170
    }
    
    void ThCounter::run() {
        while (!pFThCounter->bIfLoad) {
    //        if (!bIfRun) break;
        }
        int n=0;
        while (n<nMaxValue && bIfRun ) {
    //        if (pFThCounter->bIfLoad) {
                bool bMx = pFThCounter->pIfMutexLotThreads;
                if (bMx) {
                    pFThCounter->AddCounterMx();
                }
                else pFThCounter->AddCounter();
    
                QString qstr = pFThCounter->pVarStrSleepLotThreads;
                if (qstr.length()>0) {
                    int nSleep = qstr.toInt();
                    usleep(nSleep);
                }
                n++;
    //        }
        }
        pFThCounter->DecrementActive();
    }
    

    CVarThread
    #ifndef CVARTHREAD_H
    #define CVARTHREAD_H
    #include <list>
    using namespace std;
    #include <QThread>
    
    class CVarThread
    {
    public:
        CVarThread(string &nam);
        CVarThread(void);
        CVarThread(const CVarThread& var);
    
        QThread *pQThread{nullptr};
    
        string		strName{""};			//
    
        bool operator==(const CVarThread &var) const;
        bool operator<(const CVarThread &var) const;
        bool operator!=(const CVarThread &var) const;
        bool operator>(const CVarThread &var) const;
        CVarThread& operator=(const CVarThread& var);
    
    
        bool    IfEqu(CVarThread& var);
    };
    typedef list<CVarThread> TArrayCVarThread;
    typedef list<CVarThread>::iterator TIteratorCVarThread;
    
    #endif // CVARTHREAD_H
    
    #include "VarThread.h"
    
    CVarThread::CVarThread(string &nam)
    {
        strName = nam;
    
    }
    
    CVarThread::CVarThread(void)
    {
        strName = "";
    }
    
    CVarThread::CVarThread(const CVarThread& var)
    {
        strName	= var.strName;
        pQThread = var.pQThread;
    }
    
    bool CVarThread::IfEqu(CVarThread& var)
    {
        if (strName != var.strName) return false;
        return true;
    }
    
    bool CVarThread::operator==(const CVarThread &var) const
    {
        if (strName==var.strName) return true;
        else return false;
    }
    
    bool CVarThread::operator<(const CVarThread &var) const
    { return strName<var.strName; }
    
    bool CVarThread::operator!=(const CVarThread &var) const
    { return strName!=var.strName; }
    
    bool CVarThread::operator>(const CVarThread &var) const
    { return strName>var.strName; }
    
    


    1. wataru
      12.06.2024 12:24
      +3

      Если хотите, могу много придирок по стилю и хорошему тону кода выдать. Код очень плохо читаем и поддерживаем. Опыт у вас явно не промышленной разработки. Но, наверняка, не хотите. Оставлю при себе, ведь вы критику плохо переносите, судя по всему.

      Что касается параллельного программирования:

      При pIfMutexLotThreads == false , MainWindow::AddCounter как раз содержит гонку данных. Вы, видимо, это заметили, поэтому ввели MainWindow::AddCounterMx

      Но в MainWindow::ViewConter вы этого не заметили. Вы там вызываете QLabel::SetText (скорее всего, QLabel, но в приведенном коде тип lineEditCounters не приведен). ViewConter вызывается из MainWindow::DecrementActive , который вызывается из каждого потока (Тут, кстати, опечатка: должно быть ViewCounter). Но SetText, как и многие другие методы в QT - не thread safe. Это может привести к тому, что у вас в текстовом поле останется ненулевое число, даже если все потоки завершатся. Вам покажется, что программа повисла. Это одно из объяснений вашей засады. Или у вас там что-то другое? Как вы вообще определяете, что потоки виснут?

      Правильный подход - это вызывать SetText через InvokeMethod, тогда проблем не будет.


      1. MasterMentor
        12.06.2024 12:24
        +3

        Это может привести к тому...

        Судя по коду там всё может привести ко всему. Например, я увидел строку запуска потоков ( start() ) , но не увидел ожидание их завершения (обычно join() ), поэтому, повангую, дальнейшим вопросом будет: "а почему программа падает при закрытии?".

        Следующей "засадой" при распараллеливании алгоритма обязательно окажется, что правильным решением будет использование очередей, а не только голых объектов синхронизации (что обычно и делается в 90+% многопоточного ПО).

        Отсюда глубокое убеждение, что "с наскока", без чтения соответствующей литературы ничего дельного не выйдет. Просто сначала время будет потрачено на бесполезные попытки сделать алгоритм методом далеко не "научного тыка", а затем - на прохождение курса по теме.


        1. lws0954 Автор
          12.06.2024 12:24

          Судя по коду там всё может привести ко всему. Например, я увидел строку запуска потоков ( start() ) , но не увидел ожидание их завершения (обычно join() ), поэтому, повангую, дальнейшим вопросом будет: "а почему программа падает при закрытии?".

          В этом нет нужды. Поток, завершив работу, делает Decrement()и завершает работу, завершив работу метода run().Так работает поток в qt. Программа падает при закрытии, если поток виснет. Тогда в деструкторе метод wait(3000) при превышении заданного периода ожидания крашит программу.

          Отсюда глубокое убеждение, что "с наскока", без чтения соответствующей литературы ничего дельного не выйдет.

          Ну, если в таком простом случае нужно что-то "копать", чтобы непонятно что "выкопать", то мне будет явно проще создать десять тех же параллельных автоматов. и с подобной задачей они справятся четко и надежно. Вот по времени - не буду пока загадывать. Но попробую :)


          1. voldemar_d
            12.06.2024 12:24

            Если они будут при этом исполняться на одном ядре процессора, а все остальные ядра будут простаивать - Вас это устроит? Если да, то зачем вообще многопоточная программа?


          1. MasterMentor
            12.06.2024 12:24
            +1

            ...В этом нет нужды. Поток, завершив работу, делает Decrement

            Вы строите антипаттерн на антипаттерне. То что Вы предлагаете запрещено (и разобрано во всех учебниках в азах почему).

            Далее:

            Я пишу в рамках ВКПа. а там иной стиль и иные требования. Здесь же я подстраивал код под ВКПа-шный, делая его как можно ближе к нему.
            Еще раз, подобный стиль мне почти чюжд ;) Я за автоматный.

            Мало того, то как Вы пишете, это не просто уход от концепции конечных автоматов, это антиавтоматное программирование. В многопотоке автоматы строят над состояниями очередей (связанных списков, на которые очень хорошо ложатся графы). (Нельзя построить многопоточный КА иначе). И в классике детально разобрано почему так. Это азы.

            PS Чтобы пояснить насколько фатальны Ваши решения по многопотоку, скажу, что если подобные решения даёт на собеседовании соискатель должности инженер-программист на С/С++, то собеседование прекращается в ту же минуту. В любой компании и при любой погоде. Примите к сведению и это.


            1. lws0954 Автор
              12.06.2024 12:24

              Вы строите антипаттерн на антипаттерне. То что Вы предлагаете запрещено (и разобрано во всех учебниках в азах почему).

              Что я запрещенного сделал? Причем тут какие-то паттерны? О каких азах речь? Такое впечатление - просто набол умных слов и букв. Поясню. Поток представлен функцией run(). Когда она завершит работу - завершит работу поток. Вот и все азы по потоку в Qt. Или я что- упустил?

              Далее. Функция gjnjrf содержит простой цикл, вызывающий другую функцию, наращивающую внешний счетчик. По большому счету потоку до этой функции дела нет. Он ее просто вызывает и дожидается возвращения eghfdktybz. Что здесь такого сложного, необычног, заумного. Скажет так, на уровне потока.

              Далее. Цикл отрабатывает заданное число итераций и завершаю свою работу. В завершение вызывает еще функцию, чтобы обозначить конец своей работы. Что делает эта функция потоку тоже по барабану. Функция отработала - произошло завершение и функции потока, Все - ему кирдык! Что тут заумного из-за чего нужно искать умного "ментора", ворошить какие-то учебники, чтобы они разъяснил, что и да как?

              Про автоматы, прошу прощения, - совсем бред. Поясните чуть понятнее без тумана. Что мне необходимо принять "к сведению"? Что за зверь такой - "многопоточный автомат"? Давайте конкретнее... .


              1. Tyiler
                12.06.2024 12:24

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

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

                Я сначала, выше написал, что сопляк какой-то тут мозги пудрит, прикидвается умудренным, а сам напздил где-то материал и скопипастил тупо. А потом почитал еще ваши статьи, и комменты..

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


      1. lws0954 Автор
        12.06.2024 12:24

        ... ведь вы критику плохо переносите, судя по всему.

        конструктивную очень даже приветствую ;)

        При pIfMutexLotThreads == false , MainWindow::AddCounter как раз содержит гонку данных. Вы, видимо, это заметили, поэтому ввели MainWindow::AddCounterMx

        Да нет. Все это, как задумано. Есть два режима рвботы теста. Один без использования объектов синхронизации, другой использует мютекс. Только и всего.

        ... Это одно из объяснений вашей засады. Или у вас там что-то другое?

        Висит, как я написал в статье, в теле добавленного цикла. Для меня именно это нонсенс. В остальном, возможно, Вы даже правы. Но это не столь принципиально, т.к. не это главное в тесте. Может, стоило мютек переместить за выводом. Я даже об этом думал, но решил не париться. Пусть выводится, что выводится ;) Да, кстати, я тут немного перемудрил. Только сейчас заметил. Вывод счетчика и таймера из метода Decrement надо совсем исключить (осталось от предыдущей верси теста). Ведь, выводом, как задумано, занимается таймер (см. сообщение таймера).

        Как вы вообще определяете, что потоки виснут?

        Ну, как. Значение счетчика зависло на каком-то значении и не изменяется длительное время. Как это еще оценивать, как не висяк? Не ждать же час, когда работы на 3 сек (в отладчике, где работает все)

        Правильный подход - это вызывать SetText через InvokeMethod, тогда проблем не будет.

        Проблему создает не вывод в виджет, а именно добавленный цикл. Это абсолютно точно. Перетащил анализ флага во внутрь цикла потока и все заработало, как часы. Хоть в отладчике хоть вне. Я чувствую, что Вы не очень что ли внимательно читаете, в чем я призываю разобраться. Ведь, есть код с добавленным циклом сразу в начале метода run, который под отладчиком работает, а без отладочного кода, т.е. в режиме Выпуск - не работает. Понимаете, один и тот же код в одном режиме (Отладка) работает, в другом (Выпуск) - не работает. Это Вас совсем не смущает? А потом, после внесения изменений, удаляющих "верний цикл", вдруг все и во всех режимах проектирования работает! Я призываю разобраться с этой проблемой, с этой засадой..


        1. wataru
          12.06.2024 12:24
          +2

          Значение счетчика зависло на каком-то значении и не изменяется длительное время.

          Какого счетчика, и как вы это определяете? Выше я уже описал, почему значение счетчика запущеных потоков в окне может стать неверным. Из-за гонки данных при выводе туда значения. Потоки уже все отработали, но в окне осталось, допустим, 1.

          Есть два режима рвботы теста. Один без использования объектов синхронизации, другой использует мютекс.

          И вот вам тут уже несколько человек объясняют, что нельзя без синхронизации изменять данные из разных потоков. Надо использовать atomic переменную хотя бы, если вы хотите без мьютексов.

          который под отладчиком работает, а без отладочного кода, т.е. в режиме Выпуск - не работает

          Как любой программист на C++ вы должны сразу понимать, что это - Undefined Behavior. Где-то в программе ошибка, которая заставляет компилятор генерировать код, не соответствующий вашим ожиданиям. Ошибки могут быть: доступ по удаленному указателю, неинициализированные переменные, выход за границу массива, гонка данных.

          Тут программа может отработать правильно, повиснуть, упасть, вывести бред - в зависимости от казалось бы вообще не влияющих ни на что факторов. Смена режима компиляции, перестановка слагаемых, запущенный антивирус, удалиение или добавление отладочного вывода - это все отлично влияет на поведение программы. Вы точно такое же поведение можете наблюдать, если у вас в программе выход за границы массива. Вот буквально, переменные в другом порядке объявите - и все заработает. Похоже на бред? Это база программирования на C++! Надо знать Undefined Behavior и к чему оно приводит.

          В вашем случае есть гонка данных. Это происходит, когда 2 потока в одно и то же время меняют одни и те же данные. При изменении режима компиляции меняется машинный код, меняются тайминги и запросто может стать так, что эти 2 потока уже не одновременно меняют данные, а с задержкой в 10нс и все работает. Потому что неоптимизированный код на 10нс дольше выполняется в одном потоке, например, потому что что-то не влезло в кеш процессора. При этом на другом компьютере или при другой фазе луны и этот код может сработать с ошибкой.

          А вообще, при Undefined behavior бывает еще интереснее. Компилятор может по вашему коду нагенерировать полный казалось бы бред при оптимизации: https://mohitmv.github.io/blog/Shocking-Undefined-Behaviour-In-Action/. Поэтому отладочная сборка - без оптимизаций - часто работает лучше режима "Выпуск". Вот, по ссылке выше, например, генерируется бесконечный цикл на месте безобидно выглядящего цикла в 10 итераций.


        1. wataru
          12.06.2024 12:24

          конструктивную очень даже приветствую ;)

          Тогда поехали:

          ThCounter::~ThCounter(void) {
              bIfRun = false;
              quit();         // корректный способ завершения потока
              wait(3000);         // ожидание завершени потока (неограниченное ожидание wait())
                              // подробнее см. Боровский А. Qt4.7... стр.170
          }

          quit тут ничего не делает, ибо в вашем потоке нет обработки событий. см справку:

          This function does nothing if the thread does not have an event loop.

          Далее, вы вроде как используете Венгерскую нотацию (p,b,str и т.д. в начале имен переменных), но при этом забываете менять эти префиксы:

          int pVarNumberOfThreads
          bool p
          bool pIfSemaphoreLotThreads

          И много других примеров. "m_" используется для членов класса, но у вас такой только один m_mutex. Еще, он по уму должен быть приватным членом.

          Премешанные члены и методы - вы сами не путаетесь, что у вас где объявлено?

          В догонку про имена, g_mutex - не глобальная переменная, а еще один (по идее приватный) член класса. В чем разница с m_mutex?

          Вместо:

          ThCounter::ThCounter(long *pIntMax, MainWindow *parent):
              QThread()
          {
              pFThCounter = parent;
              nMaxValue = *pIntMax;
              bIfRun = true;
          }

          Должно быть:

          ThCounter::ThCounter(long nMax, MainWindow *parent):
              nMax(nMax),
              pFThCounter(parent),
              bIfRun(true)
          {}

          Зачем там передавать указатель на число, а не само число? Почему вместо списка инициализации членов у вас куча операций присваивания? Какой-то код в конструкторе пишут, только если там что-то не тривиальное. Вызовы каких-то функций, какие-нибудь вычисления. Для простой инициализации челнов класса и придумали список инициализации. Qthread() можно не писать. Конструктор по умолчанию у базового класса вызовется сам.

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

          Зачем вам аж 2 класса для потока, да еще какая-то своя коллекция, я так и не понял. Хватило бы std::vector<*ThCounter> . А счетчик активных потоков - может быть в классе реализующим интерфейс счетчика.


          1. lws0954 Автор
            12.06.2024 12:24

            Так я с Вами согласен на все 100% и даже на все 200%. И если мне мой программист принес подобный код, то я ему, может, подобные замечания тоже сделал.

            Но есть один нюанс. Я не пишу код так, написал. Я пишу в рамках ВКПа. а там иной стиль и иные требования. Здесь же я подстраивал код под ВКПа-шный, делая его как можно ближе к нему. Это чтобы нащупать ошибку/проблему/ засаду (на выбор). А посему делалось все на коленке и скорую руку - лишь бы заработало. Оно заработало и заработало абсолютно идентично. В этом смысле я поставленной цели достиг, не парясь над стилем. Еще раз, подобный стиль мне почти чюжд ;) Я за автоматный.

            Понятное дело, что теперь, создавая потоки, я буду четко следовать своим принципам и не расслабляться. Не писать так, как оно было представлено в статье, а строго - в автоматном стиле. Это лишний раз меня убеждает, что я выбрал правильный стиль - автоматный. Все это представлено (кажется) ниже - Автоматный код потока.

            А Вам спасибо за критику. Все правильно. Так и надо писать, следуя определенным требования.

            Ну, и пусть "малец" неопределенного возраста со слабым знанием русского языка немножно потешится над "Атцом", которого так "разложили" :)))


            1. wataru
              12.06.2024 12:24

              Но ваш "автоматный" подход не исправляет эти недочеты. Он ничего не говорит о том, зачем вам 2 класса для потока или почему вы там указатель на int передаете в функцию. И классы у вас в коде все так же есть, судя по другому комментарию. Раз вы не умеете списки инициализации использовать, этот недочет в коде все еще остается.


            1. Bender_Rodrigez
              12.06.2024 12:24
              +1

              чюжд

              Ну, главное, что у вас знание русского на высшем.

              Ну да вытрем же сопли, коллега, и лучше копнем вглубь, ради повышения собственного уровня знания: автоматный код или стиль - это что значит в данном случае, и в чем его преимущества в программировании многопоточных приложений? Это там, где switch используется, судя по коду из комментария ниже?


              1. lws0954 Автор
                12.06.2024 12:24

                Ну, главное, что у вас знание русского на высшем.

                А, ведь, заметил, однако :))) Значит не все потеряно.. А то я уж думал - реально "жертва ЕГ" :)

                Ну да вытрем же сопли, коллега, и лучше копнем вглубь...

                Я рад, что Вы уже на изготовке, коллега ;) Копнем...

                Начнем хотя бы со статьи - Автоматное программирование... Осилите - можно дальше дерзать. Откройте мой профиль и в разделе "Публикации" есть много разного. Но в основном про автоматное програмирование. И если будут нормальные вопросы, то будут и нормальные ответы. Без соплей :) Удачи...


                1. Bender_Rodrigez
                  12.06.2024 12:24
                  +1

                  А то я уж думал - реально "жертва ЕГ" :)

                  Нет-нет, вы правильно подумали - я пропащий в этом смысле.

                  Поэтому меня и удивило, что вы, имея классическое, лучшее в мире, советское образование, как я понимаю, не подвергшись влиянию деградации сознания под названием ЕГЭ, делаете абсолютно такие же ошибки...

                  Осилите - можно дальше дерзать.

                  Из всего этого понятно, что любая, абсолютно любая, программа является программой конченого автомата и представляет таблицу переходов из одного состояния автомата в другое - каждая строка программы, if-else блок - самая примитивная реализация.

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

                  Более простым и понятным языком для практикующих инженеров все те же принципы, и про предикаты, и про действия, изложены в брошюре "Чистый код" Роберта Мартина.

                  Его функция переходов определяет следующее состояние q(t+1) в зависимости от текущего значения состояния q и значения двоичной входной переменной, представленной булевой функцией –  f(x1, x2, …, xm) от входных переменных в дизъюнктивной нормальной форме (ДНФ)

                  Оператор if?

                  Отсутствие входной булевой переменной в простой конъюнкции  ДНФ означает игнорирование состояния соответствующего входного канала при вычислении условия перехода. Соответственно при этом не нужно запускать и предикат, который сопоставлен данному каналу.

                  Оператор ИЛИ внутри условия if?

                  Существенной особенностью приведенного закона функционирования КА является привязка выходных каналов автомата к следующему моменту времени подобно изменению внутреннего состояния. Это исключает мгновенную реакцию автомата на текущее изменение сигналов входных каналов. Так моделируется свойство инерционности, присущее всем реальным системам.

                  Возвращаемое значение и локальные переменные функции?

                  Это противоречит признаваемому даже в теории факту, что в реальной ситуации выходной сигнал y(t) автомата всегда появляется после входного сигнала x(t) (см., например,  [11]).

                  Да, но если завернуть логику в функцию, то при чтении исходного кода программы в человеческом мозге задержка будет нулевая:

                  foo = bar()

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

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

                  На картошку съездили бы, чтоли.

                  По поводу параллельного программирования, достаточно неплохо и доступно, как мне кажется, описано тут:

                  https://learn.microsoft.com/en-us/dotnet/csharp/asynchronous-programming/

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

                  Многопоточность это просто

                  Представьте, что вы в курятнике и шаркаете рукой по насесту с соломой, чтобы найти яйца.

                  С другой стороны насеста идет другой человек и тоже пытается нащупать яйца.

                  И вот вы нащупываете ваши яйца, и ваш оппонент одновременно нащупывает те же яйца. Это вы входите в критическую секцию, где возникает состояние гонки.

                  Чтобы его разрешить, нужно либо перейти к другому насесту, либо спросить конкурента: "а не пидорас ли ты?"

                  Потом, нужно спросить себя: "а не пидорас ли я?"

                  Ну, в общем, кое-как, но это примитив синхронизации семафор.

                  Кстати, IMHO, C# c WinForms для ваших целей выглядит более подходящим по многим параметрам.

                  В целом, интересно для общего представления теории автоматов, не более, но кто знает - может, это дискуссия из разряда споров Нильса Бора и Альберта Эйнштейна.


          1. voldemar_d
            12.06.2024 12:24

            wait(3000); // ожидание завершени потока

            Почему не 1000? Или не 30000?


      1. MiyuHogosha
        12.06.2024 12:24

        Помоему в Qt вызов чего либу гуишного из другого потока может окончиться катастрофой. Надо использовать сигналы (тогда SetText вызовется в контесте главного потока) или QMetaObject


  1. lws0954 Автор
    12.06.2024 12:24

    Это полный код теста.


    1. wataru
      12.06.2024 12:24
      +1

      Смотрите внимательно, вы не туда отвечаете. Вы пишите коменты к статье. О них не приходят уведомления. Лучше отвечать на комментарий через кнопку "ответить". Я ваш код совершенно случайно заметил.


      1. lws0954 Автор
        12.06.2024 12:24

        Вроде стараюсь попадать :) Но выше опять что ли не попал?


  1. GAlex1980
    12.06.2024 12:24
    +4

    Чего только люди не придумают, лишь бы не объявлять переменные флагов и другие общие объекты как volatile.


    1. lws0954 Автор
      12.06.2024 12:24

      И где ж Вы были раньше? Ведь, зараза, все заработало :)


      1. wataru
        12.06.2024 12:24
        +1

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


        1. lws0954 Автор
          12.06.2024 12:24

          Я с Вами даже соглашусь. Сейчас помогло, в другой ситуации ,возможно, и нет. Но на вооружении надо иметь. Хотя, например, я очень не люблю всякие оптимизации. Было время писал для микропроцессоров так отрубил сразу. А вот тут расслабился. Как-то давно, очень давно, не сталкивался с этой проблемой. Да и здесь бы не столнулся, т.к. пишу программы все же по-иному. В другом, так сказать, стиле - автоматном. И если бы не лень, спешка, то код выглядел бы так (в статьях по ссылке про это есть) . Здесь все работает без вопросов:

          Автоматный код потока
          void ThCounter::run() {
              nState=0; int n=0;
              while (bIfRun) {
                  switch(nState) {
                  case 0:
                      if (!pFThCounter->bIfLoad) break;
                      else nState=1; break;
                  case 1:
                      if (n<nMaxValue) {
                          bool bMx = pFThCounter->pIfMutexLotThreads;
                          if (bMx) pFThCounter->AddCounterMx();
                          else pFThCounter->AddCounter();
                          QString qstr = pFThCounter->pVarStrSleepLotThreads;
                          if (qstr.length()>0) {
                              int nSleep = qstr.toInt();
                              usleep(nSleep);
                          }
                          n++;
                          break;
                      }
                      else nState=2; break;
                  case 2: bIfRun = false; break;
                  }
              }
              pFThCounter->DecrementActive();
          }

          Я, надеюсь и даже верю, что оптимизация сюда не пробъется еще долго и я буду и дальше в этом смысле - оптимизации кода чувствовать себя спокойно ;)


          1. wataru
            12.06.2024 12:24

            Все тоже самое. Вы думаете, компилятор не найдет, что тут соптимизировать? Вы лишь чуть переставили конструкции управления потоком исполнения, получив эквивалентный, на ваш взляд код. И все те же UB в нем все еще остались. Потому что у вас нет проблем с циклами. Сами циклы никаких UB не содержат.

            pFThCounter->DecrementActive() все так же содержит гонку данных, если вы там Mutex или InvokeMethod не вставили.

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


            1. lws0954 Автор
              12.06.2024 12:24

              Вы не представляете до чего я еще могу "докатиться" ;). На самом деле я бы создал код, который ближе к этому:

              Hidden text
              int ThCounter::x1() { return pFThCounter->bIfLoad; }
              int ThCounter::x2() { return n<nMaxValue; }
              void ThCounter::y1() {
                  bool bMx = pFThCounter->pIfMutexLotThreads;
                  if (bMx) pFThCounter->AddCounterMx();
                  else pFThCounter->AddCounter();
                  QString qstr = pFThCounter->pVarStrSleepLotThreads;
                  if (qstr.length()>0) {
                      int nSleep = qstr.toInt();
                      usleep(nSleep);
                  }
                  n++;
              }
              void ThCounter::run() {
                  nState=0; n=0;
                  while (bIfRun) {
                      switch(nState) {
                      case 0:
                          if (!x1()) break;
                          else nState=1; break;
                      case 1:
                          if (x2()) {
                              y1();
                              break;
                          }
                          else nState=2; break;
                      case 2: bIfRun = false; break;
                      }
                  }
              
                  pFThCounter->DecrementActive();
              }
              

              Так я делаю в ВКПа. Только заменяю еще логику на таблицу переходов автомата.

              Где тут простор оптимизатору? Максимум предикаты и действия. Ну, может в потоке что-нибудь начудить. И я не думаю, что ему удастся вставить какую-нибудь "подлянку" :)

              А как Вы думаете?


              1. wataru
                12.06.2024 12:24
                +1

                Где тут простор оптимизатору?

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

                Вот выход компилятора на ваш (слегка отредактированный, чтобы компилировалось без QT) код. С компиляциями и без. Там же совсем разный ассемблерный код получился. Значит, нашел что и где наоптимизировать компилятор. Посмотрите, как он сжал функцию ThCounter::run(), например.

                Например, он может решить, что локальная переменная bMx не достойна быть переменной на стеке и оставит ее в регистре. Переменная, у которой нет адреса. Подлянка же? А вместо операции n=0 он может сгенерировать код, выполняющий операцию xor регистра самого с сабой. Потому что эта машинная команда короче и дает такой же с точки зрения компилятора результат.

                Компилятор может переставить местами инициализацию n и nState в ассемблерном коде. Заменить ваш Switch на череду if-else (вернее, сгенерировать точно такой же ассемлерный код).

                Он может не генерировать функцию x1() и просто тупо вставлять ее код везде вместо вызова.

                И еще кучу всего настолько запутанного, что я даже представить себе не могу.

                И я не думаю, что ему удастся вставить какую-нибудь "подлянку" :)

                Если в программе нет UB, то все эти оптимизации коректны - измененная программа гарантированно работает с тем же результатом. Но если у вас UB есть, а оно есть, то именно эти оптимизации и могут оказаться "подлянкой" - из-за которых все работает неправильно.

                На самом деле я бы создал код, который ближе к этому:

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


    1. voldemar_d
      12.06.2024 12:24

      Современный C++ не рекомендует использовать volatile для таких целей.

      Нужно использовать atomic.


      1. GAlex1980
        12.06.2024 12:24

        volatile  вполне допустимо использовать для переменных, которые читаются в потоке. А atomic для тех, которые читают и пишут. В последнем случае я с Вами согласен.


        1. voldemar_d
          12.06.2024 12:24

          Про это очень много везде пишут, например:

          Volatile is for reading from tape drives. Atomics are for concurrency. One has nothing to do with the other

          И еще:

          In C++11, don't use volatile for threading, only for MMIO

          (почитайте дальше от процитированного места)

          Если кратко, volatile только подсказывает компилятору, что нельзя оптимизировать операции с этой переменной. Например, перегруженные операторы могут не просто присваивать значение переменной, а передавать ее значение в регистры какого-то аппаратного устройства. Оптимизирующий компилятор в каких-то ситуациях может выкинуть какие-то операции, посчитав их лишними, в результате в устройство данные не будут переданы или получены из него (то самое "reading from tape drives"). Вот для таких вещей нужен volatile в современном C++, а не для безопасного доступа к переменной из разных потоков.

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

          Не очень понял мысль. Если в одном потоке переменная записывается, а в другом считывается?

          Можно написать с volatile код, который даже работает. А потом, например, при модификации (возможно, что другим человеком) окажется, что уже не один поток должен писать в переменную, а два или больше. ИМХО, лучше сразу писать правильно, чтобы защититься от возможных проблем в будущем. Которые могут проявиться не сразу, а при каких-то условиях, и на машине у клиента в другом городе, например.


          1. Balling
            12.06.2024 12:24

            Это устаревшая инфлрмация. Volatile увы в gcc имеет реальные эффекты... https://patchwork.ffmpeg.org/project/ffmpeg/list/?q=Volatile&series=&submitter=&archive=&delegate=&state=*

            Один из них нужен для бага в clang, но всё же. https://bugs.llvm.org/show_bug.cgi?id=20849


            1. voldemar_d
              12.06.2024 12:24

              Это устаревшая инфлрмация

              Простите, не понял, что именно устарело?


        1. MiyuHogosha
          12.06.2024 12:24

          Это устаревшая информация из 97го года. volatile только отказывает в оптимизации загрузки (что атомик делает тоже) , но не упорядочивает


      1. voldemar_d
        12.06.2024 12:24

        Тот, кто минусовал, может подробнее пояснить, почему?


  1. alexdora
    12.06.2024 12:24
    +1

    Сам сижу на golang и многопоточность там сильно проще (каналы и тд)

    Начинаю читать статью и после слов:

    Ничто не предвещало беды, когда при увеличении числа потоков и достаточной длительности работы теста время от времени значение общей переменной-счетчика (далее просто счетчика) перестало быть правильным. 

    Понюхал, отчетливо пахнет Race condition. Спустился в комменты, а тут прям «воняет». Дальше статью не читал, комментов достаточно.

    Но добавлю вот что: какие только костыли я не видел которые вставляют в код чтобы решить эту проблему. Из самого безобидного когда расставляют sleep в коде, чтоб не дай бог не было доступа одновременного или одна функция не перегнала другую

    Но проблема обычно в фундаментальном непонимании и/или неправильном представлении самой многопоточности.

    PS: Все таки прочитал статью, ну сюр. C++ не знаю и мне сложно оценить прям код и решение проблемы на C++. Но емае, вывод просто шедеврален:

    При общении друг с другом - многократно.  Контролировать подобный коллектив весьма сложно (вспомним начало статьи [3]). Теория убеждает и даже доказывает, что вряд ли вообще такое возможно. По отношению к потокам, конечно.

    Тут статья на хабре есть, как сделали реализацию каналов как в Golang на c++. И нормально там потоки общаются и в ус не дуют. И нет никаких там проблем с тем чтоб так называемый вами «коллектив» контролировать. Я верю, что на c++ есть и другие решения, но как минимум одно есть прям готовое и на хабре, без всяких велосипедов.

    Автору желаю добра и все таки изучить матч часть. Велосипеды это здорово и интересно, но иногда быстрее поехать на машине.

    PS: сейчас задумался, почему я не знаю C++, но знаю что на хабре статья про каналы как в go для многопоточности.


    1. lws0954 Автор
      12.06.2024 12:24

      Все это правильно и, возможно, хорошо. Я знаю (немного;) С++, но не знаю Go и здесь полностью Вам доверяю. А сложно ли эту же задачу сделать на Go? Заодно и оценили бы эффективность (по времени работы теста, конечно) каналов. Ну, и я бы немного принюхался к... Go.Попробуете? Потому как питоне есть, на С++. А на Go как?


      1. voldemar_d
        12.06.2024 12:24

        Вроде как, вместо C++ рекомендуют не Go, а Rust. Реально хороший язык.


      1. alexdora
        12.06.2024 12:24

        На первый взгляд исходя из контекста статьи это 2 строчки кода Golang + само тело функции которое что-то там делает. Я С++ очень плохо знаю, поэтому вы мне скажите на словах что должна делать функция (какое действие), я напишу вам код на Golang. Просто вы в статье не описали что конкретно делает функция кроме какого-то там счетчика


  1. OverThink
    12.06.2024 12:24

    Инкремента счётчика на 1 происходит в 3 операции. Если нужно за одну операцию то нужно использовать атомарные типы. У каждого потока свой стэк.


    1. GarryC
      12.06.2024 12:24

      Даже атомарные типы не инкрементируются за 1 операцию, Вас обманули. Просто тем или иным способом все те же 3 операции делают не прерываемыми и снаружи они выглядят, как одна..


      1. Kotofay
        12.06.2024 12:24

        Разве lock xadd можно как то посередине приостановить?


  1. GarryC
    12.06.2024 12:24

    Вообще то тут засада почти в каждой строке:
    4. предполагается зависание в ожидании bIfRun, но начальное значение переменной не представлено и модификация переменной не указана. Как говорят на одном ресурсе в ответ на подобные вопросы без должных условий : "Я свой магический шар сегодня дома оставил"

    8-14 можно (и нужно) переписать и вместо:

    if (bSm || bMx) {
            if (bSm) pFThCounter->AddCounterSem();
                else {
                    pFThCounter->AddCounterMx();
                }
            }
            else pFThCounter->AddCounter();

    получить значительно более вменяемое:

    if (bSm) { pFThCounter->AddCounterSem();
    }; else if (bMx) { pFThCounter->AddCounterMx();
    }; else { pFThCounter->AddCounter();
    };

    Кстати, компилятор даже исходную (не оптимальную конструкцию) реализует также, как и для вменяемого варианта - это на тему, "что может компилятор оптимизировать".

    И так далее и тому подобное.


    1. lws0954 Автор
      12.06.2024 12:24

      предполагается зависание в ожидании bIfRun,  но начальное значение переменной не представлено и модификация переменной не указана. 

      Здесь все нормально. Выше есть полный код класса потока, где данный флаг устанавливается в конструкторе, а сбрасывается в деструкторе. Сбрасывать его в потоке, как в приведеннонм коде, можно и не делать, т.к. он (поток) все равно завершается по условию достижения максимума количества циклов (по этому условию происходит выход из цикла). Хотя обычно этот флаг я сбрасываю. Но тут что-то забыл, похоже :)

      получить значительно более вменяемое:

      Ну, это совсем уж не принципиально. Вам нравится так, мне этак. Главное, чтобы не было ошибки. А компилятор должен оптимизировать, создавая эквивалентные конструкции, т.е. в любом случае должно быть тоже нормально. Наши предпочтения не должны влиять на результат ;)

      И так далее и тому подобное

      Что там еще Вам так уж не понравилось? ;)