что такое многопоточные приложения
Другой взгляд на многопоточность
Откуда ноги растут
Что делать?
Когда вертикальное масштабирование (качественный рост) приходит к своему пику и ждет очередной революции, в дело вступает горизонтальное масштабирование (количественный рост). Решение было простым, сделать из одного процессора несколько. Так появились многоядерные процессоры.
Ядра процессора должны с чем-то работать, выполнять команды и куда-то складывать результат. Такое место называется память. Чтобы смоделировать память, мы можем представить ее как очень длинный массив данных, где индекс массива это адрес.
Представим, что мы обладаем общей памятью и ядрами в количестве N штук.
Поток
К сожалению, придется ввести еще один термин, без него никак не получится перейти к многопоточному программированию.
Давайте попробуем решить простую задачу. Какие варианты пар (a, b) возможны после завершения исполнения обоих потоков, положитесь на свою интуицию, стоит рассмотреть даже самые, казалось бы, невозможные варианты:
Ответ
Пусть поток 1 выполнился полностью, а поток 2 еще не стартовал. Тогда в результате будет пара (0, 1)
Аналогично, поток 2 выполнился полностью, а поток 1 еще не стартовал. Тогда в результате будет пара (1, 0).
Во всех остальных случаях (0, 0)
Случая (1, 1) быть не может, так как хотя бы один поток перед своим завершением обнулит какую-то переменную.
В итоге получается [ (0, 1), (1, 0), (0, 0) ]
Как вы могли догадаться, понимание результата работы многопоточного кода сводится к рассмотрению всех вариантов его исполнения. В данном случае формально такая модель исполнения называется моделью последовательной консистентности (sequential consistency, SC).
Согласно данной модели, любой результат исполнения многопоточной программы может быть получен как последовательное исполнение некоторого чередования инструкций потоков этой программы. (Предполагается, что чередование сохраняет порядок между инструкциями, относящимися к одному потоку.)
К сожалению, настоящие программы оперируют не двумя переменными и не только пишут в память, но еще и читают ее. Попробуйте решить следующий пример, тут немного сложнее:
Ответ
Многопоточность была бы простой если бы все закончилось здесь.
Конец?
Код на языке C рано или поздно завершается, хотя в модели SC не должен.
Это не вписывается в модель SC (sequential consistency), поскольку не существует такого исполнения, которое бы привело к результату (0, 0). Выше мы допустили, что «операции с памятью выполняются сразу и без каких-либо задержек». Но для современных процессоров это совсем не так.
Если вы разрабатываете ПО, вы часто сталкиваетесь с таким термином как кэш. Удобно копить результат и лишний раз не обращаться к удаленному ресурсу. База данных для сервера, это как оперативная память для процессора. Ходить в нее дорого-далеко-долго (кому что больше нравится). Куда удобнее прочитать один раз, положить в кэш и при повторном чтении читать из кэша. Тоже самое и с записью. Например вы используете в своей программе запись в лог и вам не всегда хочется писать каждое сообщение сразу в файл, вы можете их хранить некоторое время в памяти, а потом при накоплении какого-то количества записать их за один раз.
Сейчас мы разберем (в качестве модели) архитектуру x86.
1. Процессор всегда читает из кэша
2. Если в кэше такого адреса не найдено, процессор идет в память и копирует его в кэш и читает из кэша.
3. Процессор всегда пишет в буфер записи.
4. При записи нового значения в буфер запись происходит и в кэш.
5. Записи из буфера попадают в память.
Все хорошо, когда мы живем в мире одного ядра. Но когда ядер несколько начинаются вопросы.
Ядро 1 прочитало переменную f в кэш, ядро 2 изменило переменную f. Как ядро 1 узнает о изменении переменной?
Пока что, в нашей модели, никакой синхронизации между ядрами у нас нет. Так и сломался наш пример, возьмем вариант исполнения 1 (во вкладке ответ):
Барьеры
Для начала хочу затронуть тему инвалидации значений кэша. Чтобы не углубляться, значение в кэше ядра инвалидируется (значение становится «неактульным»), если изменяется (операция записи в store buffer) в другом ядре. Процесс инвалидации определяется протоколом когеренции кэшей (для x86 Intel это MESI), сейчас это не важно. Попробуем ответить на вопрос поставленные выше.
Ядро 1 прочитало переменную f в кэш, ядро 2 изменило переменную f. Как ядро 1 узнает о изменении переменной?
Например в данном случае, переменная f в кэше ядра 1 будет инвалидирована, при изменении в ядре 2. Как только мы попробуем прочитать инвалидированную переменную, чтение будет произведено из памяти (то есть значение будет актуальным).
Но так как инвалидировать значение переменной при каждом изменении в другом ядре очень дорого, запрос на инвалидацию попадает в очередь других ядер и переменная будет инвалидирована в удобный для ядра момент времени (то есть инвалидация переменной при изменении в другом ядре происходит не сразу, и ядро, обладающее старым значением, думает, что можно брать значение из кэша, если оно там есть).
Та же ситуация возникает и с store buffer, процессор записывает данные в память, когда ему будет удобно (но запрос на инвалидацию отправляется мгновенно). Подробнее взаимодействие протокола когеренции кэшей и барьеров я постараюсь раскрыть в следующей статье, а пока картинка.
Добавим на рисунок места применения барьеров
Замечание-ответ
Простым решением конечно же будет являться такое добавление барьеров. После записи x и y необходимо, чтобы они попали в память. А перед чтением x и y нужно обновить кэш.
Но, подумайте, можно ли избежать лишнего добавления барьера, поскольку каждый барьер останавливает поток пока не обновится кэш или не запишется буфер записи. Если вы пишете конкурентный код, каждый барьер может оказывать решающее значение на производительность.
Попробуем раскрутить простое решение
Представляю рабочий вариант(решение через добавление обоих барьеров), который корректно будет исполнятся в любом случае (вариант на процессоре x86).
Если Вам удалось понять содержимое статьи, то любые другие элементы многопоточного программирования дадутся Вам намного легче.
Конец
Если Вы сталкиваетесь с многопоточностью впервые, скорее всего с первого и даже с третьего раза Вам будет понятно не всё. Для полного понимания нужна практика.
В этой статье не затрагивалась операционная система, блокировки, методы синхронизации, модели памяти, компиляторные оптимизации и многое другое. Если статья покажется читателям хорошей и самое главное понятной, я постараюсь в скором времени рассказать о других аспектах многопоточности в таком же ключе.
Поскольку это моя первая статья, я скорее всего допустил множество ошибок и неточностей, поэтому буду рад услышать комментарии. Спасибо за внимание!
Многопоточность в Java – руководство с примерами
Пример одного потока :
Преимущества одного потока :
Что такое многопоточность?
Многопоточность в Java — это выполнение двух или более потоков одновременно для максимального использования центрального процесса.
Многопоточные приложения — это приложения, где параллельно выполняются два или более потоков. Данное понятие известно в Java как многопотоковое выполнение. При этом несколько процессов используют общие ресурсы, такие как центральный процессор, память и т. д.
Все потоки выполняются параллельно друг другу. Для каждого отдельного потока не выделяется память, что приводит к ее экономии. Кроме этого переключение между потоками занимает меньше времени.
Жизненный цикл потока в Java
Жизненный цикл потока :
Стадии жизни потока :
Часто используемые методы для управления многопоточностью Java :
Метод | Описание |
start() | Этот метод запускает выполнение потока, а JVM (виртуальная машина Java) вызывает в потоке метод Run (). |
Sleep(int milliseconds) | Делает поток спящим. Его выполнение будет приостановлено на указанное количество миллисекунд, после чего он снова начнет выполняться. Этот метод полезен при синхронизации потоков. |
getName() | Возвращает имя потока. |
setPriority(int newpriority) | Изменяет приоритет потока. |
yield () | Останавливает текущий поток и запускает другие. |
Например : В этом примере создается поток, и применяются перечисленные выше методы.
Объяснение кода
Вывод
5 — это приоритет потока, а « Thread Running » — текст, который является выводом нашего кода.
Синхронизация потоков Java
В многопоточности Java присутствует асинхронное поведение. Если один поток записывает некоторые данные, а другой в это время их считывает, в приложении может возникнуть ошибка. Поэтому при необходимости доступа к общим ресурсам двум и более потоками используется синхронизация.
В Java есть свои методы для обеспечения синхронизации. Как только поток достигает синхронизированного блока, другой поток не может вызвать этот метод для того же объекта. Все другие потоки должны ожидать, пока текущий не выйдет из синхронизированного блока.
Таким образом, решается проблема в многопоточных приложениях. Один поток ожидает, пока другой не закончит свое выполнение, и только тогда другим потокам будет разрешено их выполнение.
Это можно написать следующим образом:
Пример многопоточности Java
В этом Java многопоточности примере мы задействуем два потока и извлекаем имена потоков.
Пример 1
Объяснение кода
Вывод
Имена потоков выводятся как:
Пример 2
Также мы задействуем два класса:
Объяснение кода
При запуске приведенного выше кода получаем следующие выходные данные:
Вывод
Поскольку у нас два потока, то мы дважды получаем сообщение « Thread started ».
Получаем соответствующие имена потоков.
Выполняется цикл, в котором печатается счетчик и имя потока, а счетчик начинается с « 0 ».
Цикл выполняется три раза, а поток приостанавливается на 1000 миллисекунд.
— Новый;
— Готовый к выполнению;
— Выполняемый;
— Ожидающий;
— Остановленный.
Дайте знать, что вы думаете по этой теме в комментариях. За комментарии, лайки, отклики, дизлайки, подписки огромное вам спасибо!
Пожалуйста, оставьте ваши отзывы по текущей теме статьи. За комментарии, отклики, дизлайки, подписки, лайки огромное вам спасибо!
Национальная библиотека им. Н. Э. Баумана
Bauman National Library
Персональные инструменты
Многопоточное программирование
Многопоточность — свойство платформы (например, операционная система, виртуальная машина и т. д.) или прикладное программное обеспечение/приложения, состоящее в том, что процесс, порождённый в операционной системе, может состоять из нескольких потоков, выполняющих параллельные вычисления, то есть без предписанного порядка во времени. При выполнении некоторых задач такое разделение может достичь более эффективного использования ресурсов вычислительной машины. [Источник 1]
Содержание
Описание
Сутью многопоточности является квазимногозадачность на уровне одного исполняемого процесса, то есть все потоки выполняются в адресном пространстве процесса. Кроме этого, все потоки процесса имеют не только общее адресное пространство, но и общие Файловый дескриптор (дескрипторы файлов). Выполняющийся процесс имеет как минимум один (главный) поток.
Многопоточность не следует путать ни с многозадачностью, ни с многопроцессорностью, несмотря на то, что операционная система (операционные системы), реализующая многозадачность, как правило, реализует и многопоточность.
К достоинствам многопоточной реализации той или иной системы перед многозадачной можно отнести следующее:
К достоинствам многопоточной реализации той или иной системы перед однопоточной можно отнести следующее:
В случае, если потоки выполнения требуют относительно сложного взаимодействия друг с другом, возможно проявление проблем многозадачности, таких как взаимные блокировки.
Предложены 2 API потокового программирования:
Здесь рассматривается вариант POSIX (Portable Operating System Interface for Unix). Все функции этого варианта имеют в своих именах префикс pthread_ и объявлены в заголовочном файле pthread.h.
Аппаратная реализация
Различают две формы многопоточности, которые могут быть реализованы в процессорах аппаратно:
Типы реализации потоков
Взаимодействие потоков
В многопоточной среде часто возникают задачи, требующие приостановки и возобновления работы одних потоков в зависимости от работы других. В частности это задачи, связанные с предотвращенем конфликтов доступа при использовании одних и тех же данных или устройств из параллельно исполняемых потоков. Для решения таких задач используются специальные объекты для взаимодействия потоков, такие как взаимоисключения (мьютексы), семафоры, критические секции, события и т.п. Многие из этих объектов являются объектами ядра и могут применяться не только между потоками одного процесса, но и для взаимодействия между потоками разных процессов.
Создание потока управления
Создает новый поток для функции, заданной параметром func_p. Эта функция имеет аргументом указатель (void *) и возвращает значение того же типа. Реально же в функцию передается аргумент arg_p. Идентификатор нового потока возвращается через tid_p.
Аргумент attr_p указывает на структуру, задающую атрибуты вновь создаваемого потока. Если attr_p=NULL, то используются атрибуты «по умолчанию» (но это плохая практика, т.к. в разных ОС эти значения могут быть различными, хотя декларируется обратное). Одна структура, указываемая attr_p, может использоваться для управления несколькими потоками.
Инициализация атрибутов потока
Инициализирует структуру, указываемую attr_p, значениями «по умолчанию» (при этом распределяется кое-какая память). Атрибуты потока:
Освобождение памяти атрибутов потока
Область конкуренции
Состояние отсоединенности
Для отсоединенного потока невозможно его ожидание его окончания другим потоком, поэтому после окончания такого потока все его ресурсы могут быть освобождены (и использованы заново).
Завершение потока
В потоках можно использовать стандартную функцию exit(), однако это ведет к немедленному завершению всех потоков и процесса в целом. Поток завершается вместе с вызовом return() в функции, вызванной pthread_create(). Поток заканчивает свое выполнение также с помощью функции
допустимо в качестве status использовать NULL. Поток может быть завершен другим потоком посредством функции pthread_cancel() (с этой функцией работают pthread_setcanceltype, pthread_setcancelstate и pthread_testcancel).
Ожидание завершения потока
Вызывающий поток блокируется до окончания потока с идентификатором tid. Поток с идентификатором tid не может быть отсоединенным
Получение идентификатора потока
Передача управления другому потоку
Передает управление другому потоку, имеющему приоритет равный или больший приоритета вызывающего потока.
Посылка сигнала потоку
Посылает сигнал с идентификатором signum в поток, задаваемый идентификатором tid.
Манипулирование сигнальной маской потока
Изменяет сигнальную маску потока в соответствии с аргументом mode, который может принимать следующие значения:
Если значение аргумента old_p не равно NULL, то в область памяти, указываемую old_p, помещается предыдущее содержимое сигнальной маски.
Объекты синхронизации потоков управления
Потоки используют единое адресное пространство. Это означает, что все статические переменные доступны потокам в любой момент. Поэтому необходимы средства управления доступом к совместно используемым данным. Здесь возможно использование стандартных средств синхронизации различных процессов: каналы, очереди сообщений, межпроцессные семафоры. Однако, специально для межпотокового взаимодействия предложены индивидуальные средства:
Указанные средства перечислены в порядке ухудшения их эффективности. Заметим, что доступ к атомарным данным (char, int, double) реализуется за один такт процессора, поэтому существуют ситуации (зависящие от логики программы), когда такие данные сами могут выступать в качестве средства синхронизации.
Взамоисключающие блокировки
разрушает блокировку, освобождая выделенную память.
С помощью pthread_mutex_lock() поток пытается захватить блокировку. Если же блокировка уже принадлежит другому потоку, то вызывающий поток ставится в очередь (с учетом приоритетов потоков) к блокировке. После возврата из функции pthread_mutex_lock() блокировка будет принадлежать вызывающему потоку.
Функция pthread_mutex_unlock() освобождает захваченную ранее блокировку. Освободить блокировку может только ее владелец.
Условные переменные
Применяются в сочетании со взаимоис ключающими блокировками. Общая схема использования такова. Один поток устанавливает взаимоисключающую блокировку и затем блокирует себя по условной переменной (путем вызова функции pthread_cond_wait()), при этом автоматически (но временно) освобождается взаимоисключающая блокировка. Когда какой-либо другой поток посредством вызова функции pthread_cond_signal() сигнализирует по условной переменной, то первый поток разблокируется и ему возвращается во владение взаимоисключающая блокировка.
инициализирует условную переменную, выделяя память.
разрушает условную переменную, освобождая память.
автоматически освобождает взаимоисключающую блокировку, указанную mp, а вызывающий поток блокируется по условной переменной, заданной cvp. Заблокированный поток разблокируется функциями pthread_cond_signal() и pthread_cond_broadcast(). Одной условной переменной могут быть заблокированы несколько потоков.
аналогична функции pthread_cond_wait(), но имеет третий аргумент, задающий интервал времени, после которого поток разблокируется (если этого не было сделано ранее).
разблокирует ожидающий данную условную переменную поток. Если сигнала по условной переменной ожидают несколько потоков, то будет разблокирован только какой-либо один из них.
разблокирует все потоки, ожидающие данную условную переменную.
Семафоры
Семафор представляет собой целочисленную переменную. Потоки могут наращивать (post) и уменьшать (wait) ее значение на единицу. Если поток пытается уменьшить семафор так, что его значение становится отрицательным, то поток блокируется. Поток будет разблокирован, когда какой-либо другой поток не увеличит значение семафора так, что он станет неотрицательным после уменьшения его первым (заблокированным) потоком.
Потоки похожи на взаимоисключающие блокировки и условные переменные, но отличаются от них тем, что у них нет «владельца», т.е. изменить значение семафора может любой поток.
В POSIX-версии средств многопотокового программирования используются те же самые семафоры, что и для межпроцессного взаимодействия.
увеличивает значение семафора на 1, при этом может быть разблокирован один (из, возможно, нескольких) поток (какой именно не определено).
пытается уменьшить значение семафора на 1. Если при этом значение семафора должно стать отрицательным, то поток блокируется.
неблокирующая версия функции sem_wait().
Барьеры
Барьер используется для синхронизации работы нескольких потоков управления. Барьер характеризуется натуральным числом count, задающим количество синхронизируемых потоков. Поток управления, «подошедший» к барьеру (обратившийся к функции pthread_barrier), блокируется до момента накопления перед этим барьером указанного количества потоков count.
инициализирует барьер, выделяя необходимую память, устанавливая значения его атрибутов и назначая count «шириной» барьера. В настоящее время атрибуты барьеров не определены поэтому в качестве второго параметра функции pthread_barrier_init следует использовать NULL.
разрушает барьер, освобождая выделенную память.
приостанавливает вызвавший данную функцию поток до момента накопления перед барьером count потоков. Заблокированный поток может быть прерван сигналом, при этом обработчик сигнала (если он был назначен) будет вызван на выполнение обычным образом. Выход из обработчика вернет поток в состояние ожидания, если к этому моменту требуемое количество count потоков еще не скопилось перед барьером.
Параллелизм, многопоточность, асинхронность: разница и примеры применения (.NET, C#)
Авторизуйтесь
Параллелизм, многопоточность, асинхронность: разница и примеры применения (.NET, C#)
Руководитель группы разработки digital-интегратора DD Planet
Многие начинающие специалисты путают многопоточное, асинхронное и параллельное программирование. На первый взгляд, может показаться, что это одно и то же — но нет. Давайте разберёмся, сколько программных моделей используют C#-разработчики и в чём их отличия. Материал подготовлен совместно с Алексеем Гришиным, ведущим разработчиком DD Planet.
Существует несколько концепций: синхронное/асинхронное программирование и однопоточные/многопоточные приложения. Причём первая программная модель может работать в однопоточной или многопоточной среде. То есть приложение может быть: синхронным однопоточным, синхронным многопоточным и асинхронным многопоточным.
Отдельной концепцией считается параллелизм, который является подмножеством многопоточного типа приложений. Рассмотрим особенности каждой программной модели подробнее.
Синхронная модель
Потоку назначается одна задача, и начинается её выполнение. Заняться следующей задачей можно только тогда, когда завершится выполнение первой. Эта модель не предполагает приостановку одной задачи, чтобы выполнить другую.
Однопоточность
Система в одном потоке работает со всеми задачами, выполняя их поочерёдно.
Однопоточная синхронная система
Многопоточность
В этом случае речь о нескольких потоках, в которых выполнение задач идет одновременно и независимо друг от друга.
Многопоточная синхронная система
Пример такого концепта — одновременная разработка веб- и мобильного приложений и серверной части, при условии соблюдения архитектурных «контрактов».
Использование нескольких потоков выполнения — один из способов обеспечить возможность реагирования приложения на действия пользователя при одновременном использовании процессора для выполнения задач между появлением или даже во время появления событий пользователя.
Асинхронность
Характеристики асинхронного кода:
Если у системы много потоков, то их асинхронная работа выглядит примерно так:
Многопоточная асинхронная система
Конструкция async/await
Для работы с асинхронными вызовами в C# необходимы два ключевых слова:
Они используются вместе для создания асинхронного метода. У асинхронных методов могут быть следующие типы возвращаемых значений:
Пример асинхронного метода:
Результат асинхронного вычисления факториала
Этот пример приведён лишь для наглядности, особого смысла делать логику вычисления факториала асинхронной нет. Опять же, для имитации долгой работы мы использовали задержку на 8 секунд с помощью методы Thread.Sleep(). Цель была показать: асинхронная задача, которая может выполняться долгое время, не блокирует основной поток — в этом случае метод Main(), и мы можем вводить и обрабатывать данные, продолжая работу с ним.
Параллелизм
Эта программная модель подразумевает, что задача разбивается на несколько независимых подзадач, которые можно выполнить параллельно, а затем объединить результаты. Примером такой задачи может быть Parallel LINQ:
Еще один пример — вычисление среднего значения двумерного массива, когда каждый отдельный поток может подсчитать сумму своей строки, а потом объединить результат и вычислить среднее.
Однако не стоит забывать, что не все задачи поддаются распараллеливанию. Например, описанная выше задача по вычислению факториала, в которой на каждом последующем этапе нужен результат предыдущего.
Какую программную модель выбрать?
Перечисленные программные модели должны применяться в зависимости от задач. Их можно использовать как отдельно во всём приложении, так и сочетать между собой. Главное, чтобы приложение было максимально эффективным и удовлетворяло требования пользователя.
Если речь идет о сложных многопользовательских приложениях, то стремиться стоит к использованию асинхронной модели, так как важна интерактивность и отзывчивость интерфейса. Взаимодействие с пользователем в активном режиме всегда должно быть максимально эффективным, даже если в фоновом режиме в то же время выполняются другие задачи. Издержки асинхронности, например, на переключение исполняемого контекста, в таком случае нивелируются за счет общей эффективности приложения.
В разработке простых приложений, к примеру, парсера документа, необходимости в асинхронности, или даже многопоточности, может и не быть.