2.2 Потоки
От переводчика: данная статья является седьмой в цикле переводов официального руководства по библиотеке SFML. Прошлую статью можно найти тут. Данный цикл статей ставит своей целью предоставить людям, не знающим язык оригинала, возможность ознакомится с этой библиотекой. SFML — это простая и кроссплатформенная мультимедиа библиотека. SFML обеспечивает простой интерфейс для разработки игр и прочих мультимедийных приложений. Оригинальную статью можно найти тут. Начнем.
Оглавление:
1. Приступая к работе
- SFML и Visual Studio
- SFML и Code::Blocks (MinGW)
- SFML и Linux
- SFML и Xcode (Mac OS X)
- Компиляция SFML с помощью CMake
- Обработка времени
- Потоки
- Работа с пользовательскими потоками данных
- Открытие и управление окнами
- Обработка событий
- Работа с клавиатурой, мышью и джойстиками
- Использование OpenGL
- Рисование 2D объектов
- Спрайты и текстуры
- Текст и шрифты
- Формы
- Проектирование ваших собственных объектов с помощью массивов вершин
- Позиция, вращение, масштаб: преобразование объектов
- Добавление специальных эффектов с шейдерами
- Контроль 2D камеры и вида
- Проигрывание звуков и музыки
- Запись аудио
- Пользовательские потоки аудио
- Спатиализация: звуки в 3D
- Коммуникация с использованием сокетов
- Использование и расширение пакетов
- Веб-запросы с помощью HTTP
- Передача файлов с помощью FTP
Что такое поток?
Большая часть из вас уже знает, что такое поток, однако объясним, что это такое, для новичков в данной теме.
Поток — это по сути последовательность инструкций, которые выполняются параллельно с другими потоками. Каждая программа создает по меньшей мере один поток: основной, который запускает функцию main(). Программа, использующая только главный поток, является однопоточной; если добавить один или более потоков, она станет многопоточной.
Так, короче, потоки — это способ сделать несколько вещей одновременно. Это может быть полезно, например, для отображения анимации и обработки пользовательского ввода данных во время загрузки изображений или звуков. Потоки также широко используется в сетевом программировании, во время ожидания получения данные будет продолжаться обновление и рисование приложения.
Потоки SFML или std::thread?
В своей последней версии (2011), стандартная библиотека C++ предоставляет набор классов для работы с потоками. Во время написания SFML, стандарт C++11 еще не был написан и не было никакого стандартного способа создания потоков. Когда SFML 2.0 был выпущен, было много компиляторов, которые не поддерживали этот новый стандарт.
Если вы работаете с компилятором, который поддерживает новый стандарт и содержит заголовочный файл , забудьте о классах потоков SFML и используйте стандартные классы C++ вместо них. Но, если вы работаете с компилятором, не поддерживающим данный стандарт, или планируете распространять ваш код и хотите добиться полной портируемости, потоковые классы SFML являются хорошим выбором
Создание потоков с помощью SFML
Хватит разглагольствований, давайте посмотрим на код. Класс, дающий возможность создавать потоки с помощью SFML, называется sf::Thread, и вот как это (создание потока) выглядит в действии:
#include #include void func() < // эта функция запускается когда вызывается thread.launch() for (int i = 0; i < 10; ++i) std::cout int main() < // создание потока с функцией func в качестве точки входа sf::Thread thread(&func); // запуск потока thread.launch(); // главные поток продолжает быть запущенным. for (int i = 0; i
В этом коде функции main и func выполняются параллельно после вызова thread.launch(). Результатом этого является то, что текст, выводимый обеими функциями, смешивается в консоли.
Точка входа в поток, т.е. функция, которая будет выполняться, когда поток запускается, должна быть передана конструктору sf::Thread. sf::Thread пытается быть гибким и принимать различные точки входа: non-member функции или методы классов, функции с аргументами или без них, функторы и так далее. Приведенный выше пример показывает, как использовать функцию-член, вот несколько других примеров.
-
non-member функция с одним аргументом:
void func(int x) < >sf::Thread thread(&func, 5);
class MyClass < public: void func() < >>; MyClass object; sf::Thread thread(&MyClass::func, &object);
struct MyFunctor < void operator()() < >>; sf::Thread thread(MyFunctor());
// с лямбда-функцией sf::Thread thread([]()< std::cout );
// с std::bind void func(std::string, int, double) < >sf::Thread thread(std::bind(&func, "hello", 24, 0.5));
Если вы хотите использовать sf::Thread внутри класса, не забудьте, что он не имеет стандартного конструктора. Поэтому, вы должны инициализировать его в конструкторе вашего класса в списке инициализации:
class ClassWithThread < public: ClassWithThread() : m_thread(&ClassWithThread::f, this) < >private: void f() < . >sf::Thread m_thread; >;
Если вам действительно нужно создать экземпляр sf::Thread после инициализации объекта, вы можете создать его в куче.
Запуск потока
После того, как вы создали экземпляр sf::Thread, вы должны запустить его с помощью запуска функции.
sf::Thread thread(&func); thread.launch();
launch вызывает функцию, которую вы передали в конструктор нового потока, и сразу же завершает свою работу, так что вызывающий поток может сразу же продолжить выполнение.
Остановка потоков
Поток автоматически завершает свою работу, когда функция, служащая точкой входа для данного потока, возвращает свое значение. Если вы хотите ждать завершения потока из другого потока, вы можете вызвать его функцию wait.
sf::Thread thread(&func); // запуск потока thread.launch(); . // выполнение блокируется до тех пор, пока поток не завершится thread.wait();
Функция ожидания также неявно вызывается деструктором sf::Thread, так что поток не может оставаться запущенным (и бесконтрольным) после того, как его экземпляр sf::Thread уничтожается. Помните это, когда вы управляете вашими потоками (смотрите прошлую секцию статьи).
Приостановка потока
В SFML нет функции, которая бы предоставляла способ приостановки потока; единственный способ приостановки потока — сделать это из кода самого потока. Другими словами, вы можете только приостановить текущий поток. Что бы это сделать, вы можете вызвать функцию sf::sleep:
void func()
sf::sleep имеет один аргумент — время приостановки. Это время может быть выражено в любой единице, как было показано в статье про обработку времени.
Обратите внимание, что вы можете приостановить любой поток с помощью данной функции, даже главный поток.
sf::sleep является наиболее эффективным способом приостановить поток: на протяжении приостановки потока, он (поток) практически не потребляет ресурсы процессора. Приостановка, основанная на активном ожидании, вроде пустого цикла while, потребляет 100% ресурсов центрального процессора и делает… ничего. Однако имейте в виду, что продолжительность приостановки является просто подсказкой; реальная продолжительность приостановки (больше или меньше указанного вами времени) зависит от ОС. Так что не полагайтесь на эту функцию при очень точном отсчете времени.
Защита разделяемых данных
Все потоки в программе разделяют некоторую память, они имеют доступ ко всем переменным в области их видимости. Это очень удобно, но также опасно: с момента параллельного запуска потока, переменные или функции могут использоваться одновременно разными потоками. Если операция не является потокобезопасной, это может привести к неопределенному поведению (т. е. это может привести к сбою или повреждению данных).
Существует несколько программных инструментов, которые могут помочь вам защитить разделяемые данные и сделать ваш код потокобезопасным, их называют примитивами синхронизации. Наиболее распространенными являются следующие примитивы: мьютексы, семафоры, условные переменные и спин-блокировки. Все они — варианты одной и той же концепции: они защищают кусок кода, давая только определенному потоку право получать доступ к данным и блокируя остальные.
Наиболее распространенным (и используемым) примитивом является мьютекс. Мьютекс расшифровывается как «Взаимное исключение». Это гарантия, что только один поток может выполнять код. Посмотрим, как мьютексы работают, на примере ниже:
#include #include sf::Mutex mutex; void func() < mutex.lock(); for (int i = 0; i < 10; ++i) std::cout int main()
Этот код использует общий ресурс (std::cout), и, как мы видим, это приводит к нежелательным результатам. Вывод потоков смешался в консоли. Чтобы убедиться в том, что вывод правильно напечатается, вместо того, чтобы быть беспорядочно смешанным, мы защищаем соответствующие области кода мьютексом.
Первый поток, который достигает вызова mutex.lock(), блокирует мьютекс и получает доступ к коду, который печатает текст. Когда другие потоки достигают вызова mutex.lock(), мьютекс уже заблокирован, и другие потоки приостанавливают свое выполнение (это похоже на вызов sf::sleep, спящий поток не потребляет время центрального процессора). Когда первый поток разблокирует мьютекс, второй поток продолжает свое выполнение, блокирует мьютекс и печатает текст. Это приводит к тому, что текст в консоли печатается последовательно и не смешивается.
Мьютекс — это не только примитив, который вы можете использовать для защиты разделяемых данных, вы можете использовать его во многих других случаях. Однако, если ваше приложение делает сложные вещи при работе с потоками, и вы чувствуете, что возможностей мьютексов недостаточно — не стесняйтесь искать другую библиотеку, обладающую большим функционалом.
Защита мьютексов
Не волнуйтесь: мьютексы уже потокобезопасны, нет необходимости их защищать. Но они не безопасны в плане исключений. Что происходит, если исключение выбрасывается, когда мьютекс заблокирован? Он никогда не может быть разблокирован и будет оставаться заблокированным вечно. Все потоки, пытающиеся разблокировать заблокированный мьютекс, будут заблокированы навсегда. В некоторых случаях, ваше приложение будет «заморожено».
Чтобы быть уверенным, что мьютекс всегда разблокирован в среде, в которой он (мьютекс) может выбросить исключение, SFML предоставляет RAII класс, позволяющий обернуть мьютекс в класс sf::Lock. Блокировка происходит в конструкторе, разблокировка происходит в деструкторе. Просто и эффективно.
sf::Mutex mutex; void func() < sf::Lock lock(mutex); // mutex.lock() functionThatMightThrowAnException(); // mutex.unlock(), если функция выбросит исключение >// mutex.unlock()
Помните, что sf::Lock может также быть использован в функциях, которые имеют множество возвращаемых значений.
sf::Mutex mutex; bool func() < sf::Lock lock(mutex); // mutex.lock() if (!image1.loadFromFile(". ")) return false; // mutex.unlock() if (!image2.loadFromFile(". ")) return false; // mutex.unlock() if (!image3.loadFromFile(". ")) return false; // mutex.unlock() return true; >// mutex.unlock()
Распространенные заблуждения
Вещь, часто упускаемая из виду: поток не может существовать без соответствующего экземпляра sf::Thread. Следующий код можно часто увидеть на форумах:
void startThread() < sf::Thread thread(&funcToRunInThread); thread.launch(); >int main() < startThread(); // . return 0; >
Программисты, которые пишут подобный код, ожидают, что функция startThread() будет запускать поток, который будет жить самостоятельно и уничтожаться при завершении выполнения функции (переданной в качестве точки входа). Этого не происходит. Функция потока блокирует главный поток, как если бы программа не работала.
В чем дело? Экземпляр sf::Thread является локальным для функции startThread(), поэтому немедленно уничтожаются, когда функция возвращает свое значение. Вызывается деструктор sf::Thread, происходит вызов wait(), как утверждалось выше, результатом этого становится блокировка главного потока, который ожидает завершения функции потока, вместо параллельного выполнения с ней.
Так что не забывайте: вы должны управлять экземплярами sf::Thread, чтобы они жили так долго, как требуется функции потока.
- Программирование
- C++
- Разработка игр
- Параллельное программирование
Что такое поток? Понимание основ потока в информационных технологиях
Что такое поток? Понимание основ потока в информационных технологиях
- by
- в News
- вкл 3 сентября 2023
Поток — это фундаментальное понятие в области информационных технологий (ИТ). Он относится к последовательности инструкций, которые могут быть выполнены независимо центральным процессором (ЦП) компьютера. Понимание основ потока критически важно для любого работающего в области ИТ, так как оно играет значительную роль в оптимизации производительности компьютерных систем.
Простыми словами, поток можно представить как легкий процесс. Он является единицей выполнения внутри программы, способной работать параллельно с другими потоками. Потоки позволяют выполнять несколько задач одновременно, что повышает эффективность и отзывчивость компьютерной системы.
Одним из ключевых преимуществ использования потоков является улучшенное многозадачное выполнение. Разделив программу на несколько потоков, разные части программы могут выполняться параллельно. Это позволяет ЦП работать над несколькими задачами одновременно, что приводит к более быстрой и эффективной обработке данных.
Потоки также облегчают обмен ресурсами между разными частями программы. Поскольку потоки внутри программы используют одно и то же пространство памяти, они могут легко обмениваться данными и взаимодействовать друг с другом. Это позволяет эффективно сотрудничать и координировать различные компоненты программы.
Помимо многозадачности и обмена ресурсами, потоки также обеспечивают более быструю реакцию в компьютерных системах. Запуская задачи одновременно, потоки могут гарантировать, что программа остается отзывчивой даже при выполнении операций, требующих много времени. Например, программу с пользовательским интерфейсом можно использовать с потоками, чтобы интерфейс оставался отзывчивым во время выполнения сложных вычислений в фоновом режиме.
Важно отметить, что потоки не ограничиваются одним ЦП. Фактически, современные компьютерные системы часто имеют несколько ЦП или ядер, каждое из которых способно выполнить несколько потоков одновременно. Это позволяет добиться еще большей параллельности и оптимизации производительности.
При работе с потоками важно учитывать синхронизацию потоков. Поскольку потоки используют одно и то же пространство памяти, они могут иметь потенциальный доступ и модифицировать одни и те же данные одновременно. Это может привести к повреждению данных и непредсказуемому поведению. Техники синхронизации потоков, такие как блокировки и семафоры, используются для обеспечения доступа к общему ресурсу только одним потоком, предотвращая конфликты и поддерживая целостность данных.
Еще одной важной аспектом управления потоками является планирование потоков. Операционная система отвечает за планирование потоков и выделение ЦП времени каждому потоку. Различные алгоритмы планирования используются для определения порядка выполнения потоков, учитывая такие факторы, как приоритет, справедливость и эффективность.
В заключение, поток является фундаментальным понятием в ИТ, которое позволяет одновременное выполнение задач, обмен ресурсами и улучшенную отзывчивость компьютерных систем. Понимание основ потока критически важно для оптимизации производительности программ и обеспечения эффективного многозадачного выполнения. Синхронизация и планирование потоков являются важными аспектами при работе с потоками, так как они помогают поддерживать целостность данных и эффективно распределять время ЦП. Пользуясь возможностями потоков, ИТ-специалисты могут повысить эффективность и производительность компьютерных систем, в конечном итоге обеспечивая лучшие пользовательские впечатления и повышение производительности.
Что такое потоки
Пишите, звоните +7(495) 669 3756 закажите обратный звонок
Помощь / Что такое «поток» и как понять, какой поток «доступен»?
Что такое «поток» и как понять, какой поток «доступен»?
Поток – это учебная группа, прикрепленная к курсу. Состав потока, а также его обязательность\необязательность определяется преподавателем.
Студенту может быть назначено несколько потоков одновременно, например, на курс «Конкурентные преимущества нашей компании» назначен необязательный поток для всех сотрудников компании, а для сейлзов и консультантов оффлайн-магазинов – назначен такой же обязательный поток, так как для этой категории сотрудников изучение материала необходимо для успешной работы.
Список доступных поток можно увидеть в карточке курса. В том случае, если студент не присоединился ни к одному потоку, напротив названия потока будет активна кнопка «Начать». Как только студент выбрал хотя бы один поток, все остальные потоки становятся для него недоступными.
В процессе изучения материалов курса напротив выбранного потока будет активна кнопка «Продолжить». Если студент завершил курс в рамках данного потока, то он увидит кнопку «Результаты», кликнув на которую можно будет ознакомиться с результатами прохождения курса.
Потоки в Java: что это такое и как они работают
Выжимаем максимум из процессора и заставляем программы на Java выполнять несколько задач одновременно.
Иллюстрация: Merry Mary для Skillbox Media
Лев Сергеев
Программист, музыкант. Знает Java, C# и Unity3D, но не собирается останавливаться на достигнутом.
В многоядерных процессорах все ядра параллельно выполняют свои наборы машинных инструкций. Поэтому современные компьютеры довольно быстро справляются со сложными вычислительными задачами.
Но и одноядерную систему можно настроить так, чтобы она работала над несколькими наборами инструкций как бы одновременно — то есть переключалась между ними очень быстро и незаметно для пользователя. При этом за каждую подзадачу будет отвечать своё «виртуальное» ядро, или поток (его ещё называют thread, то есть «нить»). Это и есть многопоточность. Разберёмся, что это такое и как её настроить.
Что такое поток в Java
Представьте работника лаборатории, которому выдали список дел. Он может выполнять каждое строго как написано: закончив первое дело, переходить ко второму, потом к третьему — и так до самого конца. А может решать несколько задач параллельно: например, загрузить компоненты в миксер, а пока идёт их перемешивание, делать навески для следующей загрузки или писать отчёт о результатах вчерашней работы. Возможно, при таком подходе дело пойдёт значительно быстрее.
В программировании то же самое: можно разбить большое задание на подзадачи и распределить их между потоками. Это такие абстрактные сущности, которые последовательно выполняют инструкции программы. Потоки протекают в процессе, а процесс, простыми словами, — это любая запущенная программа.
Любой процесс имеет минимум один поток, который называют главным. Он запускается в первую очередь, а остальные идут параллельно. Например, при запуске программы на Java процесс — это её среда исполнения (JRE).
Обычные программы на Java работают синхронно: строчки кода выполняются одна за другой в главном потоке. Но можно создать несколько тредов и управлять ими. Допустим, один может просто ждать, пока выполнится другой, а может в это время что-то вычислять.
Кроме «виртуальных» тредов, есть аппаратные, о которых говорилось в начале статьи. Они представляют собой «среды исполнения» программных тредов вашего кода. Когда код на Java заточен под несколько тредов, система задействует столько же реальных потоков процессора.
А если программа использует больше тредов, чем есть ядер у компьютера? Вам не стоит беспокоиться: за это отвечает планировщик ОС, который сам распределит ресурсы в угоду производительности.
Как работают потоки в Java
Чтобы увидеть, как работает поток, выполните в режиме отладки следующий код, расставив брейкпоинты на каждой строчке:
public class Main< public static void main(String[] args)< System.out.print("Hello"); System.out.print(" "); System.out.print("World"); > >
Команды будут выполняться последовательно: сначала на экране появится слово Hello, затем пробел, а в конце — World. Это обычное поведение программы, но его можно изменить с помощью класса Thread и интерфейса Runnable из стандартной библиотеки java.lang.
Посмотрим, что покажет программа, если воспользоваться методом Thread.currentThread(), который возвращает ссылку на текущую нить:
public class Main< public static void main(String[] args) < System.out.println(Thread.currentThread()); >> Вывод: Thread[main,5,main]
В квадратных скобках первым параметром указано имя потока main, о котором говорилось выше. Второй параметр — это приоритет (по умолчанию он равен 5), третий — имя группы потоков.
Отдельно имя можно получить с помощью метода getName():
System.out.println(Thread.currentThread().getName()); Вывод: main
Как создать свой поток в Java
Для этого есть класс Thread — это поток, только на уровне кода. Создать их можно сколько угодно, но одновременно будет выполняться столько, сколько поддерживает ваша система.
Интерфейс Runnable — это задача, которую выполняет поток, то есть код. Интерфейс содержит основной метод run() — в нём и находится точка входа и логика исполняемого потока.
Создать поток в Java можно двумя способами.
Первый способ:
- Определить класс — наследник класса Thread и переопределить метод run().
- Создать экземпляр своего класса и вызвать метод start().
class MyThread extends Thread< @Override public void run()< System.out.println("Hello, I’m " + Thread.currentThread()); > > public class Main< public static void main(String[] args)< MyThread myThread = new MyThread(); myThread.start(); > > Вывод: Hello, I’m Thread[Thread-0,5,main]
Обратите внимание: если на экземпляре класса Thread вместо метода start() вызвать run(), то код, написанный для другого потока, отлично выполнится, но выполнит его тот же тред, который и вызвал этот метод, а новый запущен не будет! Поэтому нужно пользоваться методом start().
Второй способ:
- Реализовать интерфейс Runnable и метод run().
- Создать экземпляр Thread и передать в конструктор свой Runnable (экземпляр класса, реализующий этот интерфейс).
class MyThread implements Runnable< @Override public void run()< System.out.print("Hello, I’m " + Thread.currentThread().getName()); > > public class Main< public static void main(String[] args)< // Первый параметр: экземпляр Runnable // Второй параметр: своё имя (необязательно) Thread myThread = new Thread(new MyThread(), "Leo"); myThread.start(); > > Вывод: Hello, I’m Leo
Второй вариант лучше — он более гибкий. Например, если бы MyThread уже наследовал какой-либо класс, то было бы невозможно пойти первым путём, так как Java не поддерживает множественное наследование.
Практика: изучаем потоки на котиках
Напишем небольшую консольную игру, в которой будут драться… коты. Обещаем: в процессе написания программы ни один кот не пострадает, зато вы увидите, как «нити» конкурируют между собой.
В коде будут новые ключевые слова и класс, которых вы раньше, скорее всего, не встречали:
- synchronized перед методом означает, что он синхронизирован. Поток, вызвавший синхронизированный метод, запрещает другим нитям к нему обращаться, пока сам не выйдет из метода.
- volatile нужен, когда одну переменную используют разные потоки, во избежание некорректных результатов.
- Класс CopyOnWriteArrayList — это тот же ArrayList, только потокобезопасный, то есть оптимизированный под использование нескольких потоков. Он находится в библиотеке java.util.concurrent.
Программа состоит из двух классов — CatFightsConsole и Cat. Вначале создаётся N боевых «котов», а когда они запускаются, они начинают драться друг с другом. Каждый кот стремится первым вызвать метод Cat.attack(), чтобы атаковать случайного из оставшихся в живых конкурентов и отнять у него жизнь. Когда жизнь кота становится равна 0, его поток завершает свою работу. Бой идёт до последнего кота.
Класс CatFightsConsole содержит метод main(String[] args), в котором производится начальная настройка программы: создание и настройка объектов, запуск потоков. Далее главный поток натыкается на метод join(), который говорит ему остановиться на этой строчке, пока не завершится поток, на котором был вызван метод. Когда все «кототреды» завершат свою работу, в консоль выведется сообщение о последнем коте — победителе.
// Главный класс public class CatFightsConsole < // Точка входа в программу, Main Thread public static void main(String[] args)< // Title System.out.println("Cat Fights Console"); // Создаём контейнер с котами List catThreads = new ArrayList<>(); // Жизни котов int life = 9; // Создаём и настраиваем классы-потоки котов, добавляя их в контейнер Collections.addAll(catThreads, new Cat("Tom", life, "Thread Tom"), new Cat("Cleocatra", life, "Thread Cleocatra"), new Cat("Dupli", life, "Thread Dupli"), new Cat("Toodles", life, "Thread Toodles")); // Запускаем котов for(Cat cat : catThreads) cat.getThread().start(); // Ждём, пока завершатся все, кроме главного for(Cat cat : catThreads)< try< // Поток, который вызвал метод join(), приостанавливается на этой строчке cat.getThread().join(); // Пока поток, на котором вызван метод, не завершит работу, Main ждёт остальных >catch (InterruptedException e) < e.printStackTrace(); >> // Последний выживший — первый элемент cats System.out.println(String.format("Кот-победитель: %s. ", Cat.cats.get(0))); > >
У класса Cat есть имя (String name), количество жизней (int life), личный поток и статический список cats со ссылками на все объекты Cat. Он реализует интерфейс Runnable, а значит, основной цикл работы потока происходит в методе run().
При запуске потока, пока объектов Cat более одного и пока у них есть жизни, вызывается синхронизированный статический метод Cat.attack(), который декрементирует переменную life с помощью метода decrementLife() у второго переданного в него объекта. Если после этого значение life равно нулю, то у этого же объекта вызывается метод getThread(), а на нём interrupt() — функция, прерывающая работу потока.
// Это класс «Кот» class Cat implements Runnable< // Статический контейнер всех созданных «кототредов» // Класс CopyOnWriteArrayList — тот же ArrayList, только потокобезопасный public static final List cats = new CopyOnWriteArrayList<>(); // Имя и количество жизней private String name; private volatile int life; // Личный поток private Thread thread; // Конструктор: задаём параметры и добавляем объект в статический список public Cat(String name, int life, String threadName) < this.name = name; // Имя this.life = life; // Количество жизни Cat.cats.add(this); // Добавляем себя в List cats thread = new Thread(this, threadName); // Создаём поток этого кота и передаём ему ссылку на себя System.out.println(String.format("Кот %s создан. HP: %d", this.name, this.life)); > // Атака. Принимает текущего кота и кота-противника. Метод синхронизирован public static synchronized void attack(Cat thisCat, Cat enemyCat) < // Дополнительная проверка жизни — во избежание конфликта (у кота может не быть жизней) if (thisCat.getLife() 0) < return; > // Если противник имеет жизни if (enemyCat.getLife() > 0) < // Отнимаем жизнь противника enemyCat.decrementLife(); System.out.println(String.format("Кот %s атаковал кота %s. Жизни %, thisCat.getName(), enemyCat.getName(), enemyCat.getLife())); // Если противник не имеет жизней if (enemyCat.getLife() 0) < // Удаляем противника из списка котов Cat.cats.remove(enemyCat); System.out.println(String.format("Кот %s покидает бой.", enemyCat.getName())); System.out.println(String.format("Оставшиеся коты: %s", Cat.cats)); System.out.println(String.format("%s завершает свою работу.", enemyCat.getThread().getName())); // interrupt() — прервать работу треда enemyCat.getThread().interrupt(); > > > // Точка входа в поток @Override public void run() < System.out.println(String.format("Кот %s идёт в бой.", name)); // Пока котов больше 1 while (Cat.cats.size() > 1)< // Атакуем произвольного кота из оставшихся, кроме себя Cat.attack(this, getRandomEnemyCat(this)); > > // Возвращает произвольный объект Cat из cats, кроме самого себя private Cat getRandomEnemyCat(Cat deleteThisCat) < // Создаём лист-копию из основного листа cats List copyCats = new ArrayList<>(Cat.cats); // Удаляем текущего кота, чтобы он не выпал в качестве противника copyCats.remove(deleteThisCat); // Возвращаем произвольного кота из оставшихся с помощью класса util.java.Random return copyCats.get(new Random().nextInt(copyCats.size())); > // Декремент жизней public synchronized void decrementLife() < life--; >// Нужен для корректного вывода @Override public String toString() < return name; > // Геттеры и сеттеры public String getName() < return name; > public int getLife() < return life; > public Thread getThread() < return thread; > > Вывод: Оставшиеся коты: [Tom, Dupli, Toodles] Cleocatra завершает свою работу. Кот Dupli атаковал кота Toodles. Жизни Toodles: 0 Кот Toodles покидает бой. Оставшиеся коты: [Tom, Dupli] Toodles завершает свою работу. Кот Dupli атаковал кота Tom. Жизни Tom: 2 Кот Dupli атаковал кота Tom. Жизни Tom: 1 Кот Dupli атаковал кота Tom. Жизни Tom: 0 Кот Tom покидает бой. Оставшиеся коты: [Dupli] Tom завершает свою работу. Кот-победитель: Dupli. Process finished with exit code 0
Изучите код и запустите его на своём компьютере. Из-за постоянной гонки потоков результаты будут различаться при каждом запуске программы.
Посмотрите, как поведут себя потоки, если дать «котам» 1 или 100 000 жизней. Проверьте, не происходит ли ошибок в вычислениях здоровья и успешно ли завершаются треды.
Также в качестве практики вы можете модернизировать код и поиграться с потоками. Например, добавить новых котов, изменить величину урона или добавить новый класс и подумать, как реализовать механизм их взаимодействия.
Состояния потока и время его жизни
Поток, как и любой другой объект, имеет цикл жизни: он рождается, живёт (готов к выполнению или выполняется), спит (находится в ожидании), и умирает (завершает свою работу).
Получить текущее состояние потока позволяет метод getState(). Он возвращает одно из значений перечисления State, которое содержит набор из 6 констант:
- NEW — новый, только что созданный поток. Это состояние присваивается, когда выделяется память для объекта.
Thread myThread = new Thread();
- RUNNABLE — вызывая метод start(), поток становится готовым к выполнению, а затем выполняемым.
- BLOCKED / WAITING / TIME_WAITING — данные состояния означают, что поток находится в ожидании своего выполнения.
- TERMINATED — после завершения работы поток уничтожается.
В следующем примере на экран выводится несколько состояний потока. Внимательно изучите код и комментарии к нему:
class MyThread implements Runnable< @Override public void run()< // Здесь прописана логика объекта > > public class Main < public static void main(String[] args) < // При создании объект имеет состояние NEW Thread myThread = new Thread(new MyThread()); System.out.println(myThread.getState()); // Нить запускается и переходит в состояние RUNNABLE myThread.start(); System.out.println(myThread.getState()); // main переходит в состояние WAITING try< myThread.join(); // main на этой строчке приостановится, чтобы подождать, пока myThread завершит свою работу в методе run(), и только потом код будет выполняться дальше >catch(InterruptedException e) < e.printStackTrace(); >// Объект завершил свою работу и получил статус TERMINATED System.out.println(myThread.getState()); // После выполнения всех инструкций нить main также становится TERMINATED > > Вывод: NEW RUNNABLE TERMINATED
Как говорилось ранее, когда запускается новый поток, старый продолжает работу. Вызвав метод join(), главный поток перейдёт на время в состояние WAITING, а затем снова станет RUNNABLE. По завершении работы программы все потоки перейдут в состояние TERMINATED.
Есть ещё метод isAlive(), который позволяет узнать, жив поток или нет. Он возвращает логическое значение true или false.
public class Main< public static void main(String[] args)< // Главный поток сейчас живой и выполняется, поэтому выведет true System.out.println("main thread: " + Thread.currentThread().isAlive()); // Новый поток создан, но ещё не запущен (не живой), поэтому вывод будет false System.out.println("new thread: " + new Thread().isAlive()); > > Вывод: main thread: true new thread: false
Что в итоге
В этой статье мы познакомились с понятием многопоточности в Java, прошлись по базовым терминам, узнали, что такое поток, как его создать и в каких состояниях он может пребывать, а также увидели, как работает несколько тредов одновременно, на примере консольного приложения. Повторим основные моменты:
- Потоки — это виртуальные сущности, которые последовательно выполняют код. Они протекают в процессах, где процесс — это программа, которая выполняется.
- Поток можно создать двумя способами: унаследовать класс Thread или реализовать интерфейс Runnable.
- Вся логика нового треда выполняется в методе run(), а запускается он методом start().
- Поток имеет свой жизненный цикл и шесть состояний, описанных в перечислении State.
- State — это свойство класса Thread, которое содержит состояния потока, а получить его можно с помощью метода getState().
- Метод join() переводит в ожидание текущий поток, а interrupt() прерывает его работу.
Многопоточность — важная тема в программировании. Все современные системы используют много потоков для увеличения производительности, а также выполнения нескольких задач одновременно. Поэтому важно понимать, как многопоточность работает.
Читайте также:
- Готовимся к собеседованию: что нужно знать о коллекциях в Java
- Тест. Какой язык создадите вы — Java или Python?
- Начинаем программировать на Python
Декрементирование — уменьшение величины переменной на единицу. Противоположная операция, инкрементирование, — увеличение на единицу.