В данной статье разберем простые примеры работы с потоками. Статья основана на многочисленных статьях других блогеров и официальной документации. Разбирался сам, для закрепления написал этот небольшой пост.
Что такое потоки? Зачем нужны потоки и как это работает?
Понятие потоков в Delphi перекочевало из Windows. Очень хорошо об этом написано в книгах Рихтера и Калверта. Но если своими словами и по простому – поток это участок кода, который выполняется параллельно.
Потоки нужны для параллельной работы разных задач приложения. Часто приложение выполняет продолжительные во времени операции. Это как минимум занимает главный поток приложения, который обрабатывает его GUI, скажем. То есть в этот момент нельзя передвинуть форму приложения, нельзя нажать на кнопку. Нельзя практически ничего, пока приложение не закончит начатую задачу.
В Windows работает механизм процессов и потоков. Процесс, согласно Чарльзу Калверту, подобен булеву выражению, он либо есть, либо нет. Потоки же выполняют всю основную работу.
Согласно Чарльзу Калверту это подобно игровому колесу в казино. Допустим оно поделено на 360 частей-секторов. Эти части и есть, образно, потоки. В каждый момент времени какая-то из частей пролетает под стрелкой, которая и укажет в конце выиграли вы или нет. Но выигрыш тут ни при чем. Нас интересует сам механизм. Аналогия в том, что в каждый момент времени одному из потоков выдается квант времени на то, чтобы он выполнил часть задачи. И все это происходит настолько быстро, что человек не успевает замечать. Вот так это работает.
Как создать поток (TThread)?
Теперь посмотрим как это работает в Delphi. Для простоты будем работать с обычными VCL приложениями.
Для начала объявим класс потока в секции type
1 2 3 4 5 6 |
... TOneMoreThread=class(tthread) protected procedure execute; override; end; ... |
Обратите внимание на процедуру execute. Это и есть та процедура, в которой выполняется основной код потока.
Далее нам нужно прописать реализацию процедуры execute нашего потока.
1 2 3 4 5 6 7 |
procedure TOneMoreThread.execute; var code:LongInt; begin inherited; //..some code here end; |
Ну и последний шаг – создать экземпляр класса потока и где-то его запустить. В самом простом варианте это будет выглядеть примерно так. Код ниже можно поместить в обработчик кнопки. По нажатию поток запустится.
1 2 3 4 |
... oneMoreThread := TOneMoreThread.Create(false); // << false означает авто запуск потока oneMoreThread.FreeOnTerminate := true; // << Уничтожение после выполнения ... |
Но это в самом простом варианте, как правило потоки мало мальски настраивают. Например так.
1 2 3 4 5 |
oneMoreThread := TOneMoreThread.Create(true); // <<true означает ручной запуск потока oneMoreThread.FreeOnTerminate := true; // <<Экземпляр должен само уничтожиться после выполнения oneMoreThread.Priority:=tpNormal; // <<Выставляем приоритет потока oneMoreThread.resume; // << непосредственно ручной запуск потока |
Синхронизация потоков. Зачем и как?
Здесь тоже все относительно просто. Есть общий ресурс – компонент, переменная, что угодно, с чем работает один из потоков. И как так организовать, чтобы их действия не перемешивались как в том примере с колесом рулетки, а они могли работать последовательно? Скажем сначала отработал один поток, потом отработал другой поток и так далее. Для этого и существует синхронизация. Процедура, которая синхронизирует так и называется synchronize. В документации существует аж 4 перегруженных варианта этой процедуры
1 2 3 4 |
procedure Synchronize(AMethod: TThreadMethod); overload; procedure Synchronize(AThreadProc: TThreadProcedure); overload; class procedure Synchronize(const AThread: TThread; AMethod: TThreadMethod); overload; static; class procedure Synchronize(const AThread: TThread; AThreadProc: TThreadProcedure); overload; static; |
Пример синхронизации потоков
Создадим простейший пример. Создадим приложение VCL. На нём разместим TMemo и кнопку TButton.
Создадим сначала 2 потока 1 класса – поэкспериментируем с synchronize процедурой. А потом создадим ещё один поток другого класса и поэкспериментируем с функцией WaitFor
Итак, для начала создадим класс потока для первых двух потоков. В секции определения типов напишем
1 2 3 4 5 6 7 |
TSomeThread = class(tthread) private s: string; procedure addstr; protected procedure execute; override; end; |
Далее напишем обработчики. Для Execute
1 2 3 4 5 6 |
procedure TSomeThread.execute; begin inherited; synchronize(addstr); // Вызов метода с синхронизацией //addstr; // Вызов метода без синхронизации end; |
Для addstr
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
procedure TSomeThread.addstr; begin form.memo.lines.add(s); sleep(500); form.memo.lines.add(s); sleep(500); form.memo.lines.add(s); sleep(500); form.memo.lines.add(s); sleep(500); form.memo.lines.add(s); end; |
На кнопке разместим следующий код
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
procedure TForm1.Button1Click(Sender: TObject); var code:LongInt; begin firstThread := TSomeThread.create(true); firstThread.freeonterminate := true; firstThread.s := '1 thread'; firstThread.priority := tpNormal; secondThread := TSomeThread.create(true); secondThread.freeonterminate := true; secondThread.s := '2 thread'; secondThread.priority := tpNormal; firstThread.resume; secondThread.resume; end; |
Тестируем режим без синхронизации
Для этого в коде поправим следующее
1 2 3 4 5 6 |
procedure TSomeThread.execute; begin inherited; // synchronize(addstr); // Вызов метода с синхронизацией addstr; // Вызов метода без синхронизации end; |
Видно, что потоки захватывали TMemo в случайном порядке. Для многих задач это не есть хорошо. Поэтому посмотрим как это все работает в режиме синхронизации.
Тестируем в режиме синхронизации
Для этого в коде поправим следующее
1 2 3 4 5 6 |
procedure TSomeThread.execute; begin inherited; synchronize(addstr); // Вызов метода с синхронизацией // addstr; // Вызов метода без синхронизации end; |
Теперь, как видно из рисунка выше – потоки пользовались компонентом TMemo по очереди. Очередность можно настраивать через приоритеты (свойство Priority). Что касается видов приоритетов, то можно назначать следующие
1 2 3 4 5 6 7 |
tpidle Работает, когда система простаивает (фоновый режим) tplowest Нижайший tplower Низкий tpnormal Нормальный tphigher Высокий tphighest Высочайший tptimecritical Критический |
Об этом можно узнать, если заглянуть в System.Classes и поиском пройтись скажем по ключевому слову tplower. Вот что поиск выдал у меня
1 2 |
TThreadPriority = (tpIdle, tpLowest, tpLower, tpNormal, tpHigher, tpHighest, tpTimeCritical) |
Почему об этом говорю – во встроенной справке Delphi XE8 не нашел этого, поэтому полез в System.Classes.
Метод потока WaitFor
Нигде толкового описания не нашел, поэтому пришлось собирать все по кусочками и проводить мини-эксперименты.
WaitFor это метод, который говорит одному потоку дождаться, пока не закончит выполнение другой поток. Но есть тонкости – если применять этот метод, то нужно
1. У того потока, которого мы будем дожидаться, нужно отключить свойство FirstThread.freeonterminate := true; Иначе функции WaitFor нечего будет проверять – поток уничтожится, и поток который ждет не сможет дождаться.
2. Второе следует из первого. Уничтожением придется заниматься самостоятельно.
Ну и ещё один момент, насколько я понял для применения этого метода потоки должны быть из разных классов. Возможно ошибаюсь, пока не провел достаточно экспериментов, но похоже, что это так.
Итак, в ButtonClick в нашей программе закомментируем у 1 и 2 потоков FirstThread.freeonterminate := true;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
procedure TForm.ButtonClick(Sender: TObject); var code:LongInt; begin firstThread := TSomeThread.create(true); //firstThread.freeonterminate := true; firstThread.s := '1 thread'; firstThread.priority := tpNormal; secondThread := TSomeThread.create(true); //SecondThread.freeonterminate := true; secondThread.s := '2 thread'; secondThread.priority := tpNormal; end; |
Добавим ещё один класс потоков в нашу программу. Этот класс и будет ждать пока не выполнятся экземпляры других классов.
1 2 3 4 5 6 7 |
TOneMoreThread=class(tthread) private s: string; procedure OneMoreAddStr; protected procedure execute; override; end; |
Теперь пропишем методы
Execute
1 2 3 4 5 6 7 8 9 |
procedure TOneMoreThread.execute; begin inherited; firstThread.WaitFor; secondThread.WaitFor; synchronize(OneMoreAddStr); firstThread.Free; // <<Уничтожаем экземпляры в ручную secondThread.Free; //// <<Уничтожаем экземпляры в ручную end; |
OneMoreAddStr
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
procedure TOneMoreThread.OneMoreAddStr; begin form.memo.lines.add(s); sleep(500); form1.memo.lines.add(s); sleep(500); form1.memo.lines.add(s); sleep(500); form.memo.lines.add(s); sleep(500); form.memo.lines.add(s); end; |
Теперь обработаем ButtonClick
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
procedure TForm.Button1Click(Sender: TObject); var code:LongInt; begin firstThread := TSomeThread.create(true); //firstThread.freeonterminate := true; // Commented for Wait for firstThread.s := '1 thread'; firstThread.priority := tpNormal; secondThread := TSomeThread.create(true); // secondThread.freeonterminate := true; // Commented for Wait for secondThread.s := '2 thread'; secondThread.priority := tpNormal; thirdThread:=TOneMoreThread.Create(true); thirdThread.FreeOnTerminate:=true; thirdThread.s:='3 thread'; thirdThread.Priority:=tpHighest; firstThread.resume; secondThread.resume; thirdThread.Resume; end; |
Обратите внимание ! Функция ThirdThread обладает приоритетом tpHighest. Это означало бы в текущих условиях, что она была бы выполнена первой! Но мы попросили 3 поток ждать, посмотрим что будет в этом случае!!!
Результат работы WaitFor
А вот что было бы, если мы не пользовались функцией WaitFor. Закоментируем её!
1 2 3 4 5 6 7 8 9 10 11 |
procedure TOneMoreThread.execute; begin inherited; // FirstThread.WaitFor; // SecondThread.WaitFor; synchronize(OneMoreAddStr); firstThread.Free; // <<Уничтожаем экземпляры в ручную secondThread.Free; //// <<Уничтожаем экземпляры в ручную end; |
Также, при желании, можно раскомментировать Freeonterminate у первого и второго потоков. Теперь посмотрим что будет дальше!
То есть потоки отработали в том приоритете, который мы и задали. Сначала 3-й, обладающий приоритетом tpHighest. Потом 2 и 1 в произвольном порядке, так как у них приоритет одинаковый.
Думаю в общих основные моменты понятны. Мы разобрали следующее
-Как создать, настроить и запустить потоки?
-Как синхронизировать потоки?
-Как использовать функцию WaitFor?
Продолжим экспериментировать в следующих постах!!!