Чем отличается дедлок от голодания python
Обязательное условие — тупик и голод
Живая блокировка происходит, когда два или более процессов постоянно повторяют одно и то же взаимодействие в ответ на изменения в других процессах, не выполняя никакой полезной работы. Эти процессы не находятся в состоянии ожидания, и они работают одновременно. Это отличается от тупика, потому что в тупике все процессы находятся в состоянии ожидания.
Пример:
Представьте себе пару процессов, использующих два ресурса, как показано:
void process_A( void )
void process_B( void )
Каждому из двух процессов нужны два ресурса, и они используют примитив опроса enter_reg, чтобы попытаться получить необходимые им блокировки. Если попытка не удалась, процесс просто пытается снова.
Если процесс A запускается первым и получает ресурс 1, а затем процесс B запускается и получает ресурс 2, независимо от того, какой из них запускается следующим, он не будет продвигаться дальше, но ни один из двух процессов не блокируется. Что на самом деле происходит, так это то, что он использует свой процессор много раз снова и снова без какого-либо прогресса, но также без какой-либо блокировки. Таким образом, эта ситуация не является тупиковой (поскольку ни один процесс не блокируется), но у нас есть нечто функционально эквивалентное тупиковой ситуации: LIVELOCK.
Что приводит к Livelocks?
Появление блокировок может происходить самым неожиданным образом. Общее количество разрешенных процессов в некоторых системах определяется количеством записей в таблице процессов. Таким образом, слоты таблицы процессов могут упоминаться как конечные ресурсы. В случае сбоя разветвления из-за переполнения таблицы ожидание случайного времени и повторная попытка будут разумным подходом для программы, выполняющей разветвление.
Рассмотрим систему UNIX, имеющую 100 слотов процесса. Десять запущенных программ, каждая из которых должна создать 12 (под) процессов. После того, как каждый процесс создал 9 процессов, 10 исходных процессов и 90 новых процессов исчерпали таблицу. Каждый из 10 оригинальных процессов теперь находится в бесконечном цикле разветвления и сбоя — что вполне соответствует ситуации тупика. Вероятность этого очень мала, но это может произойти.
Разница между тупиком, голоданием и живой блокировкой:
Живая блокировка похожа на тупиковую , за исключением того, что состояния процессов, вовлеченных в живую блокировку , постоянно изменяются относительно друг друга, и ни один из них не прогрессирует. Livelock — это особый случай истощения ресурсов; общее определение только утверждает, что конкретный процесс не прогрессирует.
Deadlocks, Livelocks и Starvation
Продолжаем серию статей о проблемах многопоточности, параллелизме, concurrency и других интересных штуках.
В 1965 году Эдсгер Дейкстра сформулировал задачу об обедающих философах. Задача была иллюстрацией проблем синхронизации при разработке параллельных алгоритмов и техник решения этих проблем.
В задачи были рассмотренный такие проблема, как deadlock, livelock, resource starvation.

Саму задачу и возможные решения можно посмотреть на wiki.
А мы рассмотрим проблемы синхронизации в контексте современных языков программирования.
Deadlock

Что такое deadlock и как избежать таких ошибок?
Deadlock или взаимная блокировка — это ошибка, которая происходит когда процессы имеют циклическую зависимость от пары синхронизированных объектов.

Deadlock — это программа, в которой все параллельные процессы ожидают друг друга. В этом состоянии программа никогда не восстановится без вмешательства извне.
Пример


fatal error: all goroutines are asleep — deadlock!
Отладка взаимных блокировок, как и других ошибок синхронизации, усложняется тем, что для их возникновения нужны специфические условия одновременного выполнения нескольких процессов. Если бы Процесс 1 успел захватить ресурс B до Процесса 2, то ошибка не произошла бы.
Но все не так плохо, оказывается, есть несколько условий, которые должны присутствовать для возникновения взаимных блокировок, и в 1971 году Эдгар Коффман перечислил эти условия в своей статье System Deadlocks. Условия теперь известны как условия Кофмана и являются основой для методов, которые помогают обнаруживать, предотвращать и исправлять взаимные блокировки.

Условия Коффмана
- Условие взаимного исключения. Каждый ресурс в данный момент или отдан ровно одному процессу, или доступен.
- Условие удержания и ожидания. Процессы, в данный момент удерживающие полученные ранее ресурсы, могут запрашивать новые ресурсы.
- Условие отсутствия принудительной выгрузки ресурса. У процесса нельзя принудительным образом забрать ранее полученные ресурсы. Процесс, владеющий ими, должен сам освободить ресурсы.
- Условие циклического ожидания. Должна существовать круговая последовательность из двух и более процессов, каждый из которых ждет доступа к ресурсу, удерживаемому следующим членом последовательности.
Указанные условия являются необходимыми. То есть, если хоть одно из них не выполняется, то взаимных блокировок никогда не возникнет. Достаточность не имеет места быть: если выполняются все четыре условия, блокировка может и не произойти, например, если в системе нет процессов, претендующих на одновременное использование одних и тех же ресурсов.
Диаграммы Холта (Holt).
Отслеживать возникновение взаимных блокировок удобно на диаграммах Холта (Holt). Диаграмма Холта представляет собой направленный граф, имеющий два типа узлов: процессы (показываются кружочками) и ресурсы (показываются квадратиками). Тот факт, что ресурс получен процессом и в данный момент занят этим процессом, указывается ребром (стрелкой) от ресурса к процессу. Ребро, направленное от процесса, к ресурсу, означает, что процесс в данный момент блокирован и находится в состоянии ожидания доступа к соответствующему ресурсу.

Критерий deadlock.
Deadlock имеет место быть, тогда и только тогда, когда диаграмма Холта, отражающая состояния процессов и ресурсов, содержит цикл.
Livelock
Livelock— это программы, которые активно выполняют параллельные операции, но эти операции никак не влияют на продвижение состояния программы вперед.
Ситуация, в которой два или более процессов непрерывно изменяют свои состояния в ответ на изменения в других процессах без какой-либо полезной работы. Это похоже на deadlock, но разница в том, что процессы становятся “вежливыми” и позволяют другим делать свою работу.
Выполнение алгоритмов поиска удаления взаимных блокировок может привести к livelock — взаимная блокировка образуется, сбрасывается, снова образуется, снова сбрасывается и так далее.
Жизненный пример такой ситуации:
Двое встречаются лицом к лицу. Каждый из них пытается посторониться, но они не расходятся, а несколько секунд сдвигаются в одну и ту же сторону.
Вы делаете телефонный звонок, но человек на другом конце тоже пытается вам позвонить. Вы оба повесите трубку и попробуйте снова через одно и то же время, что снова создаст такую же ситуацию. Это может продолжаться вечность.
Рассмотрим простой пример livelock, где муж и жена пытаются поужинать, но между ними только одна ложка. Каждый из супругов слишком вежлив, и передает ложку, если другой еще не ел.

Ложка у которой есть хозяин:
Процесс обеда. Ложка и партнер:
Обедаем пока не утолим голод( isHungry=false ).
- Если ложка сейчас не у нас, то подождем
- Если супруг(а) голодна, то уступим и передадим ложку ему/ей
- Используем ложку и наконец-то обедаем
Поесть этим милым людям не суждено. До третьего блока выполнение не дойдет.
На мой взгляд, обнаружить livelock труднее, чем deadlock, просто потому, что может показаться, что программа работает. Она может реагировать на сигналы, потреблять ресурсы и как то менять состояния, но выйти из цикла и завершить работу уже не в состоянии.
Livelock— это подмножество более широкого набора проблем, называемых Starvation.
Starvation

