Наши проекты:
Журнал · Discuz!ML · Wiki · DRKB · Помощь проекту |
||
ПРАВИЛА | FAQ | Помощь | Поиск | Участники | Календарь | Избранное | RSS |
[3.145.46.18] |
|
Страницы: (5) 1 2 [3] 4 5 все ( Перейти к последнему сообщению ) |
Сообщ.
#31
,
|
|
|
Цитата Jin X @ Единственным доводом в этом пользу может служить разве что "вдруг поток освободиться в Close между проверкой Displaying и PostThreadMessage, и тогда я обращусь к несуществующему Thread.Handle (при вызове PostThreadMessage)?" Но и тут есть простое решение: в этом случае поток может сделать сам себе FreeOnTerminate := False, а Close сделать Thread.Free. Простое решение - это использование критической секции (или ее самописного аналога). В рабочем потоке заключаем в крит.секцию изменение статуса Displaying после показа MessageBox, а в методе Close - проверку статуса и посылку сообщения PostThreadMessage. В этом случае все "вдруг" исключаются и можно использовать FreeOnTerminate:=true. Цитата Jin X @ Может ли возникнуть ситуация, что создастся поток с таким же Id и я закрою что-то другое? Каким образом эти Id генерируются? Подробности мне не известны, но думаю, что на коротких интервалах переназначение ID невозможно. Ведь по любому между получением ID процесса или потока и его открытием проходит какое-то время, за которое этот процесс или поток может успеть закрыться. Поэтому в ОС должен использоваться некий механизм назначения ID, исключающий их повтор на коротких интервалах времени. PS: Однако, если есть возможность перестраховаться (например, с использованием крит.секции), то лучше это сделать, "кабы чего не вышло" |
Сообщ.
#32
,
|
|
|
Цитата leo @ К Diaplaying всё равно никто доступа не имеет, поэтому можно и не заключать Простое решение - это использование критической секции (или ее самописного аналога). В рабочем потоке заключаем в крит.секцию изменение статуса Displaying после показа MessageBox, а в методе Close - проверку статуса и посылку сообщения PostThreadMessage. В этом случае все "вдруг" исключаются и можно использовать FreeOnTerminate:=true. Но я всё равно убрал FreeOnTerminate, оказалось, что всё-таки так удобнее делать некоторые вещи . Не получается "забыть" после того, как запустил. Так что спасибо за настойчивость Цитата leo @ У меня есть такой блок:PS: Однако, если есть возможность перестраховаться (например, с использованием крит.секции), то лучше это сделать, "кабы чего не вышло" function TMsgBoxTimeoutNotify.Close(Res: TMsgBoxTimeoutResult = MBTR_BREAKED): Boolean; begin Result := True; if Res = MBTR_DISPLAYING then Res := MBTR_BREAKED; if InterlockedExchange(Integer(FResult), Integer(Res)) = Integer(MBTR_DISPLAYING) then PostThreadMessage(FThread.ThreadID, WM_QUIT, 0, 0); if (FThread <> nil) and (FThread.Handle <> 0) then // Даже если метод Close вызван уже после установки результата потоком, всё равно... begin Result := (WaitForSingleObject(FThread.Handle, FCloseTimeout) <> WAIT_TIMEOUT); // ...ждём реального закрытия окна (обработки PostMessage). DoNotify(mbtOnClose); if not Result then begin FThread.FreeOnTerminate := True; // Если не дождались, отправляем его в свободное плавание. FThread := nil end end end; Благо TThread не обнуляет Handle и ThreadID при завершении работы потока. Но это уже придирки, конечно, т.к. я полагаю, что иначе добрая половина существующих программ, работающих с WaitForSingleObject и иже с ним, были бы "условно ненадёжными" Добавлено А вот так сделан вывод сообщения: function TMsgBoxTimeoutNotify.ShowEx(hWnd: THandle; const Text, Title: String; Flags, Timeout: DWord; Ntf: TMsgBoxTimeoutNotifyRec; const Id: String = ''; LangId: Word = 0): Boolean; begin Result := ReadyToShow(Flags); if Result then begin FreeAndNil(FThread); FResult := MBTR_DISPLAYING; FParams.hWnd := hWnd; FParams.Text := Text; FParams.Title := Title; FParams.Flags := Flags and not MBTF_FLAGS_MASK; FParams.Timeout := Timeout; FParams.LangId := LangId; FId := Id; if FId = '' then FId := GenerateId; SetNotify(Ntf); // От греха подальше пусть будет SetNotify, а не прямое присвоение :) FThread := TMsgBoxTimeoutNotifyThread.Create(True); with TMsgBoxTimeoutNotifyThread(FThread) do begin MsgBox := Self; DoNotify(mbtOnShow); Resume end end end; procedure TMsgBoxTimeoutNotify.SetNotify(Ntf: TMsgBoxTimeoutNotifyRec); begin while InterlockedExchange(NotifyDenyToChange, 1) = 1 do Sleep(0); FNotify := Ntf; NotifyDenyToChange := 0; end; procedure TMsgBoxTimeoutNotifyThread.Execute; var Res: TMsgBoxTimeoutResult; begin with MsgBox do with FParams do begin Res := MessageBoxTimeout(hWnd, PChar(Text), PChar(Title), Flags, LangId, Timeout); if not FreeOnTerminate then // Поток не в свободном плавании (см. метод Close)? if InterlockedCompareExchange(Pointer(FResult), Pointer(Res), Pointer(MBTR_DISPLAYING)) = Pointer(MBTR_DISPLAYING) then // Реузльтат будет другим (не MBTR_DISPLAYING), если окно закрыто методом Close DoNotify(mbtOnClose) end end; procedure TMsgBoxTimeout.BeforeDestruction; begin Close; FThread.Free end; Весь код пришлю позже, отладить всё надо |
Сообщ.
#33
,
|
|
|
С простыми переменными < размера регистра нет смысла юзать interlocked, чтобы проверить их значение. Чтение-запись в них и так атомарна.
|
Сообщ.
#34
,
|
|
|
Fr0sT, все атомарные (locked) операции и так проводятся над данными размером с регистр (а для 64 и 128 бит можно юзать MMX и SSE).
Внутри процессора производятся такие же операции, как и в обычной программе. Меняем EAX и память [M32]: Temp=EAX; EAX=[M32]; [M32]=Temp. И вот где-то по пути в работу может вмешаться другое ядро, тогда получится: Temp1=EAX1 // Ядро1 Temp2=EAX2 // Ядро2 EAX1=[M32] // Ядро1 EAX2=[M32] // Ядро2 [M32]=Temp1 // Ядро1 [M32]=Temp2 // Ядро2 На примере: Если: EAX1=10 EAX2=20 [M32]=30 Тогда: Temp1=EAX1 // Temp1=10 Temp2=EAX2 // Temp2=20 EAX1=[M32] // EAX1=30 EAX2=[M32] // EAX2=30 [M32]=Temp1 // [M32]=10 [M32]=Temp2 // [M32]=20 В итоге получили: EAX1=30 EAX2=30 [M32]=20 А если это делать по очереди (с locked'ами), то будет: Ядро1: Temp1=EAX1 // Temp1=10 EAX1=[M32] // EAX1=30 [M32]=Temp1 // [M32]=10 Ядро2: Temp2=EAX2 // Temp2=20 EAX2=[M32] // EAX2=10 [M32]=Temp2 // [M32]=20 В итоге получили: EAX1=30 EAX2=10 [M32]=20 Аналогично с увеличением на 1: EAX=[M32]; EAX++; [M32]=EAX. Пусть: [M32]=10 Тогда: EAX1=[M32] // EAX1=10 EAX2=[M32] // EAX2=10 EAX1++ // EAX1=11 EAX2++ // EAX2=11 [M32]=EAX1 // [M32]=11 [M32]=EAX2 // [M32]=11 (на самом деле там используется xadd, который немного по-другому работает, но для иллюстрации такой пример сгодится). Да и просто, пойдём от простой логики: если бы это было не нужно, не было бы функций InterlockedCompare, InterlockedIncrement и пр. |
Сообщ.
#35
,
|
|
|
Я не говорю про обмен - на уровне Паскаля это уже не одна операция. Я говорю о чтении или записи. Тогда (чтение тривиально, рассмотрим запись):
[M_src1]=10 [M_src2]=20 [M32]=30 Тогда: EAX1=[M_src1] // EAX1=30 EAX2=[M_src2] // EAX2=20 [M32]=EAX1 // [M32]=10 [M32]=EAX2 // [M32]=20 В итоге получили: EAX1=10 EAX2=20 [M32]=30 Добавлено Цитата Jin X @ все атомарные (locked) операции и так проводятся над данными размером с регистр (а для 64 и 128 бит можно юзать MMX и SSE) Это к чему? Я знаю. Добавлено Цитата Jin X @ Да и просто, пойдём от простой логики: если бы это было не нужно, не было бы функций InterlockedCompare, InterlockedIncrement и пр. С InterlockedIncrement очевидно (это опять же 2 операции), а вот InterlockedCompare я не очень понимаю зачем нужно. Разве что сразу получить прежнее значение. |
Сообщ.
#36
,
|
|
|
Цитата Fr0sT @ С точки зрения ASM-кода это как раз 1 операция INC [M32].С InterlockedIncrement очевидно (это опять же 2 операции) Цитата Fr0sT @ С точки зрения паскаля это тоже 1 операция, он так же генерит XCHG [M32],EAX.Я не говорю про обмен - на уровне Паскаля это уже не одна операция. Цитата Fr0sT @ Да, как минимум для этого. Как уже сказал, это тоже одна операция XCHG [M32],EAX.InterlockedCompare я не очень понимаю зачем нужно. Разве что сразу получить прежнее значение. InterlockedCompareExchange = CMPXCHG [M32],EAX. Там везде 1 операция. Вопрос не в количестве инструкций ассемблера, а в кол-ве операций ВНУТРИ процессора. Просто чтение и просто запись (A := 10; он же MOV [M32],EAX) - согласен, смысла нет (и то, это просто логическое предположение, х/з как там внутри устроено). Но где идёт изменение хранящегося внутри значения, обмен (со сравнением или без) - там да. Добавлено Цитата Fr0sT @ Это к тому, что ты пишешь:Это к чему? Я знаю. Цитата Fr0sT @ С простыми переменными < размера регистра нет смысла юзать interlocked, чтобы проверить их значение. Чтение-запись в них и так атомарна. Добавлено Я даже больше скажу: ДВЕ операции (не внутри процессора, а в количестве ассемблерных инструкций) ты залочить никак не сможешь. Ну разве что сделать spinlock, но это уже псевдо-лок. |
Сообщ.
#37
,
|
|
|
Цитата Jin X @ С точки зрения ASM-кода это как раз 1 операция INC [M32]. Если с возвратом старого значения - две. Если старое не нужно - то одна, и тогда и InterlockedIncrement не особо нужен. Цитата Jin X @ С точки зрения паскаля это тоже 1 операция, он так же генерит XCHG [M32],EAX. Попробуй сгенерить кодом на Паскале такой asm я имею в виду, без всяких функций. Компилятор не настолько умный, к сожалению. А стало быть, это целых три операции. Цитата Jin X @ Вопрос не в количестве инструкций ассемблера, а в кол-ве операций ВНУТРИ процессора. Я не большой знаток устройства процов, но мне кажется, что инструкция, хотя бы снаружи - вне зависимости от того, как реализовано унутрях - атомарна. Иначе любому менеджеру потоков/процессов пришлось бы лочить каждую инструкцию, чтобы не дай бог не запустить другой поток, пока выполняется инструкция прежнего. Цитата Jin X @ Это к тому, что ты пишешь: Все равно не понял, зачем эта фраза, ну да ладно. |
Сообщ.
#38
,
|
|
|
Цитата Fr0sT @ Зачем что-то утверждать, если не знаешь точно? Если с возвратом старого значения - две. Если старое не нужно - то одна, и тогда и InterlockedIncrement не особо нужен. InterlockedIncrement выполняет 1 операцию (инструкцию CPU): xadd !!! Которая прибавляет некоторое значение, одновременно считывая старое. Смотри картинку... Прикреплённый файл2016_09_16_15_30_29_Unit1.pas.png (13,35 Кбайт, скачиваний: 422) Добавлено Повторюсь: Цитата Jin X @ Все Interlocked-функции работают с 1 операцией! За исключением inline-функций C++, которые организуют spinlock типаДВЕ операции (не внутри процессора, а в количестве ассемблерных инструкций) ты залочить никак не сможешь. Ну разве что сделать spinlock, но это уже псевдо-лок. FORCEINLINE LONGLONG _InlineInterlockedExchange64 ( _Inout_ _Interlocked_operand_ LONGLONG volatile *Target, _In_ LONGLONG Value ) { LONGLONG Old; do { Old = *Target; } while (InterlockedCompareExchange64(Target, Value, Old) != Old); return Old; } #define InterlockedExchange64 _InlineInterlockedExchange64 FORCEINLINE LONGLONG _InlineInterlockedExchangeAdd64 ( _Inout_ _Interlocked_operand_ LONGLONG volatile *Addend, _In_ LONGLONG Value ) { LONGLONG Old; do { Old = *Addend; } while (InterlockedCompareExchange64(Addend, Old + Value, Old) != Old); return Old; } #define InterlockedExchangeAdd64 _InlineInterlockedExchangeAdd64 |
Сообщ.
#39
,
|
|
|
Бррр. Мы о разном говорим. Я - о том, что без interlocked функций определенные действия на Паскале будут транслироваться в 2 и более инструкций (обмен, сравнение-и-присвоение, увеличение-и-возврат-старого и т.д.). Которые, разумеется, небезопасны с т.з. мультитредовости. Однако если операция на Паскале транслируется в одну инструкцию (напр., Inc без получения старого значения), то необходимости в interlocked нет.
|
Сообщ.
#40
,
|
|
|
Цитата Fr0sT @ А, вон ты о чём!Бррр. Мы о разном говорим. Я - о том, что без interlocked функций определенные действия на Паскале будут транслироваться в 2 и более инструкций (обмен, сравнение-и-присвоение, увеличение-и-возврат-старого и т.д.). Которые, разумеется, небезопасны с т.з. мультитредовости. Однако если операция на Паскале транслируется в одну инструкцию (напр., Inc без получения старого значения), то необходимости в interlocked нет. Но тут ты тоже не прав . Посмотри ещё раз скриншот - там перед xadd стоит lock. Не просто так. Как я уже говорил, второе ядро может вклиниться в работу во время (внутренних!!!) вычислений первого и будет как в моём сообщении #34, где оба одновременно пытаются изменить значение, а оно меняется только на 1 вместо 2. Я накатал "по бырому" демку сего процесса. Вот основной код (см. TMyThread.Execute): unit Main; interface uses Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls, ExtCtrls, ComCtrls; type TForm1 = class(TForm) Button1: TButton; Edit1: TEdit; Label1: TLabel; CheckBox1: TCheckBox; TrackBar1: TTrackBar; Label2: TLabel; Button2: TButton; Edit2: TEdit; procedure Button1Click(Sender: TObject); procedure FormClose(Sender: TObject; var Action: TCloseAction); procedure CheckBox1Click(Sender: TObject); procedure TrackBar1Change(Sender: TObject); procedure Button2Click(Sender: TObject); private { Private declarations } public { Public declarations } end; TMyThread = class(TThread) N: Integer; procedure Execute; override; end; var Form1: TForm1; A, B: TMyThread; X: Integer; D: Integer = 0; Run: Boolean = False; UseLock: Boolean = False; implementation {$R *.dfm} procedure TMyThread.Execute; begin N := 0; repeat asm mov eax,Self cmp UseLock,0 je @NoLock lock inc [X] inc [eax+N] jmp @End @NoLock: inc [X] inc [eax+N] @End: end; Sleep(D) until Terminated end; procedure TForm1.Button1Click(Sender: TObject); begin if Run then begin A.Terminate; B.Terminate; Exit end; Run := True; X := 0; A := TMyThread.Create(False); B := TMyThread.Create(False); repeat Edit1.Text := IntToStr((A.N+B.N)-X); Application.ProcessMessages; Sleep(1) until A.Terminated; A.Free; B.Free; Run := False end; procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction); begin if Run then Button1.Click end; procedure TForm1.CheckBox1Click(Sender: TObject); begin UseLock := CheckBox1.Checked end; procedure TForm1.TrackBar1Change(Sender: TObject); begin D := TrackBar1.Position; Edit2.Text := IntToStr(D) end; procedure TForm1.Button2Click(Sender: TObject); begin X := 0; A.N := 0; B.N := 0 end; end. Суть в том, что два потока увеличивают внешнюю переменную X на 1 (с lock'ом и без lock'а) ассемблером (т.е. это однозначно единая операция) и внутреннюю переменную на 1 (каждый свою A.N и B.N). И в процессе их работы мы постоянно выводим на экран "ошибку" (A.N+B.N) - X. Если галочки "LOCK" нет, это число постоянно увеличивается. Если есть, то рост "ошибки" останавливается. Угадай почему? Если двигать ползунок Sleep (который будет увеличивать задержку между изменением значений в миллисекундах) вправо, то скорость роста "ошибки" при выключенном "LOCK" замедляется, что говорит о том, что в нормальных условиях таких совпадений не возникает (на больших значениях ждать приходится очень долго), но хороший программист всё же должен учитывать такую возможность (даже если она 1 на триллион или меньше, потому что, как говорят, "на грех и грабли стреляют") При Sleep=0 и галочке "LOCK" показатель дёргается - это нормально, т.к. чтение значений A.N, B.N и X (а также увеличение X и N в каждом из потоком) происходит не атомарно. Прикреплённый файлThreadLockDemo.zip (206,75 Кбайт, скачиваний: 62) |
Сообщ.
#41
,
|
|
|
Цитата Fr0sT @ Я не большой знаток устройства процов, но мне кажется, что инструкция, хотя бы снаружи - вне зависимости от того, как реализовано унутрях - атомарна. Иначе любому менеджеру потоков/процессов пришлось бы лочить каждую инструкцию, чтобы не дай бог не запустить другой поток, пока выполняется инструкция прежнего. Цитата Fr0sT @ Однако если операция на Паскале транслируется в одну инструкцию (напр., Inc без получения старого значения), то необходимости в interlocked нет. Любая инструкция процессора (кроме строковых операций с rep) атомарна в плане прерывания\исключения, т.е. либо она выполняется полностью, либо отменяется полностью. Поэтому в однопроцессорной системе инструкции, выполняющие чтение и запись в память, также являются атомарными и в плане чтения\записи (если не рассматривать специальные экзотические случаи, когда память м.б. изменена не самим процессором, а "внешними агентами" - видеокартой, контроллером DMA и т.п.). Но в многопроцессорных (многоядерных) системах x86 эти инструкции в общем случае не являются атомарными в плане чтения\записи, т.к. между (микро)операциями чтения и записи, память может быть изменена другим логическим процессором. Например, если два потока на разных логических процессорах делают Inc одной и той же переменной, то она в итоге может измениться как на 2, так и на 1 (если оба потока одновременно прочитают одно и тоже значение). Если же выполняется lock inc, то гарантируется, что между чтением и записью ни один другой поток не сможет не только изменить значение переменной, но и даже прочитать ее ("неконсистентное" значение). Поэтому два lock inc в разных потоках гарантированно увеличивают значение переменной на 2, а не на 1-2, как при простом Inc. |
Сообщ.
#42
,
|
|
|
Цитата Fr0sT @ Interlocked-функции, зашитые в DLL, сделаны не для паскаля/Delphi. И даже не для C++, т.к. проще сделать в C++ и в Delphi такую функцию (с xadd), чем пихать её в DLL. А по поводу возврата старого значения, так почему бы не сделать так:Если с возвратом старого значения - две. Если старое не нужно - то одна, и тогда и InterlockedIncrement не особо нужен. X := A; // Получаем старое значение A := A + 1; // Увеличиваем на 1 Цитата leo @ Добавлю, что под микрооперацией подразумевается внутреняя операция в процессоре (ядре), т.е. Temp = [M32]; Temp++; [M32] = Temp.Но в многопроцессорных (многоядерных) системах x86 эти инструкции в общем случае не являются атомарными в плане чтения\записи, т.к. между (микро)операциями чтения и записи, память может быть изменена другим логическим процессором. Например, если два потока на разных логических процессорах делают Inc одной и той же переменной, то она в итоге может измениться как на 2, так и на 1 (если оба потока одновременно прочитают одно и тоже значение). Собственно, реальное поведение демонстрирует приведённая выше программа. А вот ещё одна. Использование CMPXCHG с LOCK'ом и без для SpinLock'ов (организации критических секций - участков кода, доступ к которому должен иметь только 1 поток). Прикреплённый файлThreadSpinLockDemo.zip (210,96 Кбайт, скачиваний: 66) Добавлено p.s. Справедливости ради хочу сказать, что есть 1 инструкция, которые всегда делает Bus-Lock (т.е. её не нужно предварять префиксом LOCK) - это XCHG (не CMPXCHP, не CMPXCHG8B и пр, а обычная XCHG). |
Сообщ.
#43
,
|
|
|
Вот из документации Intel:
Цитата Немного странная конструкция, особенно на ассемблере, её можно упростить.5 Examples Two examples of generic spin-wait loops are given here. 5.1 A Sample Spin-wait Lock get_lock: mov eax, 1 xchg eax, A ; Try to get lock cmp eax, 0 ; Test if successful jne spin_loop critical_section: <critical section code> mov A, 0 ; Release lock jmp continue spin_loop: pause ; Short delay cmp 0, A ; Check if lock is free jne spin_loop jmp get_lock continue: 5.2 Another Spin-wait Sample // Come here if we didn’t get the lock on the first try. for (;;) { for (int i=0; i < SPIN_COUNT; i++) { if ( (i & SPIN_MASK) == 0 && m_dwLock == UNLOCKED && InterlockedExchange( &m_dwLock, LOCKED )== UNLOCKED) return; #ifdef _X86_ _mm_pause(); #endif } SleepForSleepCount( cSleeps++ ); } Инструкция pause, как пишут, повышает производительность процессора за счёт оптимизация работы процессора с памятью и уменьшения мощности процессора (потребления энергии). Однако на загрузку процессора это не влияет. (Прикол, что эту инструкцию можно запустить даже на 8086, т.к. это, по сути rep nop ) Кстати, leo, посоветоваться с тобой хочу. Вот старт SpinLock'а: procedure StartSpinLock(var X: Integer); begin while InterlockedExchange(X, 1) <> 0 do begin asm pause end; {$IFNDEF NoSleepSpinloops} Sleep(1) {$ENDIF} end end; Так вообще корректно делать? |
Сообщ.
#44
,
|
|
|
Идеально
procedure StartSpinLock(var Key: Integer); var i: Integer; begin i := 0; while InterlockedExchange(Key, 1) <> 0 do begin asm pause end; if HiWord(i) = 0 then Inc(i) else Sleep(1) end end; procedure FinishSpinLock(var Locker: Integer); begin Key := 0 end; |
Сообщ.
#45
,
|
|
|
У меня вот тут вопрос возник: будет ли обычное 32-битное присвоение (mov [X],eax), если переменная не выровнена по границе памяти, атомарным? Т.е. половина переменной находится в одной части памяти, половина - в другой.
|