Обновить

Комментарии 31

Я, надеюсь, не похож на «мазохиста», терпящего «пот, боль и кровь» многопоточности (см. обсуждение статьи [7]).

Во-первых, вы искажаете смысл исходной цитаты, а именно "голая многопоточность -- это пот, боль и кровь." Т.е. не многопоточность вообще, а многопоточность на базе самых низкоуровневых примитивов, от atomic-ов и на коленке склепанных самостоятельно lock-free структур до semaphores, mutexes, events и condition variables. К счастью, есть более высокоуровневые подходы, очень сильно (я бы даже сказал драматически) упрощающие многопоточное программирование (но это обходится далеко не бесплатно).

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

параллельное программирование – это далеко не про скорость. Скорость – лишь ее побочный эффект.

А мужики-то и не знают...

Подобные задержки (уж поскольку мы их рассматриваем) фактически уже созданы.

вряд ли можно переоценить вклад простого создателя задержек в дело глобального распараллеливания. Можно пожелать только успехов тому, кто так беззаветно преданно жертвует остатками своего разума для достижения этой благородной цели глобального распараллеливания. аминь

Спасибо за пожелания. Надеюсь, они искренние :)

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

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

Так и про "задержку". Я привел ее формальную модель. Математически строгую. Она объясняет, что такое задержка, какие они бывают и т.д. и т.п. Понимаете ли Вы, что такое "задержка". Какова ее роль вообще, а не только в программировании. Судя по комменту - есть определенные сомнения.

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

И тогда, возможно, не придется "мучиться с многопоточностью".

И как вы умудрились при такой-то любви и таких-то знаниях так вляпаться-то?

Округа так плотно "заминирована", что избежать этого практически невозможно ;)

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

А вот, кстати... Что в том коде, который я привел в статье про "засады", было не так? Вы ж прочитали статью? И где там была "мина", ась? ;)

И где там была "мина", ась? ;)

В ДНК

Я так и знал. :)

И где там была "мина"

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

Если инкрементить в нескольких потоках глобальный счётчик вот так:

long g_counter = 0; // Shared counter (INTENTIONALLY non-atomic)

DWORD WINAPI UnsafeThreadProc(LPVOID lpParam)
{
    for (int i = 0; i < NUM_INCREMENTS; i++) {
        long local = g_counter;
        g_counter = local + 1;
    }
    return 0;
}

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

Чтобы этого избежать, либо используют интерлок

volatile LONG g_counter = 0; 

DWORD WINAPI SafeThreadProc(LPVOID lpParam)
{
    for (int i = 0; i < NUM_INCREMENTS; i++) {
        InterlockedIncrement(&g_counter);
    }
    return 0;
}

Либо критическую секцию

CRITICAL_SECTION g_Lock;

DWORD WINAPI SafeThreadProc(LPVOID lpParam)
{
    LONG newVal;
    EnterCriticalSection(&g_Lock);
    for (int i = 0; i < NUM_INCREMENTS; i++) {
        newVal = g_counter + 1;
        g_counter = newVal;
    }
    LeaveCriticalSection(&g_Lock);
    return 0;
}

Вот и всё.

Полный код - три варианта
// Небезопасный код

#include <windows.h>
#include <stdio.h>

#define NUM_THREADS 1000
#define NUM_INCREMENTS 100000

long g_counter = 0; // Shared counter (INTENTIONALLY non-atomic)

DWORD WINAPI UnsafeThreadProc(LPVOID lpParam)
{
    for (int i = 0; i < NUM_INCREMENTS; i++) { // Non-atomic read-modify-write
        long local = g_counter;
        g_counter = local + 1;
    }
    return 0;
}

