Что такое многопоточные приложения

Потоки и работа с ними

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

Процессы и потоки

Процесс — это исполнение программы. Операционная система использует процессы для разделения исполняемых приложений. Поток — это основная единица, которой операционная система выделяет время процессора. Каждый поток имеет приоритет планирования и набор структур, в которых система сохраняет контекст потока, когда выполнение потока приостановлено. Контекст потока содержит все сведения, позволяющие потоку безболезненно возобновить выполнение, в том числе набор регистров процессора и стек потока. Несколько потоков могут выполняться в контексте процесса. Все потоки процесса используют общий диапазон виртуальных адресов. Поток может исполнять любую часть программного кода, включая части, выполняемые в данный момент другим потоком.

Цели применения нескольких потоков

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

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

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

Наконец, можно использовать класс System.Threading.Thread, который представляет управляемый поток. Дополнительные сведения см. в разделе Использование потоков и работа с потоками.

Исключения следует обрабатывать в потоках. Необработанные исключения в потоках, как правило, приводят к завершению процесса. Дополнительные сведения см. в статье Исключения в управляемых потоках.

Источник

Учимся писать многопоточные и многопроцессные приложения на Python

Эта статья не для матёрых укротителей Python’а, для которых распутать этот клубок змей — детская забава, а скорее поверхностный обзор многопоточных возможностей для недавно подсевших на питон.

К сожалению по теме многопоточности в Python не так уж много материала на русском языке, а питонеры, которые ничего не слышали, например, про GIL, мне стали попадаться с завидной регулярностью. В этой статье я постараюсь описать самые основные возможности многопоточного питона, расскажу что же такое GIL и как с ним (или без него) жить и многое другое.

Python — очаровательный язык программирования. В нем прекрасно сочетается множество парадигм программирования. Большинство задач, с которыми может встретиться программист, решаются здесь легко, элегантно и лаконично. Но для всех этих задач зачастую достаточно однопоточного решения, а однопоточные программы обычно предсказуемы и легко поддаются отладке. Чего не скажешь о многопоточных и многопроцессных программах.

Многопоточные приложения

В Python есть модуль threading, и в нем есть все, что нужно для многопоточного программирования: тут есть и различного вида локи, и семафор, и механизм событий. Один словом — все, что нужно для подавляющего большинства многопоточных программ. Причем пользоваться всем этим инструментарием достаточно просто. Рассмотрим пример программы, которая запускает 2 потока. Один поток пишет десять “0”, другой — десять “1”, причем строго по-очереди.

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

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

Global Interpreter Lock

Существуют две самые распространенные причины использовать потоки: во-первых, для увеличения эффективности использования многоядерной архитектуры cоврменных процессоров, а значит, и производительности программы;
во-вторых, если нам нужно разделить логику работы программы на параллельные полностью или частично асинхронные секции (например, иметь возможность пинговать несколько серверов одновременно).

В первом случае мы сталкиваемся с таким ограничением Python (а точнее основной его реализации CPython), как Global Interpreter Lock (или сокращенно GIL). Концепция GIL заключается в том, что в каждый момент времени только один поток может исполняться процессором. Это сделано для того, чтобы между потоками не было борьбы за отдельные переменные. Исполняемый поток получает доступ по всему окружению. Такая особенность реализации потоков в Python значительно упрощает работу с потоками и дает определенную потокобезопасность (thread safety).

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

Эта программа просто пишет в файл миллион строк “1” и делает это за

0.35 секунды на моем компьютере.

Рассмотрим другую программу:

Эта программа создает 2 потока. В каждом потоке она пишет в отдельный файлик по пол миллиона строк “1”. По-сути объем работы такой же, как и у предыдущей программы. А вот со временем работы тут получается интересный эффект. Программа может работать от 0.7 секунды до аж 7 секунд. Почему же так происходит?

Это происходит из-за того, что когда поток не нуждается в ресурсе CPU — он освобождает GIL, а в этот момент его может попытаться получить и он сам, и другой поток, и еще и главный поток. При этом операционная система, зная, что ядер много, может усугубить все попыткой распределить потоки между ядрами.

Читайте также:  абсолютное содержание эозинофилов повышено что это значит у взрослого

UPD: на данный момент в Python 3.2 существует улучшенная реализация GIL, в которой эта проблема частично решается, в частности, за счет того, что каждый поток после потери управления ждет небольшой промежуток времени до того, как сможет опять захватить GIL (на эту тему есть хорошая презентация на английском)