Starvation — это любая ситуация, когда параллельный процесс не может получить все ресурсы, необходимые для выполнения его работы.
При livelock все параллельные процессы одинаково “голодают”, и никакая работа не выполняется до конца.
В более широком смысле starvation обычно подразумевает наличие одного или нескольких параллельных процессов, которые несправедливо мешают одному или нескольким другим параллельным процессам выполнять работу настолько эффективно, насколько это возможно.
Пример
У нас будет два работника. Один жадный( greedyWorker ), другой вежливый( politeWorker ). Обоим дается одинаковое кол-во времени на их полезную работу — спать по 3 наносекунде.
greedyWorker жадно удерживает общий ресурс( sharedLock ) на протяжении всего цикла работы, тогда как politeWorker пытается блокировать его только тогда, когда это необходимо.
Результат их работы:
За одно и то же время, жадный работник получил почти вдвое больше возможностей выполнять свою работу и владеть общим ресурсом.
Конечно, lock\unlock медленные и в данном примере у politeWorker очень неэффективный код, но голодания может также применяться к процессору, памяти, файловым дескрипторам, соединениям с бд, к любому ресурсу, который должен использоваться совместно.
Если у вас есть параллельный процесс, который настолько жаден, что препятствует эффективно работать другим параллельным процессам, то у вас большая проблема.
Эффективная многопоточность в Python
Хочу поделиться простым рецептом, как можно эффективно выполнять большое число http-запросов и других задач ввода-вывода из обычного Питона. Самое правильное, что можно было бы сделать — использовать асинхронные фреймворки вроде Торнадо или gevent. Но иногда этот вариант не подходит, потому что встроить event loop в уже существующий проект проблематично.
В моем случае уже существовало Django-приложение, из которого примерно раз в месяц нужно было выгрузить немного очень мелких файлов на AWS s3. Шло время, количество файлов стало приближаться к 50 тысячам, и выгружать их по очереди стало утомительным. Как известно, s3 не поддерживает множественное обновление за один PUT-запрос, а установленная опытным путем максимальная скорость запросов с сервера ec2 в том же датацентре не превышает 17 в секунду (что очень не мало, кстати). Таким образом, время обновления для 50 тысяч файлов стало приближаться к одному часу.
Питонисты с детства знают, что от использования потоков (тредов операционной системы) нет никакого толка из-за глобального лока интерпретатора. Но немногие догадываются, что как и любой лок, этот время от времени освобождается. В частности, это происходит при операциях ввода-вывода, в том числе и сетевых. А значит, потоки можно использовать для распараллеливания http-запросов — пока один поток ожидает ответа, другой спокойно обрабатывает результат предыдущего или готовит следующий.
Получается, всего-то нужен пул потоков, который будет выполнять запросы. К счастью, такой пул уже написан. Начиная с версии 3.2 для унификации всей асинхронной работы в Питоне появилась библиотека concurrent.futures . Для второй версии Питона есть бекпорт под именем futures. Код до безобразия прост:
Здесь concurrency — число рабочих потоков, upload — функция, выполняющую саму задачу, queryset — итератор объектов, которые по одному будут передаваться в задачу. Уже этот код при concurrency в 150 смог пропихнуть на сервера Амазона ≈450 запросов в секунду.
Тут необходимо замечание относительно задач: они должны быть потокобезопасны. Т.е. несколько паралельно выполняющихся задач не должны иметь общих ресурсов, либо должны ими правильно управлять. Глобальный лок интерпретатора тут плохой помощник — он не гарантирует, что выполнение потока не прервется в самом неподходящем месте. Если вы пользуетесь только urllib3, requests или boto, волноваться не о чем, они уже потокобезопасны. Про другие библиотеки нужно уточнять. Также потоконебезопасным может оказаться ваш собственный код.
Шло время, количество файлов стало приближаться к 200 тысячам. Как думаете, сколько памяти могут занимать 200 тысяч Django-моделей? А 200 тысяч фьючерсов? А 200 тысяч поставленных задач? Все вместе около гигабайта. Стало понятно, что посылать в экзекутор все сразу — не выход. Но почему бы не добавлять новые задачи по окончании предыдущих? В самом начале добавляем количество задач, равное количеству потоков, ведем учет сколько задач поставлено, сколько выполнено. Сами фьючерсы не храним, наружу не отдаем. Получается очень классная функция, которую можно использовать повторно (осторожно, это не окончательный вариант) :
В ней всего три действия: функция submit , которая выбирает следующий объект из итератора и создает для него задачу, upload_done , которая вызывается по окончании выполнения задачи и ставит следующую, и цикл, в котором ставятся первые задачи. Пробуем запустить:
Отлично, работает! Тут уже используется метод iterator кверисета. Кажется, что его можно было бы использовать и в первом примере с функцией executor.map , но executor.map выбирает сразу весь итератор и делает его бесполезным. Тут же объекты действительно выбираются по одному на каждый работающий поток.
Правда, есть проблема: стоит увеличить кол-во потоков, как начинают сыпаться исключения «ValueError: generator already executing». Код использует один и тот же генератор из всех потоков, поэтому рано или поздно два потока пытаются выбрать значения одновременно (на самом деле это может произойти когда потоков всего два, но с меньшей вероятностью). Это же касается и счетчиков, рано или поздно два процесса одновременно считают одно значение, потом оба прибавят единицу и оба запишут «исходное число + 1», а не «исходное число + 2». Поэтому всю работу с разделяемыми объектами нужно обернуть в локи.
Есть и другие проблемы. Нет обработки ошибок, которые могут произойти во время выполнения задачи. Если прервать выполнение с помощью ctrl+c, в основном потоке будет выброшено исключение, а остальные продолжат выполнение до самого конца, поэтому нужен механизм принудительного завершения очереди. У экзекутора как раз есть метод shutdown для этих целей и можно было бы отдавать экзекутор наружу, чтобы останавливать его, когда пользователь нажимает ctrl+c. Но есть вариант получше: можно создать фьючерс, который будет резолвится по окончании всех работ и подчищать экзекутор, если кто-то извне его отменит. Вот версия, в которой учтены все эти ошибки:
Тут нужно использовать reentrant лок, потому что есть определенная вероятность, что очень короткая задача успеет выполнится до навешивания обработчика в add_done_callback , и тогда обработчик будет выполнен немедленно в том же потоке и попытается еще раз захватить лок. Получится дедлок. Reentrant лок позволит тому же потоку, что захватил его в первый раз, спокойно зайти еще раз, но не даст себя захватить из другого потока, пока первый поток не освободит его столько же раз, сколько захватывал. Немного меняется и код, который использует эту очередь задач:
Больше не нужно тупо засыпать каждые 200 миллисекунд, можно засыпать по умному, ожидая завершения очереди. А в случае прерывания останавливать очередь.
Смеркалось. Шло время, количество файлов стало приближаться к 1,5 миллионам. Несмотря на то, что все выглядело так, как будто все работает с фиксированным потреблением памяти (кол-во тредов, фьючерсов и Django-моделей на протяжении всего выполнения не должно меняться), потребление памяти все равно росло. Оказалось, что queryset.iterator() работает немного не так, как ожидалось. Объекты действительно создаются только тогда, когда явно выбираются из итератора, а вот сырой ответ базы данных все равно выгребается драйвером сразу. Получается около 500 мегабайт на миллион строк. Решение этой проблемы довольно очевидно: нужно делать запросы не на все объекты сразу, а разделять порции. При этом следует избегать выборки со смещением, потому что запрос вида LIMIT 100 OFFSET 200000 на самом деле означает, что СУБД нужно пробежаться по 200100 записям. Вместо смещения следует использовать выборку по полю с индексом.
Здесь pk — скорее pagination key, нежели primary. Впрочем, зачастую primary хорошо подходит на эту роль. Такой итератор действительно расходует фиксированное количество памяти и работает не медленнее выборки за один раз. Но если увеличить кол-во потоков, возникает еще одна проблема. В Джанге соединения с базой данных являются локальными для потоков, поэтому, когда очередной поток делает запрос, создается новое соединение. Рано или поздно количество соединений доходит до критического числа и возникает исключение, подобное этому:
Правильным решением было бы использовать для всех потоков одно и то же соединение, т.к. мы уже ограничили возможность одновременно делать запросы из разных потоков. Стандартных средств для этого в Джанге нет, но это можно сделать с помощью хака, заменив объект threading.local на обычный объект:
Но надо понимать, что это убъет потокобезопасность базы данных во всем остальном приложении, поэтому такой вариант годится только для команд, запускаемых из консоли. Более гуманный вариант — закрывать соединение после каждого запроса, или после каждого элемента, что дает не сильно большой оверхэд.
Есть и третье решение: использовать отдельный поток, который будет общаться с базой данных, отдавая объекты в остальные потоки. Этот вариант ничего не ломает в остальном приложении и не привносит накладных расходов на постоянное переоткрытие соединений. Но его реализация довольно сложна и тянет не меньше чем на отдельную статью.
Возможно, пройдет еще время, кол-во файлов возрастет до 10 миллионов и появятся новые проблемы. Но пока кажется, что основная проблема будет в том, что такое обновление займет около восьми часов и будет стоит $50 только за PUT-запросы по текущим ценам Амазона.
Многопоточное программирование: как одновременно выполнять несколько задач и повышать эффективность программ
Многопоточное программирование – это способ разделения выполнения программы на несколько параллельных потоков, что позволяет эффективно использовать ресурсы процессора и повысить производительность приложения.
Многопоточное программирование: как одновременно выполнять несколько задач и повышать эффективность программ обновлено: 17 сентября, 2023 автором: Научные Статьи.Ру
Помощь в написании работы
Введение
Многопоточное программирование – это подход к разработке программ, в котором задачи выполняются параллельно в разных потоках. Этот подход позволяет эффективно использовать ресурсы компьютера и повышает производительность программы. В данной лекции мы рассмотрим основные понятия и принципы многопоточного программирования, а также рассмотрим примеры использования этого подхода. Мы также обсудим преимущества и проблемы, связанные с многопоточностью, и рассмотрим способы синхронизации потоков для предотвращения возможных конфликтов. Приступим к изучению многопоточного программирования!
Нужна помощь в написании работы?
Мы — биржа профессиональных авторов (преподавателей и доцентов вузов). Наша система гарантирует сдачу работы к сроку без плагиата. Правки вносим бесплатно.
Что такое многопоточное программирование
Многопоточное программирование – это подход к разработке программ, в котором одновременно выполняются несколько потоков исполнения. Поток исполнения – это независимая последовательность команд, которая может выполняться параллельно с другими потоками.
В традиционном однопоточном программировании, код выполняется последовательно, одна команда за другой. Однако, в многопоточном программировании, различные части кода могут выполняться параллельно, что позволяет увеличить производительность и эффективность программы.
Каждый поток исполнения имеет свой собственный стек вызовов и регистры, но разделяет общую память и ресурсы с другими потоками. Это позволяет потокам взаимодействовать друг с другом и совместно использовать данные.
Многопоточное программирование широко используется в современных приложениях, таких как веб-серверы, многопользовательские игры, обработка данных и другие задачи, где параллельное выполнение может значительно улучшить производительность и отзывчивость программы.
Зачем нужно многопоточное программирование
Многопоточное программирование – это подход к разработке программ, в котором задачи разделяются на несколько независимых потоков исполнения, которые могут выполняться параллельно. Этот подход имеет ряд преимуществ и может быть полезен во многих ситуациях.
Увеличение производительности
Одним из основных преимуществ многопоточного программирования является возможность увеличить производительность программы. Параллельное выполнение задач позволяет использовать ресурсы компьютера более эффективно и сократить время выполнения программы. Например, в многопоточном веб-сервере каждый запрос может быть обработан отдельным потоком, что позволяет обслуживать большое количество клиентов одновременно и ускоряет обработку запросов.
Улучшение отзывчивости программы
Многопоточное программирование также может улучшить отзывчивость программы. Когда задачи выполняются параллельно, программа может продолжать работать и отвечать на запросы пользователей, даже если один из потоков занят выполнением длительной операции. Например, в многопоточном графическом интерфейсе пользователь может продолжать взаимодействовать с программой, даже если один из потоков занят обработкой сложных вычислений.
Улучшение масштабируемости
Многопоточное программирование также обеспечивает лучшую масштабируемость программы. При увеличении количества ядер процессора или машин в кластере, можно добавить больше потоков для выполнения задач параллельно. Это позволяет программе эффективно использовать доступные ресурсы и масштабироваться с ростом нагрузки.
Разделение задач
Многопоточное программирование позволяет разделить сложные задачи на более мелкие и независимые подзадачи, которые могут быть выполнены параллельно. Это упрощает разработку и позволяет легче понять и поддерживать код программы. Кроме того, разделение задач позволяет использовать разные алгоритмы и подходы для каждой подзадачи, что может привести к более эффективному решению задачи в целом.
Взаимодействие и совместное использование данных
Многопоточное программирование позволяет потокам взаимодействовать друг с другом и совместно использовать данные. Это позволяет решать задачи, требующие синхронизации и координации между потоками. Например, в многопоточной базе данных несколько потоков могут одновременно выполнять запросы и обновления данных, что повышает эффективность работы с базой данных.
В целом, многопоточное программирование является мощным инструментом, который позволяет увеличить производительность, улучшить отзывчивость и масштабируемость программы, а также упростить разработку и обеспечить взаимодействие между потоками.
Преимущества многопоточного программирования
Многопоточное программирование имеет ряд преимуществ, которые делают его полезным и эффективным инструментом для разработки программ. Рассмотрим некоторые из них:
Повышение производительности
Одним из основных преимуществ многопоточного программирования является возможность распараллеливания задач. Когда программа выполняет несколько задач одновременно в разных потоках, это позволяет использовать ресурсы процессора более эффективно и ускоряет выполнение программы. Например, в многопоточном приложении один поток может заниматься обработкой пользовательского ввода, а другой – выполнением вычислений, что позволяет сократить время отклика программы.
Улучшение отзывчивости
Многопоточное программирование позволяет создавать отзывчивые приложения, которые могут выполнять несколько задач одновременно. Например, в графическом интерфейсе пользователя один поток может отвечать за отрисовку графики, а другой – за обработку пользовательского ввода. Это позволяет приложению оставаться отзывчивым и отвечать на действия пользователя независимо от выполнения других задач.
Упрощение разработки
Многопоточное программирование может упростить разработку сложных программ. Разделение программы на отдельные потоки позволяет разработчику сосредоточиться на решении конкретных задач, а не на управлении всеми аспектами программы. Кроме того, многопоточное программирование позволяет создавать модули, которые могут быть повторно использованы в разных контекстах, что упрощает разработку и поддержку программного кода.
Взаимодействие между потоками
Многопоточное программирование предоставляет механизмы для взаимодействия между потоками. Это позволяет потокам обмениваться данными, синхронизировать свою работу и координировать выполнение задач. Например, потоки могут использовать разделяемую память для обмена данными или использовать механизмы синхронизации, такие как блокировки или семафоры, для предотвращения конфликтов при доступе к общим ресурсам.
В целом, многопоточное программирование предоставляет мощный инструмент для разработки эффективных и отзывчивых программ. Однако, при использовании многопоточности необходимо учитывать потенциальные проблемы, такие как состояние гонки и взаимная блокировка, и применять соответствующие методы синхронизации и управления потоками для обеспечения корректной и безопасной работы программы.
Основные понятия и термины в многопоточном программировании
Многопоточное программирование включает в себя ряд основных понятий и терминов, которые важно понимать для эффективного использования этой техники. Ниже приведены некоторые из них:
Поток (Thread)
Поток – это независимая последовательность инструкций, которая может выполняться параллельно с другими потоками в рамках одного процесса. Каждый поток имеет свой собственный стек вызовов и регистры, но разделяет общую память с другими потоками в пределах процесса.
Параллелизм (Concurrency)
Параллелизм – это способность программы или системы выполнять несколько задач одновременно. В многопоточном программировании, параллелизм достигается путем запуска нескольких потоков, которые могут выполняться параллельно на многоядерном процессоре или в многопроцессорной системе.
Синхронизация (Synchronization)
Синхронизация – это механизм, который обеспечивает правильное взаимодействие и согласованность работы нескольких потоков. Он используется для предотвращения состояния гонки и других проблем, которые могут возникнуть при одновременном доступе к общим ресурсам.
Состояние гонки (Race Condition)
Состояние гонки – это ситуация, когда несколько потоков одновременно пытаются изменить общий ресурс или переменную, что может привести к непредсказуемым и нежелательным результатам. Для предотвращения состояния гонки необходимо использовать механизмы синхронизации, такие как блокировки или семафоры.
Взаимная блокировка (Deadlock)
Взаимная блокировка – это ситуация, когда два или более потока ожидают друг друга, чтобы освободить ресурсы, которые они заблокировали. В результате, все потоки оказываются заблокированными и программа не может продолжить свою работу. Взаимная блокировка может возникнуть, если потоки неправильно управляют блокировками или семафорами.
Критическая секция (Critical Section)
Критическая секция – это участок кода, в котором происходит доступ к общим ресурсам или переменным. Для обеспечения безопасности и предотвращения состояния гонки, критическая секция должна быть синхронизирована с помощью механизмов блокировки или других методов синхронизации.
Это лишь некоторые из основных понятий и терминов, связанных с многопоточным программированием. Понимание этих понятий поможет вам разрабатывать безопасные и эффективные многопоточные программы.
Создание и управление потоками
Поток – это независимая последовательность инструкций, которая может выполняться параллельно с другими потоками внутри одного процесса. Создание и управление потоками является важной частью многопоточного программирования.
Создание потоков
В большинстве языков программирования есть встроенные средства для создания потоков. Например, в Java можно создать поток, наследуясь от класса Thread и переопределив метод run(). В Python можно использовать модуль threading и создать поток, передавая функцию в конструктор класса Thread.
При создании потока необходимо указать точку входа, то есть метод или функцию, которая будет выполняться в потоке. Этот метод или функция должны содержать код, который будет выполняться параллельно с другими потоками.
Управление потоками
Управление потоками включает в себя планирование выполнения потоков, приостановку, возобновление и завершение потоков.
Планирование выполнения потоков – это процесс, при котором операционная система определяет, какие потоки будут выполняться в данный момент времени. Операционная система может использовать различные алгоритмы планирования для определения порядка выполнения потоков.
Приостановка и возобновление потоков позволяют временно остановить выполнение потока и затем возобновить его позже. Это может быть полезно, например, для синхронизации потоков или для управления ресурсами.
Завершение потоков происходит, когда поток завершает выполнение своей задачи или когда явно вызывается метод для завершения потока. При завершении потока его ресурсы освобождаются и он больше не может быть запущен.
Важно учитывать, что при работе с потоками необходимо обеспечить безопасность и синхронизацию доступа к общим ресурсам или переменным. Это может быть достигнуто с помощью механизмов блокировки, семафоров или других методов синхронизации.
Создание и управление потоками – это основа многопоточного программирования. Правильное использование потоков позволяет эффективно использовать ресурсы и повысить производительность программы.
Синхронизация потоков
Синхронизация потоков – это процесс координации выполнения нескольких потоков, чтобы они могли безопасно работать с общими ресурсами или переменными. Без правильной синхронизации возникают проблемы, такие как состояние гонки и взаимная блокировка.
Состояние гонки
Состояние гонки возникает, когда несколько потоков пытаются одновременно получить доступ к общему ресурсу или переменной и изменить ее значение. Это может привести к непредсказуемым результатам и ошибкам в программе. Для предотвращения состояния гонки необходимо использовать механизмы синхронизации, такие как блокировки или семафоры.
Блокировки
Блокировка – это механизм синхронизации, который позволяет потоку получить эксклюзивный доступ к общему ресурсу или переменной. Когда поток получает блокировку, другие потоки должны ждать, пока блокировка не будет освобождена. Это гарантирует, что только один поток может работать с общим ресурсом в определенный момент времени.
Пример использования блокировок:
“`java
Lock lock = new ReentrantLock();
lock.lock(); // получение блокировки
try // работа с общим ресурсом
> finally lock.unlock(); // освобождение блокировки
>
“`
Семафоры
Семафор – это механизм синхронизации, который позволяет ограничить количество потоков, которые могут одновременно получить доступ к общему ресурсу или переменной. Семафор содержит счетчик, который указывает, сколько потоков может получить доступ к ресурсу. Когда поток получает доступ, счетчик уменьшается, а когда поток освобождает доступ, счетчик увеличивается.
Пример использования семафора:
“`java
Semaphore semaphore = new Semaphore(2); // ограничение на 2 потока
try semaphore.acquire(); // получение доступа
// работа с общим ресурсом
> finally semaphore.release(); // освобождение доступа
>
“`
Взаимная блокировка
Взаимная блокировка возникает, когда два или более потока блокируются и ожидают друг друга, чтобы освободить ресурсы, которые они заблокировали. Это может привести к зависанию программы и непредсказуемому поведению. Для предотвращения взаимной блокировки необходимо правильно управлять блокировками и ресурсами.
Синхронизация потоков является важной частью многопоточного программирования. Правильное использование механизмов синхронизации позволяет избежать проблем и обеспечить безопасность работы с общими ресурсами.
Проблемы и решения в многопоточном программировании
Гонки данных (Race conditions)
Гонки данных возникают, когда два или более потока пытаются одновременно получить доступ к общему ресурсу и изменить его. Это может привести к непредсказуемым результатам, таким как некорректные значения или потеря данных.
Для решения проблемы гонок данных можно использовать механизмы синхронизации, такие как блокировки (locks) или мьютексы (mutexes), чтобы гарантировать, что только один поток может получить доступ к ресурсу в определенный момент времени.
Взаимная блокировка (Deadlock)
Взаимная блокировка возникает, когда два или более потока блокируются и ожидают друг друга, чтобы освободить ресурсы, которые они заблокировали. Это может привести к зависанию программы и непредсказуемому поведению.
Для предотвращения взаимной блокировки необходимо правильно управлять блокировками и ресурсами. Например, можно использовать стратегию “не держи больше одной блокировки одновременно” или использовать таймауты для освобождения блокировок, если они не могут быть получены в течение определенного времени.
Голодание потоков (Thread starvation)
Голодание потоков возникает, когда один или несколько потоков не получают достаточно ресурсов для выполнения своей работы. Это может произойти, например, если один поток постоянно захватывает ресурсы и не освобождает их для других потоков.
Для решения проблемы голодания потоков можно использовать различные стратегии планирования, такие как очереди задач или приоритеты потоков, чтобы обеспечить справедливое распределение ресурсов между потоками.
Несогласованность данных (Data inconsistency)
Несогласованность данных возникает, когда несколько потоков одновременно изменяют общие данные и не синхронизируют свои операции. Это может привести к некорректным или непредсказуемым значениям данных.
Для решения проблемы несогласованности данных необходимо использовать механизмы синхронизации, такие как блокировки или атомарные операции, чтобы гарантировать, что только один поток может изменять данные в определенный момент времени.
Оверхед многопоточности (Multithreading overhead)
Оверхед многопоточности возникает из-за дополнительных затрат на создание и управление потоками. Это может привести к ухудшению производительности программы, особенно если количество потоков превышает количество доступных ядер процессора.
Для уменьшения оверхеда многопоточности можно использовать пулы потоков или асинхронные операции, чтобы эффективно использовать ресурсы процессора и уменьшить количество создаваемых потоков.
Примеры использования многопоточного программирования
Параллельная обработка данных
Многопоточное программирование может быть полезно при обработке больших объемов данных. Например, если у вас есть задача обработки изображений, вы можете создать несколько потоков, каждый из которых будет обрабатывать отдельную часть изображения. Это позволит ускорить обработку и сократить время выполнения задачи.
Сетевое программирование
Многопоточное программирование также широко используется в сетевых приложениях. Например, веб-сервер может создавать отдельный поток для каждого подключенного клиента, чтобы обрабатывать его запросы параллельно. Это позволяет серверу эффективно обслуживать множество клиентов одновременно.
Многозадачность
Многопоточное программирование также используется для реализации многозадачности. Например, в операционных системах могут быть созданы отдельные потоки для выполнения различных задач, таких как обработка пользовательского ввода, отрисовка графического интерфейса и выполнение фоновых задач. Это позволяет пользователям взаимодействовать с приложением плавно и без задержек.
Вычисления в реальном времени
Многопоточное программирование может быть полезно при выполнении вычислений в реальном времени, таких как обработка аудио или видео. Создание отдельных потоков для обработки каждого кадра или сэмпла позволяет обеспечить плавное воспроизведение и быструю обработку данных.
Параллельное программирование на многоядерных процессорах
Многопоточное программирование особенно полезно на многоядерных процессорах, где каждое ядро может выполнять свою задачу параллельно. Это позволяет эффективно использовать ресурсы процессора и ускорить выполнение задач.
Это лишь некоторые примеры использования многопоточного программирования. В целом, многопоточность может быть полезна во многих ситуациях, где требуется параллельное выполнение задач для повышения производительности и эффективности программы.
Таблица сравнения многопоточного программирования
| Аспект | Однопоточное программирование | Многопоточное программирование |
|---|---|---|
| Определение | Программа выполняется последовательно в одном потоке | Программа может выполняться параллельно в нескольких потоках |
| Задачи | Решение одной задачи за раз | Решение нескольких задач одновременно |
| Производительность | Ограничена процессором и временем выполнения | Может быть увеличена за счет параллельного выполнения задач |
| Ресурсы | Использует только один процессор и память | Может использовать несколько процессоров и разделяемую память |
| Синхронизация | Не требуется синхронизация между потоками | Требуется синхронизация для предотвращения гонок данных и других проблем |
| Примеры | Простые скрипты, однопоточные приложения | Серверы, многозадачные приложения, параллельные вычисления |
Заключение
Многопоточное программирование – это подход, который позволяет выполнять несколько задач одновременно в рамках одной программы. Оно полезно в ситуациях, когда требуется эффективное использование ресурсов и повышение производительности. В многопоточном программировании важно уметь создавать и управлять потоками, а также синхронизировать их работу. Несмотря на преимущества, многопоточное программирование также может столкнуться с проблемами, такими как состояние гонки и взаимная блокировка, но существуют различные методы и инструменты для их решения. Примеры использования многопоточного программирования включают параллельную обработку данных, многопоточные серверы и многопоточные алгоритмы. Важно понимать основные понятия и принципы многопоточного программирования, чтобы эффективно использовать его в своих проектах.
Многопоточное программирование: как одновременно выполнять несколько задач и повышать эффективность программ обновлено: 17 сентября, 2023 автором: Научные Статьи.Ру
Livelock: в чем, пример, разница с Deadlock
A динамический тупик Это ситуация, когда запрос на монопольную блокировку неоднократно отклоняется, поскольку множество перекрывающихся общих блокировок продолжают мешать друг другу. Процессы продолжают менять свой статус, что еще больше мешает им выполнить задачу. Это еще больше мешает им выполнить задачу.
Примеры Livelock
Самый простой пример Лайвлока — два человека, которые встречаются лицом к лицу в коридоре, и оба отходят в сторону, чтобы пропустить другого. В конечном итоге они перемещаются из стороны в сторону, не добиваясь никакого прогресса, поскольку в каждый момент времени они движутся одинаково. Здесь они никогда не пересекаются друг с другом.

