Комментарии 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){}. А тут нормальный цикл только с пустым телом цикла.
ЕМНИП, в C++ (именно в С++, а не в чистом Си) бесконечный цикл без выраженных побочных эффектов -- это UB.
Так было в какой-то момент, но вроде P2809 теперь определяет это как легитимный код. Я походу всё время использовал for(;;); если мне где-то надо было намертво встать без особых побочных эффектов (если не обращать внимания на расход ресурсов проца, конечно, но это для быстрой проверки чего-либо, а не для продакшена).
Т.е. ошибка ушла, словно и не было. И для меня это осталось загадкой до сих пор.
Т.е. вы совершили ошибку в многопоточном коде, найти ее не смогли, написали очередную графоманскую статью о том, как вы не можете в многопоточный код, но все равно позволяете себе иронизировать над "пот, боль и кровь"?
Читаете внимательно. В режиме Debug программа работала, т.е. ошибки нет и не было. Тут если и есть ошибка, то только не моя.
Кстати про ошибки. Вот сейчас бился над одной :)
Пишу продолжение статьи и делаю таймерную задержку. У меня есть вложенный автомат - CFDelay. Все, вроде, работало и вдруг - перестает. Запускаешь - глюк. Лопачу, сравниваю код - все, вроде, ОК. Натыкаюсь что в одном варианте - работает, в другом - нет. Варианты немного отличаются, но только не в логике. Но ошибка-то есть! Короче.
Вот только выяснил. Проблема в строке:
#include "./LSYSLIB/FDelay.h"
Вставлена - работает. Нет ее - глюк!!! Компиляция - на ура! Запуск - глюк! Сейчас, кода немного подправил вызов заголовков все работает. Но... что было не так-то!?
Сделать-то сделал, но ощущение "костыля" как-то не уходит.... :(
"Тяжела и неказиста жизнь простого программиста" :)
В режиме Debug программа работала, т.е. ошибки нет и не было. Тут если и есть ошибка, то только не моя.
Детский сад, младшая ясельная группа. Когда приложение работает в Debug-е, но падает в Release, то с вероятностью в 99% -- это баг в программе, а не в компиляторе. Особенно в многопотоке, где время работы конкретных кусков кода критически важно при обращениях к разделяемым данным. В Debug-е код работает в разы медленнее и "времянка" (есть такой старый термин) распределяется совсем не так, как в Release. Из-за чего в Release проявляются такие фокусы, до которых не сразу и додумаешься.
Блин, это прописные истины, это грабли, по которым оттаптываются все, кто хоть мало-мальски имел дело с многопоточностью.
Вот про эти "грабли" и приходится разъяснять всяким разным из "ясельных групп". А то, блин, разобьют носы на всяких там качельках, а потом "плачутся" про всякие "пот, боль и кровь"... :)
Вот про эти "грабли" и приходится разъяснять всяким разным из "ясельных групп".
Разъяснять в стиле: "И для меня это осталось загадкой до сих пор."
Ну вперед.
Проблема в строке:
#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 - с библиотеками.
параллельное программирование – это далеко не про скорость. Скорость – лишь ее побочный эффект.
Скажу крамольную мысль, но обычно ради этого побочного эффекта и мучаются с многопоточностью

Автоматы, потоки. Логические схемы. Задержка распространения