Path of Exile – Уклонение (Evasion) и Уклон (Dodge). Path of Exile – Уклонение (Evasion) и Уклон (Dodge) Уклонение thread

Многопоточное программирование позволяет разделить представление и обработку информации на несколько «легковесных» процессов (light-weight processes), имеющих общий доступ как к методам различных объектов приложения, так и к их полям. Многопоточность незаменима в тех случаях, когда графический интерфейс должен реагировать на действия пользователя при выполнении определенной обработки информации. Потоки могут взаимодействовать друг с другом через основной «родительский» поток, из которого они стартованы.

В качестве примера можно привести некоторый поток, отвечающий за представление информации в интерфейсе, который ожидает завершения работы другого потока, загружающего файл, и одновременно отображает некоторую анимацию или обновляет прогресс-бар. Кроме того этот поток может остановить загружающий файл поток при нажатии кнопки «Отмена».

Создатели Java предоставили две возможности создания потоков: реализация (implementing) интерфейса Runnable и расширение(extending) класса Thread . Расширение класса - это путь наследования методов и переменных класса родителя. В этом случае можно наследоваться только от одного родительского класса Thread . Данное ограничение внутри Java можно преодолеть реализацией интерфейса Runnable , который является наиболее распространённым способом создания потоков.

Преимущества потоков перед процессами

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

Главный поток

Каждое java приложение имеет хотя бы один выполняющийся поток. Поток, с которого начинается выполнение программы, называется главным. После создания процесса, как правило, JVM начинает выполнение главного потока с метода main(). Затем, по мере необходимости, могут быть запущены дополнительные потоки. Многопоточность - это два и более потоков, выполняющихся одновременно в одной программе. Компьютер с одноядерным процессором может выполнять только один поток, разделяя процессорное время между различными процессами и потоками.

Класс Thread

В классе Thread определены семь перегруженных конструкторов, большое количество методов, предназначенных для работы с потоками, и три константы (приоритеты выполнения потока).

Конструкторы класса Thread

Thread(); Thread(Runnable target); Thread(Runnable target, String name); Thread(String name); Thread(ThreadGroup group, Runnable target); Thread(ThreadGroup group, Runnable target, String name); Thread(ThreadGroup group, String name);

  • target – экземпляр класса реализующего интерфейс Runnable;
  • name – имя создаваемого потока;
  • group – группа к которой относится поток.

Пример создания потока, который входит в группу, реализует интерфейс Runnable и имеет свое уникальное название:

Runnable r = new MyClassRunnable(); ThreadGroup tg = new ThreadGroup(); Thread t = new Thread(tg, r, "myThread");

Группы потоков удобно использовать, когда необходимо одинаково управлять несколькими потоками. Например, несколько потоков выводят данные на печать и необходимо прервать печать всех документов поставленных в очередь. В этом случае удобно применить команду ко всем потокам одновременно, а не к каждому потоку отдельно. Но это можно сделать, если потоки отнесены к одной группе.

Несмотря на то, что главный поток создаётся автоматически, им можно управлять. Для этого необходимо создать объект класса Thread вызовом метода currentThread() .

Методы класса Thread

Наиболее часто используемые методы класса Thread для управления потоками:

  • long getId() - получение идентификатора потока;
  • String getName() - получение имени потока;
  • int getPriority() - получение приоритета потока;
  • State getState() - определение состояния потока;
  • void interrupt() - прерывание выполнения потока;
  • boolean isAlive() - проверка, выполняется ли поток;
  • boolean isDaemon() - проверка, является ли поток «daemon»;
  • void join() - ожидание завершения потока;
  • void join(millis) - ожидание millis милисекунд завершения потока;
  • void notify() - «пробуждение» отдельного потока, ожидающего «сигнала»;
  • void notifyAll() - «пробуждение» всех потоков, ожидающих «сигнала»;
  • void run() - запуск потока, если поток был создан с использованием интерфейса Runnable;
  • void setDaemon(bool) - определение «daemon» потока;
  • void setPriority(int) - определение приоритета потока;
  • void sleep(int) - приостановка потока на заданное время;
  • void start() - запуск потока.
  • void wait() - приостановка потока, пока другой поток не вызовет метод notify();
  • void wait(millis) - приостановка потока на millis милисекунд или пока другой поток не вызовет метод notify();

Жизненный цикл потока

При выполнении программы объект Thread может находиться в одном из четырех основных состояний: «новый», «работоспособный», «неработоспособный» и «пассивный». При создании потока он получает состояние «новый» (NEW) и не выполняется. Для перевода потока из состояния «новый» в «работоспособный» (RUNNABLE) следует выполнить метод start(), вызывающий метод run().

Поток может находиться в одном из состояний, соответствующих элементам статически вложенного перечисления Thread.State:

NEW - поток создан, но еще не запущен;
RUNNABLE - поток выполняется;
BLOCKED - поток блокирован;
WAITING - поток ждет окончания работы другого потока;
TIMED_WAITING - поток некоторое время ждет окончания другого потока;
TERMINATED - поток завершен.

Пример использования Thread