На изображении выше вы можете видеть, что каждому из двух данных процессов требуется два ресурса, и они используют примитивный опрос входа в реестр, чтобы попытаться получить необходимые им блокировки. Если попытка не удалась, метод снова сработает.
- Процесс A удерживает ресурс Y
- Процесс B содержит ресурс X
- Процессу A требуется ресурс X
- Процесс B требует ресурса Y
Предположим, что сначала запускается процесс A и получает ресурс данных X, а затем запускается процесс B и получает ресурс Y, независимо от того, какой процесс запускается первым, ни один из них не продвигается дальше.
Однако ни один из двух процессов не блокируется. Они неоднократно используют ресурсы ЦП без какого-либо прогресса, но также останавливают любой блок обработки.
Таким образом, данная ситуация не является ситуацией тупик потому что нет ни одного заблокированного процесса, но мы сталкиваемся с ситуацией, эквивалентной взаимоблокировке, то есть LIVELOCK.
Что приводит к Livelock?
Живая блокировка возникает, когда общее количество разрешенных процессов в конкретной системе должно определяться общим количеством записей в таблице процессов. Поэтому слоты таблицы процессов следует называть конечными ресурсами.
Что такое тупик?
Взаимная блокировка — это ситуация, которая возникает в ОС, когда какой-либо процесс переходит в состояние ожидания, поскольку другой ожидающий процесс удерживает требуемый ресурс. Взаимная блокировка — это распространенная проблема в многопроцессорной обработке, когда несколько процессов используют определенный тип взаимоисключающего ресурса, известного как программная блокировка или программное обеспечение.
Пример тупика