int main(void)
{
    HANDLE threads[NUM_THREADS];

    // Create 1000 threads
    for (unsigned long i = 0; i < NUM_THREADS; ++i) {
        threads[i] = CreateThread(
            NULL,                // default security
            0,                   // default stack size
            UnsafeThreadProc,    // thread function
            (LPVOID)(ULONG_PTR)(i + 1), // parameter: 1..NUM_THREADS
            0,                   // run immediately
            NULL                 // thread id (unused)
        );
    }
    
    // Wait for all threads one by one (simplest approach; avoids 64-handle limit)
    for (unsigned long i = 0; i < NUM_THREADS; ++i) {
        WaitForSingleObject(threads[i], INFINITE);
        CloseHandle(threads[i]);
    }
    
    printf("\nExpected final count: %d\n", NUM_THREADS * NUM_INCREMENTS);
    printf("Actual final count (non-atomic): %ld\n", g_counter);
    return 0;

}

//==============================================================================

// Безопасный код - 1

#include <windows.h>
#include <stdio.h>

#define NUM_THREADS 1000
#define NUM_INCREMENTS 100000

volatile LONG g_counter = 0; // Shared counter for interlocked operations (must be LONG)

DWORD WINAPI SafeThreadProc(LPVOID lpParam)
{
    unsigned long thread_num = (unsigned long)(ULONG_PTR)lpParam;

    // Atomic increment; returns the incremented value
    LONG newVal;
    for (int i = 0; i < NUM_INCREMENTS; i++) {
        newVal = InterlockedIncrement(&g_counter);
    }
    return 0;

}

int main(void)
{
    HANDLE threads[NUM_THREADS];

    for (unsigned long i = 0; i < NUM_THREADS; ++i) {
        threads[i] = CreateThread(
            NULL,               // default security
            0,                  // default stack size
            SafeThreadProc,     // thread function
            (LPVOID)(ULONG_PTR)(i + 1), // parameter: 1..NUM_THREADS
            0,                  // run immediately
            NULL                // thread id (unused)
        );
    }
    
    // Wait for all threads, then clean up
    for (unsigned long i = 0; i < NUM_THREADS; ++i) {
        WaitForSingleObject(threads[i], INFINITE);
        CloseHandle(threads[i]);
    }
    
    printf("\nExpected final count: %d\n", NUM_THREADS * NUM_INCREMENTS);
    printf("Actual final count (atomic): %ld\n", g_counter);
    return 0;

}

//==============================================================================

// Либо так

#include <windows.h>
#include <stdio.h>

#define NUM_THREADS 1000
#define NUM_INCREMENTS 100000

volatile LONG g_counter = 0; // Shared counter for interlocked operations (must be LONG)

// Optional: serialize printf to keep lines intact (no mixed output)
CRITICAL_SECTION g_Lock;

DWORD WINAPI SafeThreadProc(LPVOID lpParam)
{
    // Atomic increment; returns the incremented value
    LONG newVal;
    EnterCriticalSection(&g_Lock);
    for (int i = 0; i < NUM_INCREMENTS; i++) {
        newVal = g_counter + 1;
        g_counter = newVal;
    }
    LeaveCriticalSection(&g_Lock);
    return 0;
}

int main(void)
{
    HANDLE threads[NUM_THREADS];

    InitializeCriticalSection(&g_Lock);
    
    // Create 1000 threads
    for (unsigned long i = 0; i < NUM_THREADS; ++i) {
        threads[i] = CreateThread(NULL, 0, SafeThreadProc,     // thread function
            (LPVOID)(ULONG_PTR)(i + 1), 0,                  // run immediately
            NULL
        );
    }
    
    // Wait for all threads, then clean up
    for (unsigned long i = 0; i < NUM_THREADS; ++i) {
        WaitForSingleObject(threads[i], INFINITE);
        CloseHandle(threads[i]);
    }
    
    DeleteCriticalSection(&g_Lock);
    
    printf("\nExpected final count: %d\n", NUM_THREADS * NUM_INCREMENTS);
    printf("Actual final count (atomic): %ld\n", g_counter);
    return 0;

}

Если есть желание понять на самом низком уровне как это работает, то можно на ассемблере написать как-то так, используя инструкцию XCHG:

SpinLock DD 0            ; Shared spin lock (0 = unlocked, 1 = locked)
Counter  DQ 0            ; 64-bit shared counter