В примере ChickenEgg рассматривается параллельная работа двух потоков (главный поток и поток Egg), в которых идет спор, «что было раньше, яйцо или курица?». Каждый поток высказывает свое мнение после небольшой задержки, формируемой методом ChickenEgg.getTimeSleep(). Побеждает тот поток, который последним говорит свое слово.

Package example; import java.util.Random; class Egg extends Thread { @Override public void run() { for(int i = 0; i < 5; i++) { try { // Приостанавливаем поток sleep(ChickenEgg.getTimeSleep()); System.out.println("Яйцо"); }catch(InterruptedException e){} } } } public class ChickenEgg { public static int getTimeSleep() { final Random random = new Random(); int tm = random.nextInt(1000); if (tm < 10) tm *= 100; else if (tm < 100) tm *= 10; return tm; } public static void main(String args) { Egg egg = new Egg (); // Создание потока System.out.println("Начинаем спор: кто появился первым?"); egg.start(); // Запуск потока for(int i = 0; i < 5; i++) { try { // Приостанавливаем поток Thread.sleep(ChickenEgg.getTimeSleep()); System.out.println("Курица"); }catch(InterruptedException e){} } if(egg.isAlive()) { // Cказало ли яйцо последнее слово? try { // Ждем, пока яйцо закончит высказываться egg.join(); } catch (InterruptedException e){} System.out.println("Первым появилось яйцо!!!"); } else { //если оппонент уже закончил высказываться System.out.println("Первой появилась курица!!!"); } System.out.println("Спор закончен"); } }

Начинаем спор: кто появился первым? Курица Курица Яйцо Курица Яйцо Яйцо Курица Курица Яйцо Яйцо Первым появилось яйцо!!! Спор закончен

Невозможно точно предсказать, какой поток закончит высказываться последним. При следующем запуске «победитель» может измениться. Это происходит вследствии так называемого «асинхронного выполнения кода». Асинхронность обеспечивает независимость выполнения потоков. Или, другими словами, параллельные потоки независимы друг от друга, за исключением случаев, когда бизнес-логика зависимости выполнения потоков определяется предусмотренными для этого средств языка.

Интерфейс Runnable

Интерфейс Runnable содержит только один метод run() :

Interface Runnable { void run(); }

Метод run() выполняется при запуске потока. После определения объекта Runnable он передается в один из конструкторов класса Thread .

Пример класса RunnableExample, реализующего интерфейс Runnable

Package example; class MyThread implements Runnable { Thread thread; MyThread() { thread = new Thread(this, "Дополнительный поток"); System.out.println("Создан дополнительный поток " + thread); thread.start(); } @Override public void run() { try { for (int i = 5; i > 0; i--) { System.out.println("\tдополнительный поток: " + i); Thread.sleep(500); } } catch (InterruptedException e) { System.out.println("\tдополнительный поток прерван"); } System.out.println("\tдополнительный поток завершён"); } } public class RunnableExample { public static void main(String args) { new MyThread(); try { for (int i = 5; i > 0; i--) { System.out.println("Главный поток: " + i); Thread.sleep(1000); } } catch (InterruptedException e) { System.out.println("Главный поток прерван"); } System.out.println("Главный поток завершён"); } }

При выполнении программы в консоль было выведено следующее сообщение.

Создан дополнительный поток Thread[Дополнительный поток,5,main] Главный поток: 5 дополнительный поток: 5 дополнительный поток: 4 Главный поток: 4 дополнительный поток: 3 дополнительный поток: 2 Главный поток: 3 дополнительный поток: 1 дополнительный поток завершён Главный поток: 2 Главный поток: 1 Главный поток завершён

Синхронизация потоков, synchronized

В процессе функционирования потоки часто используют общие ресурсы приложения, определенные вне потока. Если несколько потоков начнут одновременно вносить изменения в общий ресурс, то результаты выполнения программы могут быть непредсказуемыми. Рассмотрим следующий пример:

Package example; class CommonObject { int counter = 0; } class CounterThread implements Runnable { CommonObject res; CounterThread(CommonObject res) { this.res = res; } @Override public void run() { // synchronized(res) { res.counter = 1; for (int i = 1; i < 5; i++){ System.out.printf(""%s" - %d\n", Thread.currentThread().getName(), res.counter); res.counter++; try { Thread.sleep(100); } catch(InterruptedException e){} } // } } } public class SynchronizedThread { public static void main(String args) { CommonObject commonObject= new CommonObject(); for (int i = 1; i < 6; i++) { Thread t; t = new Thread(new CounterThread(commonObject)); t.setName("Поток " + i); t.start(); } } }

В примере определен общий ресурс в виде класса CommonObject, в котором имеется целочисленное поле counter. Данный ресурс используется внутренним классом , создающим поток CounterThread для увеличения в цикле значения counter на единицу. При старте потока полю counter присваивается значение 1. После завершения работы потока значение res.counter должно быть равно 4.

Две строчки кода класса CounterThread закомментированы. О них речь пойдет ниже.

В главном классе программы SynchronizedThread.main запускается пять потоков. То есть, каждый поток должен в цикле увеличить значение res.counter с единицы до четырех; и так пять раз. Но результат работы программы, отображаемый в консоли, будет иным:

"Поток 4" - 1 "Поток 2" - 1 "Поток 1" - 1 "Поток 5" - 1 "Поток 3" - 1 "Поток 2" - 6 "Поток 4" - 7 "Поток 3" - 8 "Поток 5" - 9 "Поток 1" - 10 "Поток 2" - 11 "Поток 4" - 12 "Поток 5" - 13 "Поток 3" - 13 "Поток 1" - 15 "Поток 4" - 16 "Поток 2" - 16 "Поток 3" - 18 "Поток 5" - 18 "Поток 1" - 20

То есть, с общим ресурсов res.counter работают все потоки одновременно, поочередно изменяя значение.

Чтобы избежать подобной ситуации, потоки необходимо синхронизировать. Одним из способов синхронизации потоков связан с использованием ключевого слова synchronized . Оператор synchronized позволяет определить блок кода или метод, который должен быть доступен только одному потоку. Можно использовать synchronized в своих классах определяя синхронизированные методы или блоки. Но нельзя использовать synchronized в переменных или атрибутах в определении класса.

Блокировка на уровне объекта

Блокировать общий ресурс можно на уровне объекта, но нельзя использовать для этих целей примитивные типы. В примере следует удалить строчные комментарии в классе CounterThread, после чего общий ресурс будет блокироваться как только его захватит один из потоков; остальные потоки будут ждать в очереди освобождения ресурса. Результат работы программы при синхронизации доступа к общему ресурсу резко изменится:

"Поток 1" - 1 "Поток 1" - 2 "Поток 1" - 3 "Поток 1" - 4 "Поток 5" - 1 "Поток 5" - 2 "Поток 5" - 3 "Поток 5" - 4 "Поток 4" - 1 "Поток 4" - 2 "Поток 4" - 3 "Поток 4" - 4 "Поток 3" - 1 "Поток 3" - 2 "Поток 3" - 3 "Поток 3" - 4 "Поток 2" - 1 "Поток 2" - 2 "Поток 2" - 3 "Поток 2" - 4

Следующий код демонстрирует порядок использования оператора synchronized для блокирования доступа к объекту.

Synchronized (оbject) { // other thread safe code }

Блокировка на уровне метода и класса

Блокировать доступ к ресурсам можно на уровне метода и класса. Следующий код показывает, что если во время выполнения программы имеется несколько экземпляров класса DemoClass, то только один поток может выполнить метод demoMethod(), для других потоков доступ к методу будет заблокирован. Это необходимо когда требуется сделать определенные ресурсы потокобезопасными.

Public class DemoClass { public synchronized static void demoMethod(){ // ... } } // или public class DemoClass { public void demoMethod(){ synchronized (DemoClass.class) { // ... } } }

Каждый объект в Java имеет ассоциированный с ним монитор, который представляет своего рода инструмент для управления доступа к объекту. Когда выполнение кода доходит до оператора synchronized , монитор объекта блокируется, предоставляя монопольный доступ к блоку кода только одному потоку, который произвел блокировку. После окончания работы блока кода, монитор объекта освобождается и он становится доступным для других потоков.

Некоторые важные замечания использования synchronized

  1. Синхронизация в Java гарантирует, что два потока не могут выполнить синхронизированный метод одновременно.
  2. Оператор synchronized можно использовать только с методами и блоками кода, которые могут быть как статическими, так и не статическими.
  3. Если один из потоков начинает выполнять синхронизированный метод или блок, то этот метод/блок блокируются. Когда поток выходит из синхронизированного метода или блока JVM снимает блокировку. Блокировка снимается, даже если поток покидает синхронизированный метод после завершения из-за каких-либо ошибок или исключений.
  4. Синхронизация в Java вызывает исключение NullPointerException, если объект, используемый в синхронизированном блоке, не определен, т.е. равен null.
  5. Синхронизированные методы в Java вносят дополнительные затраты на производительность приложения. Поэтому следует использовать синхронизацию, когда она абсолютно необходима.
  6. В соответствии со спецификацией языка нельзя использовать synchronized в конструкторе, т.к. приведет к ошибке компиляции.

Примечание: для синхронизации потоков можно использовать объекты синхронизации Synchroniser"s пакета java.util.concurrent .

Взаимная блокировка

С использованием блокировок необходимо быть очень внимательным, чтобы не создать «взаимоблокировку», которая хорошо известна разработчикам. Этот термин означает, что один из потоков ждет от другого освобождения заблокированного им ресурса, в то время как сам также заблокировал один из ресурсов, доступа к которому ждёт второй поток. В данном процессе могут участвовать два и более потоков.

Основные условия возникновения взаимоблокировок в многопотоковом приложении:

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

Взаимодействие между потоками в Java, wait и notify

При взаимодействии потоков часто возникает необходимость приостановки одних потоков и их последующего извещения о завершении определенных действий в других потоков. Так например, действия первого потока зависят от результата действий второго потока, и надо каким-то образом известить первый поток, что второй поток произвел/завершил определенную работу. Для подобных ситуаций используются методы:

  • wait() - освобождает монитор и переводит вызывающий поток в состояние ожидания до тех пор, пока другой поток не вызовет метод notify();
  • notify() - продолжает работу потока, у которого ранее был вызван метод wait();
  • notifyAll() - возобновляет работу всех потоков, у которых ранее был вызван метод wait().

Все эти методы вызываются только из синхронизированного контекста (синхронизированного блока или метода).

Рассмотрим пример «Производитель-Склад-Потребитель» (Producer-Store-Consumer). Пока производитель не поставит на склад продукт, потребитель не может его забрать. Допустим производитель должен поставить 5 единиц определенного товара. Соответственно потребитель должен весь товар получить. Но, при этом, одновременно на складе может находиться не более 3 единиц товара. При реализации данного примера используем методы wait() и notify() .

Листинг класса Store

Package example; public class Store { private int counter = 0; public synchronized void get() { while (counter < 1) { try { wait(); } catch (InterruptedException e) {} } counter--; System.out.println("-1: товар забрали"); System.out.println("\tколичество товара на складе: " + counter); notify(); } public synchronized void put() { while (counter >= 3) { try { wait(); }catch (InterruptedException e) {} } counter++; System.out.println("+1: товар добавили"); System.out.println("\tколичество товара на складе: " + counter); notify(); } }

Класс Store содержит два синхронизированных метода для получения товара get() и для добавления товара put() . При получении товара выполняется проверка счетчика counter. Если на складе товара нет, то есть counter < 1, то вызывается метод wait() , который освобождает монитор объекта Store и блокирует выполнение метода get() , пока для этого монитора не будет вызван метод notify() .

При добавлении товара также выполняется проверка количества товара на складе. Если на складе больше 3 единиц товара, то поставка товара приостанавливается и вызывается метод notify() , который передает управление методу get() для завершения цикла while().

Листинги классов Producer и Consumer

Классы Producer и Consumer реализуют интерфейс Runnable , методы run() у них переопределены. Конструкторы этих классов в качестве параметра получают объект склад Store. При старте данных объектов в виде отдельных потоков в цикле вызываются методы put() и get() класса Store для «добавления» и «получения» товара.

Package example; public class Producer implements Runnable { Store store; Producer(Store store) { this.store=store; } @Override public void run() { for (int i = 1; i < 6; i++) { store.put(); } } } public class Consumer implements Runnable { Store store; Consumer(Store store) { this.store=store; } @Override public void run(){ for (int i = 1; i < 6; i++) { store.get(); } } }

Листинг класса Trade

В главном потоке класса Trade (в методе main ) создаются объекты Producer-Store-Consumer и стартуются потоки производителя и потребителя.

Package example; public class Trade { public static void main(String args) { Store store = new Store(); Producer producer = new Producer(store); Consumer consumer = new Consumer(store); new Thread(producer).start(); new Thread(consumer).start(); } }

При выполнении программы в консоль будут выведены следующие сообщения:

1: товар добавили количество товара на складе: 1 +1: товар добавили количество товара на складе: 2 +1: товар добавили количество товара на складе: 3 -1: товар забрали количество товара на складе: 2 -1: товар забрали количество товара на складе: 1 -1: товар забрали количество товара на складе: 0 +1: товар добавили количество товара на складе: 1 +1: товар добавили количество товара на складе: 2 -1: товар забрали количество товара на складе: 1 -1: товар забрали количество товара на складе: 0

Поток-демон, daemon

Java приложение завершает работу тогда, когда завершает работу последний его поток. Даже если метод main() уже завершился, но еще выполняются порожденные им потоки, система будет ждать их завершения.

Однако это правило не относится к потоков-демонам (daemon ). Если завершился последний обычный поток процесса, и остались только daemon потоки, то они будут принудительно завершены и выполнение приложения закончится. Чаще всего daemon потоки используются для выполнения фоновых задач, обслуживающих процесс в течение его жизни.

Объявить поток демоном достаточно просто. Для этого нужно перед запуском потока вызвать его метод setDaemon(true). Проверить, является ли поток daemon "ом можно вызовом метода isDaemon(). В качестве примера использования daemon-потока можно рассмотреть класс Trade, который принял бы следующий вид:

Package example; public class Trade { public static void main(String args) { Producer producer = new Producer(store); Consumer consumer = new Consumer(store); // new Thread(producer).start(); // new Thread(consumer).start(); Thread tp = new Thread(producer); Thread tc = new Thread(consumer); tp.setDaemon(true); tc.setDaemon(true); tp.start(); tc.start(); try { Thread.sleep(100); } catch (InterruptedException e) {} System.out.println("\nГлавный поток завершен\n"); System.exit(0); } }

Здесь можно самостоятельно поэкспериментировать с определением daemon-потока для одного из классов (producer, consumer) или обоих классов, и посмотреть, как система (JVM) будет вести себя.

Thread и Runnable, что выбрать?

Зачем нужно два вида реализации многопоточности; какую из них и когда использовать? Ответ несложен. Реализация интерфейса Runnable используется в случаях, когда класс уже наследует какой-либо родительский класс и не позволяет расширить класс Thread . К тому же хорошим тоном программирования в java считается реализация интерфейсов. Это связано с тем, что в java может наследоваться только один родительский класс. Таким образом, унаследовав класс Thread , невозможно наследовать какой-либо другой класс.

Расширение класса Thread целесообразно использовать в случае необходимости переопределения других методов класса помимо метода run().

Приоритеты выполнения и голодание

Иногда разработчики используют приоритеты выполнения потока. В Java есть планировщик потоков (Thread Scheduler ), который контролирует все запущенные потоки и решает, какие потоки должны быть запущены и какая строка кода должна выполняться. Решение основывается на приоритете потока. Поэтому потоки с меньшим приоритетом получают меньше процессорного времени по сравнению с потоками с бо́льшим приоритет. Данное разумное решением может стать причиной проблем при злоупотреблении. То есть, если бо́льшую часть времени исполняются потоки с высоким приоритетом, то низкоприоритетные потоки начинают «голодать», поскольку не получают достаточно времени для того, чтобы выполнить свою работу должным образом. Поэтому рекомендуется задавать приоритет потока только тогда, когда для этого имеются веские основания.

Неочевидный пример «голодания» потока даёт метод finalize() , предоставляющий возможность выполнить код перед тем, как объект будет удалён сборщиком мусора. Однако приоритет финализирующего потока невысокий. Следовательно, возникают предпосылки для потокового голодания, когда методы finalize() объекта тратят слишком много времени (большие задержки) по сравнению с остальным кодом.

Другая проблема со временем исполнения может возникнуть от того, что не был определен порядок прохождения потоком блока synchronized . Когда несколько параллельных потоков должны выполнить некоторый код, оформленный блоком synchronized , может получиться так, что одним потокам придётся ждать дольше других, прежде чем войти в блок. Теоретически они могут вообще туда не попасть.

Скачать примеры

Рассмотренные на странице примеры многопоточности и синхронизации потоков в виде проекта Eclipse можно скачать (14Кб).

Уклонение (не путать с уворотом (dodge), шанс которого персонаж получает только с пассивных навыков Acrobatics, Phase Acrobatics и Acrobatics Improvement) – параметр персонажа, отвечающий за шанс увернуться от удара противника (53 базовый рейтинг уклонения для всех классов +3 за каждый уровень). Каждые 5 пунктов ловкости (dexterity) добавляет +1% рейтинга уклонения.

Быстрый бег – залог успеха, если уклонение – основа вашей защиты.

Механика

При сработавшем уклонении персонаж избежит урона, эффектов и оглушения, которые должно было вызвать попадание. Шанс увернуться от атаки не может быть менее 5% и более 95% и рассчитывается по формуле:

Шанс увернуться = 1 - Точность атакующего / (Точность атакующего + (Уклонение атакуемого)/4)^0,8)

Уклонение также дает шанс избежать критического урона, причем шанс этот рассчитывается отдельно (и на заклинания всё также не работает). Если нанесен критический удар и он попал по персонажу, производится новый расчет, для выяснения прошел ли критический урон с этого удара или нет. Если первый расчет показал попадание, а второй – промах, удар пройдет, но будет обычным, без крита.

Технически, рейтинг уклонения не использует генератор случайных чисел, сделано это специально, для избежания большого количества случайных попаданий по персонажу подряд (обидно умереть в хардкор-лиге имея 95% шанс увернуться, потому что генератор случайных чисел пропустил по вам 5 ударов подряд, даже несмотря на микроскопический шанс оного). Опуская математические подробности, имея более 50% уклонения вы фактически гарантируете, что никогда не получите два попадания подряд.

Уклонение (evasion) и уворот (dodge)

Шанс уворота (включая тот, что дается пассивным навыком Phase Acrobatics и работает против заклинаний) по механике работает также как и шанс уклонения. Суммируются оба этих параметра по следующей формуле.

Модуль threading впервые был представлен в Python 1.5.2 как продолжение низкоуровневого модуля потоков. Модуль threading значительно упрощает работу с потоками и позволяет программировать запуск нескольких операций одновременно . Обратите внимание на то, что потоки в Python лучше всего работают с операциями I/O, такими как загрузка ресурсов из интернета или чтение файлов и папок на вашем компьютере.

Если вам нужно сделать что-то, для чего нужен интенсивный CPU, тогда вам, возможно, захочется взглянуть на модуль multiprocessing , вместо threading . Причина заключается в том, что Python содержит Global Interpreter Lock (GIL), который запускает все потоки внутри главного потока. По этой причине, когда вам нужно запустить несколько интенсивных операций с потоками, вы заметите, что все работает достаточно медленно. Так что мы сфокусируемся на том, в чем потоки являются лучшими: операции I/O.

Небольшое интро

Поток позволяет вам запустить часть длинного кода так, как если бы он был отдельной программой. Это своего рода вызов наследуемого процесса, за исключением того, что вы вызываете функцию или класс, вместо отдельной программы. Я всегда находил конкретные примеры крайне полезными. Давайте взглянем на нечто совершенно простое:

Import threading def doubler(number): """ A function that can be used by a thread """ print(threading.currentThread().getName() + "\n") print(number * 2) print() if __name__ == "__main__": for i in range(5): my_thread = threading.Thread(target=doubler, args=(i,)) my_thread.start()

Здесь мы импортируем модуль threading и создаем обычную функцию под названием doubler. Наша функция принимает значение и удваивает его. Также она выводи название потока, который вызывает функцию и выводит бланк-строчку в конце. Далее, в последнем блоке кода, мы создаем пять потоков, и запускаем каждый из них по очереди.

Используя многопоточность можно решить много рутинных моментов. Например загрузка видео или другого материала в социальные сети, такие как Youtube или Facebook. Для развития своего Youtube канала можно использовать https://publbox.com/ru/youtube который возьмет на себя администрирование вашего канала. Youtube отличный источник заработка и чем больше каналов тем лучше. Без Publbox вам не обойтись.

Обратите внимание на то, что когда мы определяем поток, мы устанавливаем его целью на нашу функцию doubler , и мы также передаем аргумент функции. Причина, по которой параметр args выглядит немного непривычно, заключается в том, что нам нужно передать sequence функции doubler, и она принимает только один аргумент, так что нужно добавить запятую в конце, чтобы создать sequence одной из них. Обратите внимание на то, что если вы хотите подождать, пока поток определится, вы можете вызвать его метод join (). Когда вы запустите этот код, вы получите следующую выдачу:

Thread-1 0 Thread-2 2 Thread-3 4 Thread-4 6 Thread-5 8

Конечно, вам скорее всего не захочется выводить вашу выдачу в stdout. Это может закончиться сильным беспорядком. Вместо этого, вам нужно использовать модуль Python под названием logging . Это защищенный от потоков модуль и он прекрасно выполняет свою работу. Давайте немного обновим указанный ранее пример и добавим модуль logging , и заодно назовем наши потоки:

Import logging import threading def get_logger(): logger = logging.getLogger("threading_example") logger.setLevel(logging.DEBUG) fh = logging.FileHandler("threading.log") fmt = "%(asctime)s - %(threadName)s - %(levelname)s - %(message)s" formatter = logging.Formatter(fmt) fh.setFormatter(formatter) logger.addHandler(fh) return logger def doubler(number, logger): """ A function that can be used by a thread """ logger.debug("doubler function executing") result = number * 2 logger.debug("doubler function ended with: {}".format(result)) if __name__ == "__main__": logger = get_logger() thread_names = ["Mike", "George", "Wanda", "Dingbat", "Nina"] for i in range(5): my_thread = threading.Thread(target=doubler, name=thread_names[i], args=(i,logger)) my_thread.start()

Самое большое изменение в этом коде – это добавление функции get_logger . Эта часть кода создаст логгер, который настроен на дебаг. Это сохранит log в нынешнюю рабочую папку (другими словами, туда, откуда запускается скрипт) и затем мы настраиваем формат каждой линии на логированный. Формат включает временной штамп, название потока, уровень логгирования и логгированое сообщение. В функции doubler мы меняем наши операторы вывода на операторы логгирования .

Обратите внимание на то, что мы передаем логгер в функцию doubler, когда создаем поток . Это связанно с тем, что если вы определяет объект логгирования в каждом потоке, вы получите несколько singletons и ваш журнал будет содержать множество повторяющихся строк. В конце мы называем наши потоки, создав список наименований, и затем устанавливаем каждый поток на особое наименование, использую параметр name. Когда вы запустите этот код, вы должны получить лог файл со следующим содержимым:

Эта выдача достаточно понятная, так что давайте пойдем дальше. Я хочу разобрать еще один вопрос в этой статье. Мы поговорим о наследовании класса под названием threading.Thread . Давайте снова рассмотрим предыдущий пример, только вместо вызова потока напрямую, мы создадим свой собственный подкласс. Вот обновленный код:

Import logging import threading class MyThread(threading.Thread): def __init__(self, number, logger): threading.Thread.__init__(self) self.number = number self.logger = logger def run(self): """ Run the thread """ logger.debug("Calling doubler") doubler(self.number, self.logger) def get_logger(): logger = logging.getLogger("threading_example") logger.setLevel(logging.DEBUG) fh = logging.FileHandler("threading_class.log") fmt = "%(asctime)s - %(threadName)s - %(levelname)s - %(message)s" formatter = logging.Formatter(fmt) fh.setFormatter(formatter) logger.addHandler(fh) return logger def doubler(number, logger): """ A function that can be used by a thread """ logger.debug("doubler function executing") result = number * 2 logger.debug("doubler function ended with: {}".format(result)) if __name__ == "__main__": logger = get_logger() thread_names = ["Mike", "George", "Wanda", "Dingbat", "Nina"] for i in range(5): thread = MyThread(i, logger) thread.setName(thread_names[i]) thread.start()

В этом примере мы только что унаследовали класс threading.Thread . Мы передали число, которое хотим удвоить, а также передали объект логгированмя, как делали это ранее. Но на этот раз, мы настроим название потока по-другому, вызвав функцию setName в объекте потока. Нам все еще нужно вызвать старт в каждом потоке, но запомните, что нам не нужно определять это в наследуемом классе. Когда вы вызываете старт, он запускает ваш поток, вызывая метод run . В нашем классе мы вызываем функцию doubler для выполнения наших вычислений. Выдача сильно похожа на ту, что была в примере ранее, за исключением того, что я добавил дополнительную строку в выдаче. Попробуйте сами и посмотрите, что получится.

Замки и Синхронизация

Когда у вас в распоряжении более одного потока, тогда вам, возможно, понадобится понять, как избежать конфликтов . Под этим я имею ввиду то, что вы можете использовать случай, где более одного потока нуждаются в доступе к одном и тому же ресурсу в одно и то же время. Если вы не думаете о таких проблемах и соответственном планировании, тогда вы можете столкнуться с самыми худшими проблемами в крайне неудобное время, и, как правило, в момент выпуска кода.

Решение проблемы – это использовать замки . Замок предоставлен модулем Python threading и может держать один поток, или не держать поток вообще. Если поток пытается acquire замок на ресурсе, который уже закрыт, этот поток будет ожидать до тех пор, пока замок не откроется. Давайте посмотрим на практичный пример одного кода, который не имеет никакого замочного функционала, но мы попробуем его добавить:

Import threading total = 0 def update_total(amount): """ Updates the total by the given amount """ global total total += amount print (total) if __name__ == "__main__": for i in range(10): my_thread = threading.Thread(target=update_total, args=(5,)) my_thread.start()

Мы можем сделать этот пример еще интереснее, добавив вызов time.sleep . Следовательно, проблема здесь в том, что один поток может вызывать update_total и перед тем, как он обновится, другой поток может вызвать его и тоже попытается обновить его. В зависимости от порядка операций, значение может быть добавлено единожды. Давайте добавим замок к функции. Существует два способа сделать эта. Первый – это использование try/finally , если мы хотим убедиться, что замок снят. Вот пример:

Import threading total = 0 lock = threading.Lock() def update_total(amount): """ Updates the total by the given amount """ global total lock.acquire() try: total += amount finally: lock.release() print (total) if __name__ == "__main__": for i in range(10): my_thread = threading.Thread(target=update_total, args=(5,)) my_thread.start()

Здесь мы просто вешаем замок, перед тем как сделать что-либо другое. Далее, мы пытаемся обновить total и finally , мы снимаем замок и выводим нынешний total . Мы можем упростить данную задачу, используя оператор Python под названием with :

Import threading total = 0 lock = threading.Lock() def update_total(amount): """ Updates the total by the given amount """ global total with lock: total += amount print (total) if __name__ == "__main__": for i in range(10): my_thread = threading.Thread(target=update_total, args=(5,)) my_thread.start()

Как вы видите, нам больше не нужны try/finally , так как контекстный менеджер, предоставленный оператором with , сделал все это за нас. Конечно, вы можете обнаружить, что пишите код там, где необходимы несколько потоков с доступом к нескольким функциям. Когда вы впервые начнете писать конкурентный код , вы можете сделать что-нибудь на подобии следующего:

Import threading total = 0 lock = threading.Lock() def do_something(): lock.acquire() try: print("Lock acquired in the do_something function") finally: lock.release() print("Lock released in the do_something function") return "Done doing something" def do_something_else(): lock.acquire() try: print("Lock acquired in the do_something_else function") finally: lock.release() print("Lock released in the do_something_else function") return "Finished something else" if __name__ == "__main__": result_one = do_something() result_two = do_something_else()

Этот код хорошо работает в данном случае, но подразумевается, что у вас есть несколько потоков , вызывающих обе эти функции. Пока один поток работает над функциями, второй может, в свою очередь, обновлять данные и вы получите некорректный результат. Проблема в том, что вы сначала можете не заметить, что с результатами что-то неладное. Как найти решение этой проблеме? Давайте в ней разберемся. Первое, к чему можно прийти, это повесить замок на двух вызовах функций. Давайте попробуем обновить указанный ранее пример, чтобы получить что-то вроде следующего:

Import threading total = 0 lock = threading.RLock() def do_something(): with lock: print("Lock acquired in the do_something function") print("Lock released in the do_something function") return "Done doing something" def do_something_else(): with lock: print("Lock acquired in the do_something_else function") print("Lock released in the do_something_else function") return "Finished something else" def main(): with lock: result_one = do_something() result_two = do_something_else() print (result_one) print (result_two) if __name__ == "__main__": main()

Когда вы запустите этот код, вы увидите, что он просто висит. Причина в том, что мы просто указываем модулю threading повесить замок. Так что когда мы вызываем первую функцию, она видит, что замок уже висит и блокируется. Это будет длиться до тех пор, пока замок не снимут, что никогда и не случится, так как это не предусмотрено в коде. Хорошее решение в данном случае – использовать re-entrant замок. Модуль threading предоставляет такой, в виде функции RLock . Просто замените строку lock = threading.Lock () на lock = threading.RLock () и попробуйте перезапустить код. Теперь он должен заработать. Если вы хотите попробовать код выше но добавить в него потоки, то мы можем заменить call на main следующим образом:

If __name__ == "__main__": for i in range(10): my_thread = threading.Thread(target=main) my_thread.start()

Так мы запустим основную функцию в каждом потоке, что в свою очередь приведет к вызову остальных двух функций. В конце вы получите достаточно крупную выдачу.

Таймеры

Модуль threading включает в себя один очень удобный класс, под названием Timer , который вы можете использовать запуска действия, спустя определенный отрезок времени. Данный класс запускает собственный поток и начинают работу с того же метода start (), как и обычные потоки. Вы также можете остановить таймер, используя метод cancel. Обратите внимание на то, что вы можете отменить таймер еще до того, как он стартовал. Однажды у меня был случай, когда мне нужно было наладить связь с под-процессом, который я начал, но мне нужен было обратный отсчет. Несмотря на существования ряда различных способов решения этой отдельной проблемы, моим любимым решением всегда было использование класса Timer модуля threading. Для этого примера мы взглянем на применение команды ping. В Linux, команда ping будет работать, пока вы её не убьете. Так что класс Timer становится особенно полезным для мира Linux. Вот пример:

Import subprocess from threading import Timer kill = lambda process: process.kill() cmd = ["ping", "www.google.com"] ping = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) my_timer = Timer(5, kill, ) try: my_timer.start() stdout, stderr = ping.communicate() finally: my_timer.cancel() print (str(stdout))

Здесь мы просто настраиваем лямбду, которую мы можем использовать, чтобы убить процесс. Далее мы начинаем нашу работу над ping и создаем объект Timer. Обратите внимание на то, что первый аргумент – это время ожидания в секундах, затем – функция, которую нужно вызвать и аргумент, который будет передан функции. В нашем случае, наша функция – это лямбда, и мы передаем её список аргументов, где список содержит только один элемент. Если вы запустите этот код, он будет работать примерно 5 секунд, после чего выведет результат пинга.

Другие Компоненты Потоков

Модуль threading также включает в себя поддержу других объектов. Например, вы можете создать семафор , который является одним из древнейших синхронизационных примитивов в компьютерной науке. Семафор управляет внутренним счетчиком , который будет декрементируется когда вы вызываете acquire и увеличивается, когда вы вызываете release . Счетчик разработан таким образом, что он не может падать ниже нуля. Так что если так вышло, что вы вызываете acquire когда он равен нулю, то он заблокируется.

Еще один полезный инструмент, который содержится в модуле, это Event . С его помощью вы можете получить связь между двумя потоками, используя сигналы. Мы рассмотрим примеры применения Event в следующей статье. Наконец-то, в версии Python 3.2 был добавлен объект Barrier . Это примитив, который управляет пулом потока, при этом не важно, где потоки должны ждать своей очереди. Для передачи барьера, потоку нужно вызвать метод wait (), который будет блокировать до тех пор, пока все потоки не сделают вызов. После чего все потоки пройдут дальше одновременно.

Связь потоков

Существует ряд случаев, когда вам нужно сделать так, чтобы потоки были связанны друг с другом. Как я упоминал ранее, вы можете использовать Event для этой цели. Но более удобный способ – использовать Queue . В нашем примере мы используем оба способа! Давайте посмотрим, как это будет выглядеть:

Import threading from queue import Queue def creator(data, q): """ Creates data to be consumed and waits for the consumer to finish processing """ print("Creating data and putting it on the queue") for item in data: evt = threading.Event() q.put((item, evt)) print("Waiting for data to be doubled") evt.wait() def my_consumer(q): """ Consumes some data and works on it In this case, all it does is double the input """ while True: data, evt = q.get() print("data found to be processed: {}".format(data)) processed = data * 2 print(processed) evt.set() q.task_done() if __name__ == "__main__": q = Queue() data = thread_one = threading.Thread(target=creator, args=(data, q)) thread_two = threading.Thread(target=my_consumer, args=(q,)) thread_one.start() thread_two.start() q.join()

Давайте немного притормозим. Во первых, у нас есть функция creator (также известная, как producer ), которую мы используем для создания данных, с которыми мы хотим работать (или использовать). Далее мы получаем еще одну функцию, которую мы используем для обработки данных, под названием my_consumer . Функция creator использует метод Queue под названием put, чтобы добавить данные в очередь, затем потребитель, в свою очередь, будет проверять, есть ли новые данные и обрабатывать их, когда такие появятся. Queue обрабатывает все закрытия и открытия замков, так что лично вам эта участь не грозит.

В данном примере мы создали список значений, которые мы хотим дублировать. Далее мы создаем два потока, один для функции creator/producer , второй для consumer (потребитель). Обратите внимание на то, что мы передаем объект Queue каждому потоку, что является прямо таки магией, учитывая то, как обрабатываются замки. Очередь начнется с первого потока, который передает данные второму. Когда первый поток передает те или иные данные в очередь, он также передает их к Event , после чего дожидается, когда произойдет события, чтобы закончить. Далее, в функции consumer, данные обрабатываются, и после этого вызывается метод настройки Event , который указывает первому потоку, что второй закончил обработку, так что он может продолжать. Последняя строка кода вызывает метод join объекта Queue, который указывает Queue подождать, пока потоки закончат обработку. Первый поток заканчивает, когда ему больше нечего передавать в Queue.

Подведем итоги

Мы рассмотрели достаточно много материала. А именно:

  1. Основы работы с модулем threading
  2. Как работают замки
  3. Что такое Event и как его можно использовать
  4. Как использовать таймер
  5. Внутрипотоковая связь с использованием Queue/Event

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