- Реальным примером может служить движение транспорта только в одном направлении.
- Здесь мост считается ресурсом.
- Таким образом, когда происходит тупик, его можно легко решить, если одна машина даст задний ход (вытеснение ресурсов и откат).
- В случае возникновения тупиковой ситуации может потребоваться резервное копирование нескольких автомобилей.
- Так что голодание возможно.
Что такое голодание?
«Голод» — это ситуация, когда все процессы с низким приоритетом блокируются, а процессы с высоким приоритетом продолжаются. В любой системе запросы к ресурсам с высоким/низким приоритетом продолжают происходить динамически. Таким образом, требуется определенная политика, чтобы решить, кто и когда получит поддержку.
При использовании некоторых алгоритмов некоторые процессы могут не получить желаемого обслуживания, даже если они не зашли в тупик. Недостаточность возникает, когда некоторые потоки делают общие ресурсы недоступными в течение длительного периода времени.
Пример голодания
Например, объект предлагает синхронизированный метод, возврат которого может занять много времени. Если один поток часто использует этот метод, другие потоки, которым также требуется частый синхронизированный доступ к тому же объекту, часто будут блокироваться.
Разница между тупиком, голоданием и Livelock
- Взаимная блокировка — это ситуация, которая возникает в ОС, когда какой-либо процесс переходит в состояние ожидания, поскольку требуемый ресурс удерживается другим ожидающим процессом.
- С другой стороны, активная блокировка почти похожа на тупиковую, за исключением того, что состояния процессов, участвующих в динамической блокировке, постоянно меняются друг на друга, ни один из них не прогрессирует.
- Итак, Livelock — это уникальный случай нехватки ресурсов.
Итоги
- Определение: Livelock — это ситуация, когда запрос на эксклюзивную блокировку неоднократно отклоняется, поскольку множество перекрывающихся общих блокировок продолжают мешать друг другу.
- Livelock возникает, когда общее количество разрешенных процессов в конкретной системе должно определяться общим количеством записей в таблице процессов.
- Взаимная блокировка — это ситуация, которая возникает в ОС, когда какой-либо процесс переходит в состояние ожидания, поскольку другой ожидающий процесс удерживает требуемый ресурс.
- Реальным примером может служить движение транспорта только в одном направлении.
- Примером Лайвлока могут быть два человека, которые встречаются лицом к лицу в коридоре, и оба отходят в сторону, чтобы пропустить другого.
- «Голод» — это ситуация, когда все процессы с низким приоритетом блокируются, а процессы с высоким приоритетом продолжаются.
- Алгоритм циклического планирования с примером
- Синхронизация процессов: проблема критической секции в ОС
- Планирование процессов в ОС: долгосрочный, средний, краткосрочный планировщик
- Алгоритм приоритетного планирования: упреждающий, невытесняющий ПРИМЕР
- SSD против HDD: в чем разница между SSD и HDD
Разбор основных концепций параллелизма
Завтра у нас плавненько стартует практически юбилейный поток курс «Разработчик Java» — уже шестой по счёту начиная с апреля прошлого года. А это значит, что мы снова подобрали, перевели интереснейший материал, которым делимся с вами.
Эта памятка поможет Java-разработчикам, работающим с многопоточными программами, понять основные концепции параллелизма и способы их применения. Вы ознакомьтесь с ключевыми аспектами языка Java со ссылками на стандартную библиотеку.
С момента своего создания Java поддерживает ключевые концепции параллелизма, такие как потоки и блокировки. Эта памятка поможет Java-разработчикам, работающим с многопоточными программами, понять основные концепции параллелизма и способы их применения.
| Концепция | Описание |
|---|---|
| Атомарная операция — это операция, которая выполняется полностью или не выполняется совсем, частичное выполнение невозможно. | |
| Visibility (видимость) | Условия, при которых один поток видит изменения, сделанные другим потоком |
Таблица 1: Концепции параллелизма