ThreadProc PROC
  
	mov r8, NUM_INCREMENTS
	align 16
SpinWait:  ; Spin lock acquire
    mov eax, 1
    xchg eax, [SpinLock] ; Atomically try to acquire lock
    cmp eax, 0           ; Was lock previously 0 (unlocked)?
    je LockAcquired      ; If yes, we acquired the lock
    ; If not acquired, wait and retry
    PAUSE                ; Hint to CPU that we are in a spin-wait loop
    jmp SpinWait

LockAcquired:
    ; Critical section begins - Increment 64-bit counter
    mov rax, [Counter]
    inc rax
    mov [Counter], rax
    ; Critical section ends - Release lock
    mov [SpinLock], 0
	dec r8 ; total increments counter
	jnz SpinWait ; loop to the start

    xor eax, eax
    ret
ENDPROC ThreadProc

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

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

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

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

.

Проблема была в том, что в режиме Debug работал проект без проблем, а в режиме Release вылетал напрочь.

А, понятно, так бывает, да.

Обычно это иллюстрируют вот таким "не надо так делать" примером:

#include <windows.h>
#include <stdio.h>

BOOL quit = FALSE;
BOOL stop = FALSE;

DWORD WINAPI SetterThread(LPVOID lpParam)
{
    printf("Setter: started\n");
    Sleep(1000);
    stop = TRUE;
    printf("Setter: stop flag set\n");
    while (!quit) {}
    printf("Setter: stopped\n");
    return 0;
}

DWORD WINAPI GetterThread(LPVOID lpParam)
{
    printf("Getter: started, waiting for the stop flag...\n");
    while (!stop) {}
    printf("Getter: got stop flag, quit\n");
    quit = TRUE;
    return 0;
}

int main(void)
{
    HANDLE hSetter = CreateThread(NULL, 0, SetterThread, NULL, 0, NULL);
    HANDLE hGetter = CreateThread(NULL, 0, GetterThread, NULL, 0, NULL);

    // Wait for both threads to complete
    WaitForSingleObject(hSetter, INFINITE);
    WaitForSingleObject(hGetter, INFINITE);

    CloseHandle(hSetter);
    CloseHandle(hGetter);
    return 0;
}

Тут проблема в том, что оптимизатор может "оптимизировать" этот код:

while (!stop) {}

примерно вот так (либо в регистры процессора закеширует):

if (!stop) {
    for (;;) {} // infinite loop
}

И он никогда не завершится. Я это могу воспроизвести на древнем clang 3.3.

Лечится объявлением переменных как volatile, тогда оптимизатор их трогать не будет, либо использованием InterlockedCompareExchange, либо нормальной синхронизацией потоков через WaitForSingleObject.

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

Обычно любому наблюдаемому и воспроизводимому феномену находится рациональное объяснение.

Правильно. Что-то подобное подозревал и я. Потому и начал "копать" цикл. И результат можно именно этим и объяснить, т.е. какой-то не очень корректной (для потоков, конечно) оптимизацией. Но выяснять до конца тоже не очень большой смысл.По крайней мере для меня. Но в целом Вы все объяснили достаточно достоверно. Как бы ДНК оно и есть ДНК. Пусть оно таким и остается :) Хотя, конечно, такой опыт тоже нужен. Но это уже и есть ... "кровопускание" :(

Тут проблема в том, что оптимизатор может "оптимизировать" этот код:
while (!stop) {}

ЕМНИП, в C++ (именно в С++, а не в чистом Си) бесконечный цикл без выраженных побочных эффектов -- это UB. И компилятор может сотворить все, что ему захочется.

Где тут бесконечный цикл? Он был бы в случае: while(true){}. А тут нормальный цикл только с пустым телом цикла.

Компилятор не видит изменения для stop и считает, что выражение всегда истинно.

Компилятор - не видит?! Да какое мне до этого дело. Если программист сказал stop, значит, - стоп! Больно умный! :)