«Выходит на Python нельзя писать эффективные многопоточные программы?», — спросите вы. Нет, конечно, выход есть и даже несколько.

Многопроцессные приложения

Для того, чтобы в некотором смысле решить проблему, описанную в предыдущем параграфе, в Python есть модуль subprocess. Мы можем написать программу, которую хотим исполнять в параллельном потоке (на самом деле уже процессе). И запускать ее в одном или нескольких потоках в другой программе. Такой способ действительно ускорил бы работу нашей программы, потому, что потоки, созданные в запускающей программе GIL не забирают, а только ждут завершения запущенного процесса. Однако, в этом способе есть масса проблем. Основная проблема заключается в том, что передавать данные между процессами становится трудно. Пришлось бы как-то сериализовать объекты, налаживать связь через PIPE или друге инструменты, а ведь все это несет неизбежно накладные расходы и код становится сложным для понимания.

Здесь нам может помочь другой подход. В Python есть модуль multiprocessing. По функциональности этот модуль напоминает threading. Например, процессы можно создавать точно так же из обычных функций. Методы работы с процессами почти все те же самые, что и для потоков из модуля threading. А вот для синхронизации процессов и обмена данными принято использовать другие инструменты. Речь идет об очередях (Queue) и каналах (Pipe). Впрочем, аналоги локов, событий и семафоров, которые были в threading, здесь тоже есть.

Кроме того в модуле multiprocessing есть механизм работы с общей памятью. Для этого в модуле есть классы переменной (Value) и массива (Array), которые можно “обобщать” (share) между процессами. Для удобства работы с общими переменными можно использовать классы-менеджеры (Manager). Они более гибкие и удобные в обращении, однако более медленные. Нельзя не отметить приятную возможность делать общими типы из модуля ctypes с помощью модуля multiprocessing.sharedctypes.

Еще в модуле multiprocessing есть механизм создания пулов процессов. Этот механизм очень удобно использовать для реализации шаблона Master-Worker или для реализации параллельного Map (который в некотором смысле является частным случаем Master-Worker).

Из основных проблем работы с модулем multiprocessing стоит отметить относительную платформозависимость этого модуля. Поскольку в разных ОС работа с процессами организована по-разному, то на код накладываются некоторые ограничения. Например, в ОС Windows нет механизма fork, поэтому точку разделения процессов надо оборачивать в:

Впрочем, эта конструкция и так является хорошим тоном.

Для написания параллельных приложений на Python существуют и другие библиотеки и подходы. Например, можно использовать Hadoop+Python или различные реализации MPI на Python (pyMPI, mpi4py). Можно даже использовать обертки существующих библиотек на С++ или Fortran. Здесь можно было упомянуть про такие фреймфорки/библиотеки, как Pyro, Twisted, Tornado и многие другие. Но это все уже выходит за пределы этой статьи.

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

Источник

Основы многопоточности

Многопоточность — тема, которую боятся многие программисты. Вероятно, это связано с тем, что многопоточные программы, если написаны неправильно, могут вызывать больше ошибок, чем однопоточные, и ущерб, нанесенный несколькими потоками, бывает труднее оценить. Некоторым тяжело дается понимание работы многопоточности, а кто-то даже не старается ее понять. Лично я считаю, что это одна из самых интересных особенностей в программировании.

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

Компьютерный процесс в сравнении с потоком

Проще говоря, процесс — это программа в ходе своего выполнения. Когда мы выполняем программу или приложение, запускается процесс. Каждый процесс состоит из одного или нескольких потоков.

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

Роль ядер процессора

Каждый поток в процессе — это задача, которую должен выполнить процессор. Большинство процессоров сегодня умеют выполнять одновременно две задачи на одном ядре, создавая дополнительное виртуальное ядро. Это называется одновременная многопоточность или многопоточность Hyper-Threading, если речь о процессоре от Intel. Эти процессоры называются многоядерными процессорами. Таким образом, двухъядерный процессор имеет 4 ядра: два физических и два виртуальных. Каждое ядро может одновременно выполнять только один поток.

Почему многопоточность?

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

Читайте также:  какие симптомы когда капает с конца

Время запачкать руки