Состояние гонки (Race condition)
Состояние гонки возникает, когда один и тот же ресурс используется несколькими потоками одновременно, и в зависимости от порядка действий каждого потока может быть несколько возможных результатов. Код, приведенный ниже, не является потокобезопасным, и переменная value может быть инициализирована больше, чем один раз, так как check-then-act (проверка на null , а затем инициализация), которая лениво инициализирует поле, не является атомарной:
class Lazy < private volatile T value; T get() < if (value == null) value = initialize(); return value; >>
Гонка данных (Data race)
Гонка данных возникает, когда два или более потока пытаются получить доступ к одной и той же не финальной переменной без синхронизации. Отсутствие синхронизации может привести к внесению изменений, которые не будут видны другим потокам, из-за этого возможно чтение устаревших данных, что, в свою очередь, приводит к бесконечным циклам, поврежденным структурам данных или неточным вычислениям. Этот код может привести к бесконечному циклу, потому что считывающий поток может так и не заметить изменения, внесенные перезаписывающими потоками:
class Waiter implements Runnable < private boolean shouldFinish; void finish() < shouldFinish = true; >public void run() < long iteration = 0; while (!shouldFinish) < iteration++; >System.out.println("Finished after: " + iteration); > > class DataRace < public static void main(String[] args) throws InterruptedException < Waiter waiter = new Waiter(); Thread waiterThread = new Thread(waiter); waiterThread.start(); waiter.finish(); waiterThread.join(); >>
Модель памяти Java: отношение happens-before
Модель памяти Java определяется с точки зрения таких действий, как чтение/запись полей и синхронизация в мониторе. Действия упорядочены с помощью отношения happens-before (выполняется прежде), которое может быть использовано для объяснения того, когда поток видит результат действий другого потока, и что представляет собой правильно синхронизированная программа.
ОТНОШЕНИЯ HAPPENS-BEFORE ИМЕЮТ СЛЕДУЮЩИЕ СВОЙСТВА:
- Вызов Thread#start происходит до любого действия в этом потоке.
- Возврат монитора происходит до любого последующего захвата этого же монитора.
- Запись в volatile-переменную происходит до любого последующего считывания volatile-переменной.
- Запись в final-переменную происходит до публикации ссылки объекта.
- Все действия в потоке выполняются до возвращения из Thread#join в этом потоке.

Изображение 1: Пример happens-before
Стандартные функции синхронизации
Ключевое слово synchronized
Ключевое слово synchronized используется для предотвращения одновременного выполнения разными потоками одного и того же блока кода. Оно гарантирует, что, если вы получили блокировку (войдя в синхронизированный блок), данные, на которые наложена эта блокировка, обрабатываются в эксклюзивном режиме, поэтому операция может считаться атомарной. Кроме того, оно гарантирует, что другие потоки увидят результат операции после того, как получат такую же блокировку.
class AtomicOperation < private int counter0; private int counter1; void increment() < synchronized (this) < counter0++; counter1++; >> >
Ключевое слово synchronized можно также раскрыть на уровне методов.
| ССЫЛКА, ИСПОЛЬЗУЕМАЯ КАК МОНИТОР | |
|---|---|
| static | ссылка на объект Class |
| non-static | this-ссылка |
Таблица 2: Мониторы, которые используются, когда весь метод синхронизирован
Блокировка реентерабельна (reentrant), поэтому, если поток уже содержит блокировку, он может успешно получить ее снова.
class Reentrantcy < synchronized void doAll() < doFirst(); doSecond(); >synchronized void doFirst() < System.out.println("First operation is successful."); >synchronized void doSecond() < System.out.println("Second operation is successful."); >>
Уровень соперничества влияет на способ захвата монитора:
| Описание | |
|---|---|
| init | Только что создан, пока никем не был захвачен. |
| biased | Борьбы нет, и код, защищенный блокировкой, выполняется только одним потоком. Самый дешевый для захвата. |
| thin | Монитор захватывается несколькими потоками без борьбы. Для блокировки используется сравнительно дешевый CAS. |
| fat | Возникает борьба. JVM запрашивает мьютексы ОС и позволяет планировщику ОС обрабатывать парковки потоков и пробуждения. |
Таблица 3: Состояния мониторов
Методы wait/notify/notifyAll объявляются в классе Object . wait используется, чтобы заставить поток перейти в состояние WAITING или TIMED_WAITING (если передано значение тайм-аута). Чтобы разбудить поток, можно сделать любое из этих действий:
- Другой поток вызывает notify, который пробуждает произвольный поток, ожидающий на мониторе.
- Другой поток вызывает notifyAll, который пробуждает все потоки, ожидающие на мониторе.
- Вызывается Thread#interrupt. В этом случае бросается исключение InterruptedException.
class ConditionLoop < private boolean condition; synchronized void waitForCondition() throws InterruptedException < while (!condition) < wait(); >> synchronized void satisfyCondition() < condition = true; notifyAll(); >>
- Имейте в виду, что для того, чтобы использовать wait/notify/notifyAll для объекта, вам необходимо сначала наложить блокировку на этот объект.
- Всегда ждите внутри цикла, проверяющего условие, выполнение которого вы ожидаете. Это касается проблемы синхронизации, если другой поток удовлетворяет условию до начала ожидания. Кроме того, это защищает ваш код от побочных пробуждений, которые могут (и будут) происходить.
- Всегда проверяйте, что вы удовлетворяете условию ожидания перед вызовом notify/notifyAll. Несоблюдение этого требования приведет к уведомлению, но поток не сможет избежать цикла ожидания.
volatile решает проблему видимости и делает изменение значения атомарным, потому что здесь есть отношение happens-before: запись в volatile-переменную происходит до любого последующего считывания volatile-переменной. Таким образом, оно гарантирует, что при последующем считывании поля будет видно значение, которое было задано самой последней записью.
class VolatileFlag implements Runnable < private volatile boolean shouldStop; public void run() < while (!shouldStop) < //do smth >System.out.println("Stopped."); > void stop() < shouldStop = true; >public static void main(String[] args) throws InterruptedException < VolatileFlag flag = new VolatileFlag(); Thread thread = new Thread(flag); thread.start(); flag.stop(); thread.join(); >>
Атомарность
Пакет java.util.concurrent.atomic содержит набор классов, которые поддерживают составные атомарные действия над одним значением без блокировок, подобно volatile .
Используя классы AtomicXXX, можно реализовать атомарную операцию check-then-act :
class CheckThenAct < private final AtomicReferencevalue = new AtomicReference<>(); void initialize() < if (value.compareAndSet(null, "Initialized value")) < System.out.println("Initialized only once."); >> >
И AtomicInteger , и AtomicLong имеют атомарную операцию инкремента/декремента:
class Increment < private final AtomicInteger state = new AtomicInteger(); void advance() < int oldState = state.getAndIncrement(); System.out.println("Advanced: '" + oldState + "' ->'" + (oldState + 1) + "'."); > >
Если вам нужен счетчик и нет необходимости получать его значение атомарно, подумайте об использовании LongAdder вместо AtomicLong/AtomicInteger . LongAdder обрабатывает значение в нескольких ячейках и увеличивает их число, если нужно, и, следовательно, он работает лучше при высокой конкуренции.
ThreadLocal
Один из способов хранить данные в потоке и сделать блокировку необязательной — это использовать хранилище ThreadLocal . Концептуально ThreadLocal действует так, как будто в каждом потоке есть своя версия переменной. ThreadLocal обычно используется для фиксации значений каждого потока, таких как «текущая транзакция», или других ресурсов. Кроме того, они используются для содержания поточных счетчиков, статистики или генераторов идентификаторов.
class TransactionManager < private final ThreadLocalcurrentTransaction = ThreadLocal.withInitial(NullTransaction::new); Transaction currentTransaction() < Transaction current = currentTransaction.get(); if (current.isNull()) < current = new TransactionImpl(); currentTransaction.set(current); >return current; > >
Безопасная публикация
Публикация объекта делает его ссылку доступной за пределами текущей области (например, возврат ссылки из геттера). Обеспечение безопасной публикации объекта (только когда он полностью создан) может потребовать синхронизации. Безопасность публикации может быть достигнута с использованием:
- Статических инициализаторов. Только один поток может инициализировать статические переменные, поскольку инициализация класса выполняется под исключительной блокировкой.
class StaticInitializer < // Публикация неизменяемого объекта без дополнительной инициализации public static final Year year = Year.of(2017); public static final Setkeywords; // Использование статического инициализатора для построения сложного объекта static < // Создание изменяемого множества SetkeywordsSet = new HashSet<>(); // Состояние инициализации keywordsSet.add("java"); keywordsSet.add("concurrency"); // Делаем множество немодифицируемым keywords = Collections.unmodifiableSet(keywordsSet); > >
- Volatile-поля. Считывающий поток всегда будет считывать последнее значение, потому что запись в volatile-переменную происходит до (happens before) любого последующего чтения.
class Volatile < private volatile String state; void setState(String state) < this.state = state; >String getState() < return state; >>
- Атомарности. Например, AtomicInteger сохраняет значение в volatile-поле, поэтому правило для volatile-переменных здесь тоже применимо.
class Atomics < private final AtomicInteger state = new AtomicInteger(); void initializeState(int state) < this.state.compareAndSet(0, state); >int getState() < return state.get(); >>
class Final < private final String state; Final(String state) < this.state = state; >String getState() < return state; >>
Убедитесь, что this-ссылка не испарилась во время создания.
class ThisEscapes < private final String name; ThisEscapes(String name) < Cache.putIntoCache(this); this.name = name; >String getName() < return name; >> class Cache < private static final MapCACHE = new ConcurrentHashMap<>(); static void putIntoCache(ThisEscapes thisEscapes) < // 'this' ссылка испарилась прежде, чем объект полностью сконструирован. CACHE.putIfAbsent(thisEscapes.getName(), thisEscapes); >>
- Правильно синхронизированных полей.
class Synchronization < private String state; synchronized String getState() < if (state == null) state = "Initial"; return state; >>
Неизменяемые объекты
Одним из самых замечательных свойств неизменяемых объектов является потокобезопасность, поэтому синхронизация для них не нужна. Требования к неизменному объекту:
- Все поля являются final-полями.
- Все поля должны быть либо изменчивыми, либо неизменяемыми объектами, но не выходить за пределы объекта, поэтому состояние объекта не может быть изменено после создания.
- Ссылка this не исчезает во время создания.
- Класс является final-классом, поэтому переопределение его поведения в подклассах невозможно.
// Помечается как final - подклассы запрещены public final class Artist < // Неизменяемый объект, поле final private final String name; // Коллекция неизменяемых объектов, final поле private final Listtracks; public Artist(String name, List tracks) < this.name = name; // Защитная копия Listcopy = new ArrayList<>(tracks); // Превращение изменяемой коллекции в неизменяемую this.tracks = Collections.unmodifiableList(copy); // 'this' никуда не передается во время создания > // Getters, equals, hashCode, toString > //Помечается как final - запрещается наследование public final class Track < //Неизменяемый объект, поле final private final String title; public Track(String title) < this.title = title; >// Getters, equals, hashCode, toString >
Класс java.lang.Thread используется для представления приложения или потока JVM. Код всегда выполняется в контексте некоторого класса Thread (чтобы получить текущий поток вы можете использовать Thread#currentThread()).
| Описание | |
|---|---|
| NEW | Не запускался. |
| Запущен и работает. | |
| BLOCKED | Ожидание на мониторе — он пытается получить блокировку и войти в критическую секцию. |
| WAITING | Ожидание выполнения определенного действия другим потоком (notify/notifyAll, LockSupport#unpark). |
| То же, что и WAITING, но с таймаутом. | |
| TERMINATED | Остановлен |
Таблица 4: Состояния потоков
| Описание | |
|---|---|
| start | Запускает экземпляр класса Thread и выполняет метод run(). |
| join | Блокирует до окончания потока. |
| interrupt | Прерывает поток. Если поток заблокирован в методе, который отвечает на прерывания, в другом потоке будет брошен InterruptedException, в противном случае будет установлен статус прерывания. |
| stop, suspend, resume, destroy | Все эти методы устарели. Они выполняют опасные операции в зависимости от состояния рассматриваемого потока. Вместо них используйте Thread#interrupt() или флаг volatile, чтобы указать потоку, что он должен делать |
Таблица 5: Thread coordination methods Методы координации потоков
Как обрабатывать InterruptedException?
- Очистите все ресурсы и завершите выполнение потока, если это возможно на текущем уровне.
- Объявите, что текущий метод бросает InterruptedException.
- Если метод не порождает исключение InterruptedException, прерванный флаг должен быть восстановлен в true, вызывая Thread.currentThread().interrupt() и должно быть порождено исключение, которое является более подходящим на этом уровне. Очень важно вернуть флаг true, чтобы дать возможность обрабатывать прерывания на более высоком уровне.
В потоках может указываться UncaughtExceptionHandler , который получит уведомление о любом неперехваченном исключении, из-за которого поток прерывается.
Thread thread = new Thread(runnable); thread.setUncaughtExceptionHandler((failedThread, exception) -> < logger.error("Caught unexpected exception in thread '<>'.", failedThread.getName(), exception); >); thread.start();
Жизнеспособность (Liveness)
Deadlock , или взаимная блокировка, возникает, когда есть несколько потоков и каждый ожидает ресурс, принадлежащий другому потоку, так что формируется цикл из ресурсов и ожидающих их потоков. Наиболее очевидным видом ресурса является монитор объекта, но любой ресурс, который вызывает блокировку (например, wait/notify ), также подходит.
Пример потенциального дэдлока:
class Account < private long amount; void plus(long amount) < this.amount += amount; >void minus(long amount) < if (this.amount < amount) throw new IllegalArgumentException(); else this.amount -= amount; >static void transferWithDeadlock(long amount, Account first, Account second) < synchronized (first) < synchronized (second) < first.minus(amount); second.plus(amount); >> > >
Взаимная блокировка происходит, если в одно и то же время:
- Один поток пытается перенести данные с одного аккаунта на другой и уже наложил блокировку на первый аккаунт.
- Другой поток пытается перенести данные со второго аккаунта на первый, и уже наложил блокировку на второй аккаунт.
- Порядок блокировок — всегда накладывайте блокировки в одном и том же порядке.
class Account < private long id; private long amount; // Некоторые методы опущены static void transferWithLockOrdering(long amount, Account first, Account second)< boolean lockOnFirstAccountFirst = first.id < second.id; Account firstLock = lockOnFirstAccountFirst ? first : second; Account secondLock = lockOnFirstAccountFirst ? second : first; synchronized (firstLock) < synchronized (secondLock) < first.minus(amount); second.plus(amount); >> > >
- Блокировка с тайм-аутом — не блокируйте бессрочно при наложении блокировки, лучше как можно быстрее снимите все блокировки и попробуйте снова.
class Account < private long amount; // Некоторые методы опущены static void transferWithTimeout( long amount, Account first, Account second, int retries, long timeoutMillis ) throws InterruptedException < for (int attempt = 0; attempt < retries; attempt++) < if (first.lock.tryLock(timeoutMillis, TimeUnit.MILLISECONDS)) < try < if (second.lock.tryLock(timeoutMillis, TimeUnit.MILLISECONDS)) < try < first.minus(amount); second.plus(amount); >finally < second.lock.unlock(); >> > finally < first.lock.unlock(); >> > > >
JVM способен обнаруживать взаимные блокировки мониторов и выводить информацию о них в дампах потоков.
Livelock и потоковое голодание
Livelock возникает, когда потоки тратят все свое время на переговоры о доступе к ресурсу или обнаруживают и избегают тупиковой ситуации так, что поток фактически не продвигается вперед. Голодание возникает, когда потоки сохраняют блокировку в течение длительных периодов, так что некоторые потоки «голодают» без прогресса.
java.util.concurrent
Пулы потоков
Основным интерфейсом для пулов потоков является ExecutorService.java.util.concurrent также предоставляет статическую фабрику Executors, которая содержит фабричные методы для создания пула потоков с наиболее распространенными конфигурациями.
| Метод | Описание |
|---|---|
| newSingleThreadExecutor | Возвращает ExecutorService только с одним потоком. |
| newFixedThreadPool | Возвращает ExecutorService с фиксированным количеством потоков. |
| newCachedThreadPool | Возвращает ExecutorService с пулом потоков различного размера. |
| Возвращает ScheduledExecutorService с одним потоком. | |
| newScheduledThreadPool | Возвращает ScheduledExecutorService с основным набором потоков. |
| newWorkStealingPool | Возвращает крадущий задачи ExecutorService. |
Таблица 6: Методы статической фабрики
При определении размера пулов потока часто бывает полезно определить размер числа логических ядер в машине, на которой запущено приложение. Получить это значение в Java можно вызвав Runtime.getRuntime().AvailableProcessors() .
| Реализация | Описание |
|---|---|
| ThreadPoolExecutor | Реализация по умолчанию с изменяющим размер пулом потока, одной рабочей очереди и настраиваемой политикой для отклоненных задач (через RejectedExecutionHandler) и создания потоков (через ThreadFactory). |
| Расширение ThreadPoolExecutor, которое обеспечивает возможность планирования периодических задач. | |
| ForkJoinPool | Крадущий задачи пул: все потоки в пуле пытаются найти и запустить либо поставленные задачи, либо задачи, созданные другими активными задачами. |
Таблица 7: Реализации пула потоков
Задачи отправляются с помощью ExecutorService#submit , ExecutorService#invokeAll или ExecutorService#invokeAny , которые имеют несколько перегрузок для разных типов задач.
| Описание | |
|---|---|
| Runnable | Представляет задачу без возвращаемого значения. |
| Callable | Представляет вычисление с возвращаемым значением. Он также выбрасывает исходный Exeption, поэтому не требуется обертка для проверенного исключения. |
Таблица 8: Функциональные интерфейсы задач
Future — это абстракция для асинхронного вычисления. Она представляет результат вычисления, который может быть доступен в какой-либо момент: либо вычисленное значение, либо исключение. Большинство методов ExecutorService используют Future как возвращаемый тип. Он предоставляет методы для изучения текущего состояния future или блокирует до тех пор, пока не будет доступен результат.
ExecutorService executorService = Executors.newSingleThreadExecutor(); Future future = executorService.submit(() -> "result"); try < String result = future.get(1L, TimeUnit.SECONDS); System.out.println("Result is '" + result + "'."); >catch (InterruptedException e) < Thread.currentThread().interrupt(); throw new RuntimeException(e); >catch (ExecutionException e) < throw new RuntimeException(e.getCause()); >catch (TimeoutException e) < throw new RuntimeException(e); >assert future.isDone();
Пакет java.util.concurrent.locks имеет стандартный интерфейс Lock . Реализация ReentrantLock дублирует функциональность ключевого слова synchronized, но также предоставляет дополнительные функции, такие как получение информации о состоянии блокировки, неблокирующий tryLock() и прерываемая блокировке. Пример использования явного экземпляра ReentrantLock:
class Counter < private final Lock lock = new ReentrantLock(); private int value; int increment() < lock.lock(); try < return ++value; >finally < lock.unlock(); >> >
ReadWriteLock
Пакет java.util.concurrent.locks также содержит интерфейс ReadWriteLock (и реализацию ReentrantReadWriteLock), который определяется парой блокировок для чтения и записи, обычно позволяя считывать одновременно нескольким читателям, но допуская только одного писателя.
class Statistic < private final ReadWriteLock lock = new ReentrantReadWriteLock(); private int value; void increment() < lock.writeLock().lock(); try < value++; >finally < lock.writeLock().unlock(); >> int current() < lock.readLock().lock(); try < return value; >finally < lock.readLock().unlock(); >> >
CountDownLatch
CountDownLatch инициализируется счетчиком. Потоки могут вызывать await() , чтобы ждать, пока счетчик не достигнет 0. Другие потоки (или тот же поток) могут вызвать countDown() , чтобы уменьшить счетчик. Нельзя использовать повторно, как только счетчик достигнет 0. Используется для запуска неизвестного набора потоков, как только произошло некоторое количество действий.
CompletableFuture
CompletableFuture является абстракцией для произведения асинхронных вычислений. В отличие от простого Future, где единственная возможность получить результат — блокировать, рекомендуется регистрировать обратные вызовы для создания конвейера задач, которые должны выполняться, когда доступен результат или исключение. Либо во время создания (через CompletableFuture#supplyAsync/runAsync ), либо во время добавления обратных вызовов (методы семейства *async ) может быть указан исполнитель, где должно выполняться вычисление (если он не указан стандартным глобальным ForkJoinPool#commonPool ).
Учтите, что если CompletableFuture уже завершен, обратные вызовы, зарегистрированные с помощью не *async методов, будут выполняться в вызывающем потоке.
Если есть несколько future , вы можете использовать CompletableFuture#allOf , чтобы получить future , который будет завершен, когда все future будут завершены, или CompletableFuture#anyOf , который будет завершен, как только будет завершен какой-либо future .
ExecutorService executor0 = Executors.newWorkStealingPool(); ExecutorService executor1 = Executors.newWorkStealingPool(); //Завершено, когда оба future завершены CompletableFuture waitingForAll = CompletableFuture .allOf( CompletableFuture.supplyAsync(() -> "first"), CompletableFuture.supplyAsync(() -> "second", executor1) ) .thenApply(ignored -> " is completed."); CompletableFuture future = CompletableFuture.supplyAsync(() -> "Concurrency Refcard", executor0) //Использование того же исполнителя .thenApply(result -> "Java " + result) //Использование другого исполнителя .thenApplyAsync(result -> "Dzone " + result, executor1) //Завершено, когда это и другое future завершено .thenCombine(waitingForAll, (first, second) -> first + second) //Неявно использование ForkJoinPool#commonPool как исполнителя .thenAcceptAsync(result -> < System.out.println("Result is '" + result + "'."); >) //Общий обработчик .whenComplete((ignored, exception) -> < if (exception != null) exception.printStackTrace(); >); //Первый блокирующий вызов - блокирует, пока он не будет завершен. future.join(); future //Выполняется в текущем потоке (который является основным). .thenRun(() -> System.out.println("Current thread is '" + Thread.currentThread().getName() + "'.")) //Неявное использование ForkJoinPool#commonPool как исполнителя .thenRunAsync(() -> System.out.println("Current thread is '" + Thread.currentThread().getName() + "'."));
Параллельные коллекции
Самый простой способ сделать коллекцию потокобезопасной — использование родственных методов Collections#synchronized* . Поскольку это решение работает плохо при высокой конкуренции, java.util.concurrent предоставляет множество структур данных, которые оптимизированы для параллельного использования.
| Реализация | Описание |
|---|---|
| Предоставляет семантику копирования при записи, где каждая модификация структуры данных приводит к новой внутренней копии данных (поэтому запись очень дорогая, тогда как чтение дешевое). Итераторы в структуре данных всегда видят снепшот данных с момента создания итератора. |
Таблица 9: Списки в java.util.concurrent
| Описание |
|---|
| Обычно выступает в качестве сегментированной хэш-таблицы. Операции чтения, как правило, не блокируют и отражают результаты последней завершенной записи. Запись первого узла в пустой ящик выполняется просто CAS-ом (сравнить и установить), тогда как другим операциям записи требуются блокировки (первый узел сегмента используется как блокировка). |
| Обеспечивает параллельный доступ наряду функциональностью сортированного Map, подобной TreeMap. Границы производительности такие же как у TreeMap, хотя несколько потоков обычно могут читать и записывать из ассоциативного массива без конфликтов, если они не изменяют одну и ту же часть отображения. |
Таблица 10: Ассоциативные массивы в java.util.concurrent
| Описание |
|---|
| Подобно CopyOnWriteArrayList, он использует семантику copy-on-write для реализации интерфейса Set. |
| Подобно ConcurrentSkipListMap, но реализует интерфейс Set. |
Таблица 11: Множества в java.util.concurrent
Другим подходом к созданию параллельного множества является обертка параллельного Map:
Set concurrentSet = Collections.newSetFromMap(new ConcurrentHashMap());
Очереди выступают в качестве труб между «производителями» и «потребителями». Элементы помещаются в один конец трубы и выходят из другого конца трубы в том же порядке «первый зашел, первый вышел» (FIFO). Интерфейс BlockingQueue расширяет Queue , чтобы предоставить дополнительные варианты того, как обрабатывать сценарий, где очередь может быть заполнена (когда производитель добавляет элемент) или пустой (когда потребитель читает или удаляет элемент). В этих случаях BlockingQueue предоставляет методы, которые либо блокируют навсегда, либо блокируют в течение определенного периода времени, ожидая изменения условия из-за действий другого потока.
| Реализация | Описание |
|---|---|
| Неограниченная неблокирующая очередь, поддерживаемая связанным списком. | |
| LinkedBlockingQueue | Опционально ограниченная блокирующая очередь, поддерживаемая связанным списком. |
| Неограниченная блокирующая очередь, поддерживаемая минимальной кучей. Элементы удаляются из очереди в порядке, основанном на компараторе Comparator, связанном с очередью (вместо порядка FIFO). | |
| DelayQueue | Неограниченная блокирующая очередь элементов, каждый из которых имеет значение задержки. Элементы могут быть удалены только тогда, когда их задержка прошла и удаляются в порядке старейшего истекшего элемента. |
| SynchronousQueue | Очередь о-длины, где производитель и потребитель блокируются до тех пор, пока не прибудет другой. Когда оба потока приходят, значение передается напрямую от производителя к потребителю. Полезно при передаче данных между потоками. |
Таблица 12: Очереди в java.util.concurrent
Как всегда ждём пожелания и вопросы.