ЕМНИП, в C++ (именно в С++, а не в чистом Си) бесконечный цикл без выраженных побочных эффектов -- это UB.

Так было в какой-то момент, но вроде P2809 теперь определяет это как легитимный код. Я походу всё время использовал for(;;); если мне где-то надо было намертво встать без особых побочных эффектов (если не обращать внимания на расход ресурсов проца, конечно, но это для быстрой проверки чего-либо, а не для продакшена).

Так было в какой-то момент, но вроде P2809 теперь определяет это как легитимный код.

Для многих практикующих C++ников стандарт C++26 еще не скоро станет обыденной реальностью.

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

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

Читаете внимательно. В режиме Debug программа работала, т.е. ошибки нет и не было. Тут если и есть ошибка, то только не моя.

Кстати про ошибки. Вот сейчас бился над одной :)

Пишу продолжение статьи и делаю таймерную задержку. У меня есть вложенный автомат - CFDelay. Все, вроде, работало и вдруг - перестает. Запускаешь - глюк. Лопачу, сравниваю код - все, вроде, ОК. Натыкаюсь что в одном варианте - работает, в другом - нет. Варианты немного отличаются, но только не в логике. Но ошибка-то есть! Короче.

Вот только выяснил. Проблема в строке:

#include "./LSYSLIB/FDelay.h"

Вставлена - работает. Нет ее - глюк!!! Компиляция - на ура! Запуск - глюк! Сейчас, кода немного подправил вызов заголовков все работает. Но... что было не так-то!?

Сделать-то сделал, но ощущение "костыля" как-то не уходит.... :(

"Тяжела и неказиста жизнь простого программиста" :)

В режиме Debug программа работала, т.е. ошибки нет и не было. Тут если и есть ошибка, то только не моя.

Детский сад, младшая ясельная группа. Когда приложение работает в Debug-е, но падает в Release, то с вероятностью в 99% -- это баг в программе, а не в компиляторе. Особенно в многопотоке, где время работы конкретных кусков кода критически важно при обращениях к разделяемым данным. В Debug-е код работает в разы медленнее и "времянка" (есть такой старый термин) распределяется совсем не так, как в Release. Из-за чего в Release проявляются такие фокусы, до которых не сразу и додумаешься.

Блин, это прописные истины, это грабли, по которым оттаптываются все, кто хоть мало-мальски имел дело с многопоточностью.

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

Вот про эти "грабли" и приходится разъяснять всяким разным из "ясельных групп".

Разъяснять в стиле: "И для меня это осталось загадкой до сих пор."

Ну вперед.

Ну, все знать невозможно. А тут явно не моя "засада".

А тут явно не моя "засада".

lavrov.jpg

Проблема в строке:

#include "./LSYSLIB/FDelay.h"

Вставлена - работает. Нет ее - глюк!!! Компиляция - на ура! Запуск - глюк! Сейчас, кода немного подправил вызов заголовков все работает. Но... что было не так-то!?

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

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

Извиняйте - оговорился. Кстати, когда писал, то помню был какой-то "дискомфорт", но не стал заморачиваться... :)

По поводу FDelay.h. Конечно, на гите должно все быть, если интересует. Там все до безобразия просто. Это обычный автомат, считающий такты. В одной ситуации от вызывается подобно подпрограмме, как вложенный автомат, в другой - на его базе создается процесс.

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

Конечно, на гите должно все быть, если интересует.

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

Сейчас мы их "докачаем" ;). Смотрите мою статью. Там в конце ссылка на гит.

Про класс CFDelay. См. заголовок и реализацию - FDelay.h, FDelay.cpp. Как пользоваться - методы FCreateParDelay(), FIsActiveParDelay() для параллельной задержки и FCreateDelay(int nDelay) - для вложенной Они в классе LFsaAppl (lfsaappl.h, lfsaappl.cpp).

Но есть нюанс, который, возможно, влияет. На ESP32 - без пользовательских библиотек, в Qt - с библиотеками.

параллельное программирование – это далеко не про скорость. Скорость – лишь ее побочный эффект.

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

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации