Там ещё (в предыдущем коде) я по запаре вместо esi использовал ebx в паре мест (в адресации). И всё-таки реализация накладывает свои коррективы на теорию. Не будет Remain'а, я ж от него отказался в прошлый раз как раз из-за того, что нужно менять сразу 2 переменные в основном цикле. Итак, имеем структуру ThreadData, которая содержит 4 поля dword для каждого потока: Handle, Lock (флаг запрета обработки итераций), Current (номер текущей итерации), Last (номер последней итерации). Обозначу смещения до них как @Handle, @Lock, @Current и @Last (0, 4, 8 и 12). Работающий поток: Код работающего потока: Код (ASM): ; esi = номер текущего потока, начиная с 0. mov ebp,[ForProc] ; ebp = адрес основной процедуры цикла lea edi,[esi+esi] lea [edi*8+ThreadData] ; edi = ThreadData + esi*16 mov ebx,[edi+@Current] cmp ebx,[edi+@Last] ; Current <= Last ? jle .start ; да, стартуем jmp .finish ; нет, это пустой цикл, сразу выходим ; Spin-цикл ожидания разблокировки (пока кто-то пытается отобрать у нас работу) .pause: mov eax,16 @@: pause cmp dword [edi+@Lock],0 je .start ; выходим из цикла dec eax jnz @B invoke SwitchToThread ; раз в 16 итераций переключаем контекст, т.к. при таком большом числе ; циклов есть немалая вероятность, что перехватывающий поток находится на том же ядре jmp .pause .loop: mov [edi+@Current],ebx ; mov атомарен, lock не нужен cmp dword [edi+@Lock],0 jne .pause ; у нас пытаются отобрать работу? .start: stdcall ebp, ebx, esi ; вызов основной функции цикла (с 2 параметрами: номером итерации и потока) inc ebx ; Current++ cmp ebx,[edi+@Last] ; Current+1 <= Last ? jle .loop ; да, продолжаем цикл .findjob: ; тут мы переходим в режим перехватчика и начинаем искать незавершённый поток, ; у которого есть необработанные итерации (не считая текущей, которую он обрабатывает) Перехватчик (отбирающий поток) делает так: Код (ASM): ; edx = номер потока с наибольшей разницей Last-Cur (если у всех потоков эта разница <= 1 ; завершаем работу, не доходя до этого места в коде) ; eax = Last этого потока lea ecx,[edx+edx] lea ecx,[ecx*8+ThreadData] ; ecx = ThreadData + edx*16 mov edx,1 xchg [ecx+@Lock],edx ; блокируем работающий поток (даже если мы попали не туда, и он ; прокрутит ещё одну итерацию, это не страшно, дальше заблокируется, у нас есть запас) test edx,edx jnz .findjob ; какая-то зараза (другой поток) успела влезть и перехватить работающий поток раньше нас - идём искать новую работу mov edx,eax sub edx,[ecx+@Current] ; кол-во оставшихся итераций cmp edx,1 ; Last-Current <= 1 ? jle .toolate ; да, осталось слишком мало итераций shr edx,1 ; кол-во отбираемых итераций mov ebx,eax sub ebx,edx ; новый @Last cmpxchg [ecx+@Last],ebx ; if (eax = Last) Last = ebx mov [ecx+@Lock],0 ; освобождаем поток jnz .findjob ; оказывается, другой поток успел отхватить работу аж до xchg! inc ebx ; наш новый Current = Last+1 потока, у которого мы отобрали работу mov [edi+@Last],0x80000000 ; Last = MinInt, чтобы другой поток не отжал у нас то, чего ещё у нас пока нет ; (влезая между двумя следующими mov'ами) mov [edi+@Current],ebx mov [edi+@Last],eax ; наш новый Last jmp .start .toolate: mov [ecx+@Lock],0 ; освобождаем поток jmp .findjob ; идём искать новую работу .finish: ; завершение цикла для данного потока И этот вариант мне тоже не до конца нравится из-за длинной блокировки основного потока при перехвате (и SwitchToThread). Более длинной, чем при cmpxchg8b. Но вариант с cmpxchg8b может обломаться при быстрых циклах. Короче, х/з. Ещё что-то придумывать???
А почему нельзя сделать так? Поток 1 увеличивает current каждый раз на 1 и приступает к работе, пока current <= last. Поток 2 хочет украсть данные. Он вычисляет delta = (current - last)/2 Потока 1 и увеличивает current Потока 1 на это значение в атомарном режиме. Далее поток записывает к себе значение current 1-го потока до изменения и last = current + delta. Всё, потоку 2 дальше можно продолжать работу. Условие, при котором можно красть данные у целевого потока: last - current > 1. Так что ворующему потоку можно быстро пробежаться по массиву задач потоков и выбрать целевой поток тот, у которого разница будет максимальной.
Проблема в том, что мы не знаем, в каком месте Поток1 будет исполняться, когда Поток2 увеличит его Current на Delta. Например, это может произойти между чтением и записью Current'а Потоком1. Даже если Поток1 делает inc [Current], он же делает это не атомарно, а эта инструкция состоит из нескольких микроопераций вроде temp=Current, temp++, Current=temp. И если наш lock сработает в момент между temp=Current и Current=temp, произойдёт вот что (допустим, Current = 10, Last = 20): 1. Поток1: temp=Current (10) 2. Поток1: temp++ (11) 3. Поток2: lock Current+=Delta (15) 4. Поток1: Current=temp (11) Ooops! И это не выдумки, я специально протестил, что будет, если 2 потока будут делать inc X параллельно по 1 млрд раз, но один из них будет с lock'ом, а второй – без. Результат порядка 1.9 млрд. Это, конечно, несравнимо больше, чем когда они оба делают это без lock'а (там вообще результат ≈ 1.001 ... 1.01 млрд), но до 2 млрд всё же не дотягивает (как и ожидалось, в принципе). В общем, у меня появилась одна идейка. Надо просто в Lock записывать не 1 и 0, а изначально записать 0x7FFFFFFF, а потом заносить новый Last, но уменьшенный на 1. А работающий в цикле поток будет останавливаться не при любом ненулевом значении, а только когда Current дойдёт до Lock (ну или превысит его). Т.е. он почти никогда не будет подвисать. Надо только это реализовать сначала, а то каждый раз возникает очередная "эврика", а в ходе реализации какой-нибудь нюанс появляется. Но тут вроде всё должно получиться
Итак, новый вариант... Только в Lock мы будем записывать новый Last, не уменьшая его на 1. Работающий поток: Код (ASM): ; THREAD_UNLOCKED = 0x7FFFFFFF - значение, при котором поток разблокирован (MaxInt) ; esi = номер текущего потока, начиная с 0. mov ebp,[ForProc] ; ebp = адрес основной процедуры цикла lea edi,[esi+esi] lea [edi*8+ThreadData] ; edi = ThreadData + esi*16 mov ebx,[edi+@Current] cmp ebx,[edi+@Last] ; Current <= Last ? jle .start ; да, стартуем jmp .finish ; нет, это пустой цикл, сразу выходим .loop: mov [edi+@Current],ebx ; Current++ (mov атомарен, lock не нужен) .start: stdcall ebp, ebx, esi ; вызов основной функции цикла (с 2 параметрами: номером итерации и потока) inc ebx ; Current+1 cmp ebx,[edi+@Lock] ; Current+1 <= Lock ? jle .continue ; да, продолжаем ; делаем паузу, если достигли значение Lock (spin-цикл ожидания разблокировки) .pause: mov eax,16 @@: pause cmp dword [edi+@Lock],THREAD_UNLOCKED je .continue ; выходим из spin-цикла dec eax jnz @B invoke SwitchToThread ; раз в 16 итераций переключаем контекст, т.к. при таком большом числе ; циклов есть немалая вероятность, что перехватывающий поток находится на том же ядре jmp .pause ; продолжаем цикл .continue: cmp ebx,[edi+@Last] ; Current+1 <= Last ? jle .loop ; да, продолжаем цикл .findjob: ; тут мы переходим в режим перехватчика и начинаем искать незавершённый поток, ; у которого есть необработанные итерации (не считая текущей, которую он обрабатывает) Перехватчик: Код (ASM): ; ecx = номер потока с наибольшей разницей Last-Cur (если у всех потоков эта разница <= 1 ; завершаем работу, не доходя до этого места в коде) ; ebx = Last этого потока add ecx,ecx lea ecx,[ecx*8+ThreadData] ; ecx = ThreadData + ecx*16 mov eax,ebx mov edx,ebx sub eax,[ecx+@Current] shr eax,1 ; (Last - Current) / 2 sub edx,eax ; edx = Last - eax = новый Lock, он же новый Last mov eax,THREAD_UNLOCKED lock cmpxchg [ecx+@Lock],edx ; if (Lock == THREAD_UNLOCKED) { Lock = edx ; zf = 1; } else { eax = Lock; zf = 0; } jnz .findjob ; какая-то зараза (другой поток) успела влезть и перехватить работающий поток раньше нас, идём искать новую работу cmp [ecx+@Current],edx jge .toolate ; поток уже дошёл до итерации Lock (ситуация Current = Lock = новый Last нам тоже не подходит) mov eax,ebx cmpxchg [ecx+@Last],edx ; if (eax == Last) { Last = edx; } mov [ecx+@Lock],THREAD_UNLOCKED ; освобождаем поток jnz .findjob ; оказывается, поток закончил работу и взял итерации у кого-то другого, ; либо другой поток успел отжать работу у нашего подопечного ещё до lock cmpxchg! inc edx ; наш новый Current = новый Last потока, у которого мы отобрали работу + 1 mov [edi+@Last],0x80000000 ; наш Last = MinInt, чтобы другой поток не отжал у нас то, чего ещё у нас пока нет ; (влезая между двумя следующими mov'ами) mov [edi+@Current],edx mov [edi+@Last],eax jmp .start .toolate: mov [ecx+@Lock],THREAD_UNLOCKED ; освобождаем поток jmp .findjob ; идём искать новую работу .finish: ; завершение цикла для данного потока Осталось теперь проверить на практике