Мы напишем программу, где запустим две функции (выполним две задачи). Сначала мы будем выполнять функции последовательно, через один и тот же поток, а затем создадим отдельные потоки для каждой из них. При этом отметим время выполнения для обоих подходов и увидим кое-что волшебное:

Summation1 и Summation2 — это два класса-заглушки, которые нам нужно выполнить. На их месте может быть любая бизнес-логика, которую вам захочется реализовать. Я только добавил случайные циклы, чтобы увеличить время выполнения — это поможет лучше продемонстрировать результаты:

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

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

На мой взгляд, понимание основ чего-то — лучший способ это “что-то” освоить. Надеюсь, эта статья помогла вам получить представление о необходимости и полезности многопоточного программирования.
Существует гораздо больше информации о потоках: жизненный цикл потоков, проблемы синхронности и способы их решения и т. д. Обязательно ознакомьтесь с ними. Как говорится, тяжело в учении — легко в бою!

Источник

Национальная библиотека им. Н. Э. Баумана
Bauman National Library

Персональные инструменты

Многопоточность

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

Содержание

Описание

Многопоточность (как доктрину программирования) не следует путать ни с многозадачностью, ни с многопроцессорностью, несмотря на то, что операционные системы, реализующие многозадачность, как правило, реализуют и многопоточность.

Достоинства
Недостатки

Аппаратная реализация

Различают две формы аппаратной реализации многопоточности:

Типы реализации потоков

Взаимодействие потоков

В многопоточной среде часто возникают проблемы, связанные с использованием параллельно исполняемыми потоками одних и тех же данных или устройств. Для решения подобных проблем используются такие методы взаимодействия потоков, как взаимоисключения (мьютексы), семафоры, критические секции и события. [Источник 2]

Особенности реализации

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

Еще одной областью исследований является то, какие типы событий должны вызывать переключение потоков: потери в кэш-памяти, межпотоковая связь, завершение DMA и т.д. [Источник 3]

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

Критика терминологии

Термин «поток» связан с переводами иностранной технической литературы, выполненными в 1970-х годах издательством «Мир». В настоящее время в «академических кругах» (то есть в учебниках, методических пособиях, курсах вузов, диссертациях и пр.) он считается эталонным. Это противоречит его же переводу «нить» в общеязыковом контексте, а также создаёт путаницу с термином Data stream.

Потоки называют также потоками выполнения (от англ. thread of execution); иногда называют «нитями» (буквальный перевод англ. thread) или неформально «тредами».

Источник

Многопоточные приложения в C# для чайников

Что такое многопоточные приложения? Грубо говоря, это приложения с несколькими «рабочими», которые одновременно выполняют разные или однотипные задачи. Зачем это нужно? Ресурсы компьютера используются не всегда эффективно. Например, ваша программа скачивает страницу из интернета, потом анализирует ее, затем — качает следующую. Во время анализа простаивает интернет соединение, а во время закачки — скучает процессор. Это можно исправить. Уже во время анализа текущей страницы параллельно качать следующую.

Я попытаюсь объяснить, как оптимизировать свои программы, используя многопоточность, и приведу пару примеров с кодом. Прошу под кат.

Для начала, давайте разберемся, когда можно распараллелить программу. Например, у вас есть один поток данных. Его нужно обрабатывать в строго определенном порядке и без результатов предыдущей обработки, следующую операцию выполнять нельзя. В такой программе можно создать дополнительные потоки, но будут ли они нужны? Мой преподаватель по компьютерным системам приводил следующий пример.

Допустим, у нас есть 2 рабочих, которые хорошо копают ямы. Предположим, что один выкопает яму глубиной 2 метра, за 1 час. Тогда два рабочих, выкопают эту яму за полчаса. Это похоже на правду. Давайте возьмем 3600 таких рабочих. Теоретически, они выкопают яму глубиной 2 метра за 1 секунду. Но на практике они будут друг другу мешать, топтаться на одном месте и нервничать.

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

Читайте также:  Что такое мускус бобра и для чего он

Далее немного практики. Что бы начать работать с потоками, необходимо подключить пространство имен System.Threading, добавив в начало файла с кодом следующую директиву:

Любой поток в C# это функция. Функции не могут быть сами по себе, они обязательно являются методами класса. Поэтому, что бы создать отдельный поток, нам понадобится класс с необходимым методом. Самый простой вариант метода возвращает void и не принимает аргументов:

Пример запуска такого потока:

После вызова метода Start() у объекта потока, управление вернется сразу, но в этот момент уже начнет работать ваш новый поток. Новый поток выполнит тело функции MyThreadFunction и завершится. Мой друг спросил меня, а почему функция не возвращает значение? А потому, что его некуда вернуть. После вызова Start(), управление передается дальше, при этом созданный поток может работать еще длительное время. Что бы обмениваться данными между потоками, можно пользоваться переменными класса. Об этом позже.

Кроме того, существует еще один вариант метода, из которого можно сделать поток. Выглядит он вот так:

Его назначение — применение в тех случаях, когда нужно при создании потоков, передавать им какие-то данные. Любой класс в C# наследуется от Object, поэтому можно передавать внутрь потока любой объект. Подробнее позже.

Давайте напишем простой пример, что бы проиллюстрировать работу потоков. Реализуем следующее. Наша программа запустит дополнительный поток, после чего выведет три раза на экран «Это главный поток программы!», в это время созданный поток выведет три раза на экран «Это дочерний поток программы!». Вот, что получилось:

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

Идем дальше. Что бы создать несколько потоков, необязательно использовать несколько функций, если потоки одинаковые. Вот пример:

Как видите, для создания 10 потоков нам понадобилась всего 1 функция.

Каждый поток имеет свой стек, поэтому локальные переменные метода для каждого потока свои. Что бы это продемонстрировать, мы создадим поток для метода класса, а потом вызовем этот же метод из главного потока. Вот код:

После завершения выполнения потоков, в консоле будет выведено 10 чисел.

Если же мы хотим, что бы каждый поток вел себя по-разному, то есть два решения. Первое — создание дополнительной функции для потока. Например, вот так:

Второе решение более запутанное. Смысл в том, что бы используя один метод выполнять разные действия в разных потоках. Для этого, внутри метода потока нужно определить «кто я?». Как вариант, используем метод потока, который может принимать значения. В поток будем передавать булевый флаг. Если он равен истине — значит это первый поток, если ложь — значит второй. Метод потока сам будет определять «кто я?» и в зависимости от этого, выполнять разные действия. Вот код:

Как работает данная программа? Каждый поток использует свой экземпляр метода. Если потоку выдали флаг true, значит он выполняет один код, если false — другой. Этот пример так же наглядно демонстрирует как работать с методами потоков, который принимают параметры.

Давайте разберем, как обмениваться данными между разными потоками. Перед тем, как я приведу пример с кодом, расскажу немного теории. Во всех многопоточных приложениях существует синхронизация данных. Когда два или больше потоков, пытаются взаимодействовать с какими либо данными одновременно — может возникнуть ошибка. Для этого в C# существует несколько способов синхронизации, но мы рассмотрим самый простой с помощью оператора lock.

Теперь давайте пример. Создадим отдельный класс, который будет создавать дополнительные потоки. Каждый поток будет работать с одной переменной (свойство класса). Для обеспечения сохранности данных мы будем использовать оператор lock. Его синтаксис очень простой:

Как он работает? Когда один из потоков, доходит до оператора lock, он проверяет, не заблокирован ли object1. Если нет — выполняет указанные в скобках операторы. Если заблокирован — ждет, когда object1 будет разблокирован. Таким образом, оператор lock предотвращает одновременное обращение нескольких потоков к одним и тем же данным.

А вот и пример программы:

Я использовал отдельную переменную, для оператора lock, поскольку он не может заблокировать доступ к int переменной. А вообще, мне как то на хабре посоветовали всегда использовать «локеры» для блокировки других данных.

Теперь о программе. Каждый поток сначала выводит значение свойства value, а потом увеличивает его на единицу. Зачем же тут нужен оператор lock? Запустите программу. Она выведет по порядку «0 1 2 3 4». Потоки по очереди выводят значение value, и все хорошо. Предположим, что оператора lock нету. Тогда может получиться следующее:

… и так далее. В результате мы получим, что-то вроде «0 0 1 3 4». Это связано с тем, что процессор и планировщик задач не знают в каком порядке выполняться операции, поэтому они делают это оптимально с точки зрения выполнения программы, при этом логика нарушается.

Думаю, на сегодня хватит, и так много получилось. Основные примеры можно скачать здесь. Пишите комментарии, отзывы и пожелания. Задавайте свои ответы. Удачной компиляции.

Источник

Информ портал о технике и не только