что такое потоки в программировании
Потоки и работа с ними
Многопоточность позволяет увеличивать скорость реагирования приложения и, если приложение работает в многопроцессорной или многоядерной системе, его пропускную способность.
Процессы и потоки
Процесс — это исполнение программы. Операционная система использует процессы для разделения исполняемых приложений. Поток — это основная единица, которой операционная система выделяет время процессора. Каждый поток имеет приоритет планирования и набор структур, в которых система сохраняет контекст потока, когда выполнение потока приостановлено. Контекст потока содержит все сведения, позволяющие потоку безболезненно возобновить выполнение, в том числе набор регистров процессора и стек потока. Несколько потоков могут выполняться в контексте процесса. Все потоки процесса используют общий диапазон виртуальных адресов. Поток может исполнять любую часть программного кода, включая части, выполняемые в данный момент другим потоком.
Цели применения нескольких потоков
Используйте несколько потоков, чтобы увеличить скорость реагирования приложения и воспользоваться преимуществами многопроцессорной или многоядерной системы, чтобы увеличить пропускную способность приложения.
Представьте себе классическое приложение, в котором основной поток отвечает за элементы пользовательского интерфейса и реагирует на действия пользователя. Используйте рабочие потоки для выполнения длительных операций, которые, в противном случае будут занимать основной поток, в результате чего пользовательский интерфейс будет недоступен. Для более оперативной реакции на входящие сообщения или события также можно использовать выделенный поток связи с сетью или устройством.
Если программа выполняет операции, которые могут выполняться параллельно, можно уменьшить общее время выполнения путем выполнения этих операций в отдельных потоках и запуска программы в многопроцессорной или многоядерной системе. В такой системе использование многопоточности может увеличить пропускную способность, а также повысить скорость реагирования.
Наконец, можно использовать класс System.Threading.Thread, который представляет управляемый поток. Дополнительные сведения см. в разделе Использование потоков и работа с потоками.
Исключения следует обрабатывать в потоках. Необработанные исключения в потоках, как правило, приводят к завершению процесса. Дополнительные сведения см. в статье Исключения в управляемых потоках.
BestProg
Понятие потока. Архитектура потоков в C#. Потоки с опорными хранилищами. Потоки с декораторами. Адаптеры потоков
Содержание
Поиск на других ресурсах:
1. Что такое поток в программировании? Понятие потока
В программировании поток (stream) — это логическое устройство, предусматривающее:
Поток представляет собой абстракцию, которая обеспечивает ввод/вывод информации в программе. Система ввода/вывода связывает поток с физическим устройством (рисунок 1). Работа потока на ввод или на вывод содержит одинаковый набор команд независимо от физического устройства. Так, например, вывод на принтер или экран осуществляется одинаковыми вызовами функций или вывод на консоль работает так же как и вывод в файл. В свою очередь, одна и та же функция может работать с различными типами физических устройств.
Рисунок 1. Взаимодействие потока с различными типами физических устройств ввода/вывода (принтер, удаленный компьютер, файл)
Потоки с опорными хранилищами реализуют конкретный вид хранилища, которым может быть:
Потоки с декораторами реализуют модификацию данных, передаваемых в опорные хранилища. Примерами такой модификации могут быть:
Для модификации уже существующего потока, потоки с декораторами используют подход, заложенный в паттерне Декоратор. Подобные схемы использования паттерна Декоратор применяются и в других языках программирования (например, Java).
Обе категории потоков работают исключительно с байтами. Для представления байтов в текстовом, понятном для человека, виде, используются адаптеры потоков.
3. Потоки с опорными хранилищами. Обзор
Потоки с опорными хранилищами связаны с определенным типом хранилища: файлы, память, сеть и тому подобное. Основные потоки с опорными хранилищами представлены следующими классами:
4. Потоки с декораторами. Обзор
Потоки с декораторами реализуют модификацию (трансформацию) передаваемых данных в опорные хранилища для их хранения или иного использования. Потоки с декораторами используют паттерн Декоратор для модификации существующего потока данных в нужный. Ниже перечислены основные классы, которые обеспечивают работу потоков с декораторами:
5. Адаптеры потоков. Назначение. Обзор
Адаптеры потоков относятся к более высокому уровню взаимодействия с программой. Они позволяют конвертировать байтовые потоки (потоки с декораторами, потоки с опорными хранилищами) в конкретный формат.
Адаптеры потоков работают по единому принципу: они помещают байтовый поток в оболочку адаптерного класса с соответствующими методами. Эти методы выполняют преобразование байтового потока данных к нужному формату (например, получение XML-формата данных).
Ниже перечислены основные классы, относящиеся к адаптерам потоков:
Потоки, блокировки и условные переменные в C++11 [Часть 1]
В первой части этой статьи основное внимание будет уделено потокам и блокировкам в С++11, условные переменные во всей своей красе будут подробно рассмотрены во второй части…
Потоки
В C++11, работа с потокам осуществляется по средствам класса std::thread (доступного из заголовочного файла
Также следует отметить, что если функция потока кидает исключение, то оно не будет поймано try-catch блоком. Т.е. следующий код не будет работать (точнее работать то будет, но не так как было задумано: без перехвата исключений):
Для передачи исключений между потоками, необходимо ловить их в функции потока и хранить их где-то, чтобы, в дальнейшем, получить к ним доступ.
Блокировки
Программа должна выдавать примерно следующее:
Теперь, результат работы программы будет следующего вида:
Можно поспорить насчет того, что метод dump() должен быть константным, ибо не изменяет состояние контейнера. Попробуйте сделать его таковым и получите ошибку при компиляции:
Предположим, что эта функция вызвана из двух разных потоков, из первого потока: элемент удаляется из 1 контейнера и добавляется во 2, из второго потока, наоборот, элемент удаляется из 2 контейнера и добавляется в 1. Это может вызвать deadlock (если контекст потока переключается от одного потока к другому, сразу после первой блокировки).
2.2 Потоки
От переводчика: данная статья является седьмой в цикле переводов официального руководства по библиотеке SFML. Прошлую статью можно найти тут. Данный цикл статей ставит своей целью предоставить людям, не знающим язык оригинала, возможность ознакомится с этой библиотекой. SFML — это простая и кроссплатформенная мультимедиа библиотека. SFML обеспечивает простой интерфейс для разработки игр и прочих мультимедийных приложений. Оригинальную статью можно найти тут. Начнем.
1. Приступая к работе
Что такое поток?
Большая часть из вас уже знает, что такое поток, однако объясним, что это такое, для новичков в данной теме.
Поток — это по сути последовательность инструкций, которые выполняются параллельно с другими потоками. Каждая программа создает по меньшей мере один поток: основной, который запускает функцию main(). Программа, использующая только главный поток, является однопоточной; если добавить один или более потоков, она станет многопоточной.
Так, короче, потоки — это способ сделать несколько вещей одновременно. Это может быть полезно, например, для отображения анимации и обработки пользовательского ввода данных во время загрузки изображений или звуков. Потоки также широко используется в сетевом программировании, во время ожидания получения данные будет продолжаться обновление и рисование приложения.
Потоки SFML или std::thread?
В своей последней версии (2011), стандартная библиотека C++ предоставляет набор классов для работы с потоками. Во время написания SFML, стандарт C++11 еще не был написан и не было никакого стандартного способа создания потоков. Когда SFML 2.0 был выпущен, было много компиляторов, которые не поддерживали этот новый стандарт.
Создание потоков с помощью SFML
Хватит разглагольствований, давайте посмотрим на код. Класс, дающий возможность создавать потоки с помощью SFML, называется sf::Thread, и вот как это (создание потока) выглядит в действии:
В этом коде функции main и func выполняются параллельно после вызова thread.launch(). Результатом этого является то, что текст, выводимый обеими функциями, смешивается в консоли.
Точка входа в поток, т.е. функция, которая будет выполняться, когда поток запускается, должна быть передана конструктору sf::Thread. sf::Thread пытается быть гибким и принимать различные точки входа: non-member функции или методы классов, функции с аргументами или без них, функторы и так далее. Приведенный выше пример показывает, как использовать функцию-член, вот несколько других примеров.
Последний пример, который использует функтор, является наиболее мощным, поскольку он может принимать любые типы функторов и поэтому делает класс sf::Thread совместимым со многими типами функций, которые напрямую не поддерживаются. Эта функция особенно интересна с лямбда-выражениями C++11 или std::bind.
Если вы хотите использовать sf::Thread внутри класса, не забудьте, что он не имеет стандартного конструктора. Поэтому, вы должны инициализировать его в конструкторе вашего класса в списке инициализации:
Если вам действительно нужно создать экземпляр sf::Thread после инициализации объекта, вы можете создать его в куче.
Запуск потока
После того, как вы создали экземпляр sf::Thread, вы должны запустить его с помощью запуска функции.
launch вызывает функцию, которую вы передали в конструктор нового потока, и сразу же завершает свою работу, так что вызывающий поток может сразу же продолжить выполнение.
Остановка потоков
Поток автоматически завершает свою работу, когда функция, служащая точкой входа для данного потока, возвращает свое значение. Если вы хотите ждать завершения потока из другого потока, вы можете вызвать его функцию wait.
Функция ожидания также неявно вызывается деструктором sf::Thread, так что поток не может оставаться запущенным (и бесконтрольным) после того, как его экземпляр sf::Thread уничтожается. Помните это, когда вы управляете вашими потоками (смотрите прошлую секцию статьи).
Приостановка потока
В SFML нет функции, которая бы предоставляла способ приостановки потока; единственный способ приостановки потока — сделать это из кода самого потока. Другими словами, вы можете только приостановить текущий поток. Что бы это сделать, вы можете вызвать функцию sf::sleep:
sf::sleep имеет один аргумент — время приостановки. Это время может быть выражено в любой единице, как было показано в статье про обработку времени.
Обратите внимание, что вы можете приостановить любой поток с помощью данной функции, даже главный поток.
sf::sleep является наиболее эффективным способом приостановить поток: на протяжении приостановки потока, он (поток) практически не потребляет ресурсы процессора. Приостановка, основанная на активном ожидании, вроде пустого цикла while, потребляет 100% ресурсов центрального процессора и делает… ничего. Однако имейте в виду, что продолжительность приостановки является просто подсказкой; реальная продолжительность приостановки (больше или меньше указанного вами времени) зависит от ОС. Так что не полагайтесь на эту функцию при очень точном отсчете времени.
Защита разделяемых данных
Все потоки в программе разделяют некоторую память, они имеют доступ ко всем переменным в области их видимости. Это очень удобно, но также опасно: с момента параллельного запуска потока, переменные или функции могут использоваться одновременно разными потоками. Если операция не является потокобезопасной, это может привести к неопределенному поведению (т. е. это может привести к сбою или повреждению данных).
Существует несколько программных инструментов, которые могут помочь вам защитить разделяемые данные и сделать ваш код потокобезопасным, их называют примитивами синхронизации. Наиболее распространенными являются следующие примитивы: мьютексы, семафоры, условные переменные и спин-блокировки. Все они — варианты одной и той же концепции: они защищают кусок кода, давая только определенному потоку право получать доступ к данным и блокируя остальные.
Наиболее распространенным (и используемым) примитивом является мьютекс. Мьютекс расшифровывается как «Взаимное исключение». Это гарантия, что только один поток может выполнять код. Посмотрим, как мьютексы работают, на примере ниже:
Этот код использует общий ресурс (std::cout), и, как мы видим, это приводит к нежелательным результатам. Вывод потоков смешался в консоли. Чтобы убедиться в том, что вывод правильно напечатается, вместо того, чтобы быть беспорядочно смешанным, мы защищаем соответствующие области кода мьютексом.
Первый поток, который достигает вызова mutex.lock(), блокирует мьютекс и получает доступ к коду, который печатает текст. Когда другие потоки достигают вызова mutex.lock(), мьютекс уже заблокирован, и другие потоки приостанавливают свое выполнение (это похоже на вызов sf::sleep, спящий поток не потребляет время центрального процессора). Когда первый поток разблокирует мьютекс, второй поток продолжает свое выполнение, блокирует мьютекс и печатает текст. Это приводит к тому, что текст в консоли печатается последовательно и не смешивается.
Мьютекс — это не только примитив, который вы можете использовать для защиты разделяемых данных, вы можете использовать его во многих других случаях. Однако, если ваше приложение делает сложные вещи при работе с потоками, и вы чувствуете, что возможностей мьютексов недостаточно — не стесняйтесь искать другую библиотеку, обладающую большим функционалом.
Защита мьютексов
Не волнуйтесь: мьютексы уже потокобезопасны, нет необходимости их защищать. Но они не безопасны в плане исключений. Что происходит, если исключение выбрасывается, когда мьютекс заблокирован? Он никогда не может быть разблокирован и будет оставаться заблокированным вечно. Все потоки, пытающиеся разблокировать заблокированный мьютекс, будут заблокированы навсегда. В некоторых случаях, ваше приложение будет «заморожено».
Чтобы быть уверенным, что мьютекс всегда разблокирован в среде, в которой он (мьютекс) может выбросить исключение, SFML предоставляет RAII класс, позволяющий обернуть мьютекс в класс sf::Lock. Блокировка происходит в конструкторе, разблокировка происходит в деструкторе. Просто и эффективно.
Помните, что sf::Lock может также быть использован в функциях, которые имеют множество возвращаемых значений.
Распространенные заблуждения
Вещь, часто упускаемая из виду: поток не может существовать без соответствующего экземпляра sf::Thread. Следующий код можно часто увидеть на форумах:
Программисты, которые пишут подобный код, ожидают, что функция startThread() будет запускать поток, который будет жить самостоятельно и уничтожаться при завершении выполнения функции (переданной в качестве точки входа). Этого не происходит. Функция потока блокирует главный поток, как если бы программа не работала.
В чем дело? Экземпляр sf::Thread является локальным для функции startThread(), поэтому немедленно уничтожаются, когда функция возвращает свое значение. Вызывается деструктор sf::Thread, происходит вызов wait(), как утверждалось выше, результатом этого становится блокировка главного потока, который ожидает завершения функции потока, вместо параллельного выполнения с ней.
Так что не забывайте: вы должны управлять экземплярами sf::Thread, чтобы они жили так долго, как требуется функции потока.
Потоки — это Goto параллельного программирования
Автор фото: Rainer Zenz
Сперва — немного истории и отсылок с уже состоявшимся обсуждениям.
Goto considered harmful
На Хабре тема использования/изгнания Goto из программ на языках высокого уровня поднималась неоднократно:
habrahabr.ru/post/114211
habrahabr.ru/post/114470
habrahabr.ru/post/114326
Несомненно, существование Goto — источник нескончаемого холивара. Однако современные языки «общего назначения», приблизительно начиная с Java, не включают в свой синтаксис Goto, по крайней мере в его первозданном виде.
Где Goto ещё в ходу
Отмечу одно часто применяемое, но ещё не упомянутое применение операции прыжка по метке, которое лично меня касается достаточно сильно: языки ассемблера и машинные коды. Практически все архитектуры микропроцессоров имеют инструкции условных и безусловных переходов. Более того, я не припомню ассемблера, в котором аппаратно сделан оператор for или while. В результате программисты, работающие на этом уровне абстракции, вынуждены разбираться со всей мешаниной нелокальных переходов. У Дейкстры по этому поводу есть замечание: «. goto должен быть изгнан из всех высокоуровневых языков (т.е. отовсюду, кроме — может быть — простого машинного кода)» [в оригинале: «everything except —perhaps— plain machine code»].
Опущу описание всех известных аргументов против Goto; желающие могут найти их по ссылкам выше. Напишу сразу вывод, как его понимаю я: использование Goto значительно понижает «высокоуровневость» кода, пряча алгоритм в деталях последовательной реализации. Перейдём лучше к потокам.
В чём заключается проблема потоков
Вывод почти дословно повторяет тот, что был сделан чуть выше: использование потоков значительно понижает «высокоуровневость» кода, пряча алгоритм в деталях параллельной реализации. «Ручное управление» потоками в программе, написанной на языке высокого уровня, обнажает многие детали нижележащей аппаратуры, которые при этом видеть не хочется.
Что же, если не потоки?
Как же использовать возможности многоядерной аппаратуры, не прибегая к потокам? Конечно же, есть различные языки программирования, изначально спроектированные с расчётом на эффективное написание параллельных программ. Тут и Erlang, и функциональные языки. Если нужна экстремальная масштабируемость решения, следует искать ответ в них и предлагаемых ими механизмах. Но что делать программистам, использующим более традиционные языки, например, Си++, и/или работающих с уже существующим кодом?
OpenMP — хорошо, да не то
Довольно долго ни в С, ни в C++ (в отличие от, например, более «молодой» Java) наличие параллелизма в программах никак не было отражено, т.е. фактически было отдано на откуп «сторонним» библиотекам вроде Pthread. Довольно давно известен OpenMP, вносящий структурированный fork-join параллелизм в эти языки, а также в Fortran. По моему мнению, этот стандарт не приносит решений, связанных указанными выше проблемами потоков. Т.е. OpenMP — всё ещё слишком низкоуровневый механизм. Последняя ревизия стандарта не предложила повышения уровня абстракции, а добавила возможностей (и сложностей) тем, кто хочет с помощью OpenMP пускать коды на гетерогенных системах (подробнее про версию 4.0 писали на Хабре).
Расширения и библиотеки
Между новыми языками, изначально пытающимися поддержать параллелизм, и традиционными языками, полностью его игнорирующими, лежат расширения — попытки добавить необходимые абстракции и закрепить их в синтаксисе, — и библиотеки — завёрнутые в уже существующие концепции языка (такие как вызов подпрограмм) решения проблем. Расширения языков теоретически позволяют добиться лучших результатов, чем библиотеки, ведь с их помощью мы вырываемся из ограничений исходного языка, создавая новый. Но очень нечасто такие расширения завоёвывают популярность у широкой аудитории пользователей. Признание зачастую приходит только после стандартизации такого расширения как части языка.
Расширениями языков и библиотеками, в том числе для параллельного программирования, занимаются многие компании, университеты и комбинации оных. У Intel, есть, конечно же, много раз упоминавшиеся на Хабре варианты и первого, и второго: Intel Cilk Plus, Intel Threading Building Blocks. Выражу своё мнение, что Cilk (Plus) более интересен как средство повышения уровня абстракции параллелизма чем TBB. Радует наличие поддержки его в GCC.
Что ещё почитать
Напоследок хочу поделиться одной книгой. Главная её идея для меня заключается в том, что необходимо внесение понимания о существовании структуры у параллельных приложений в процесс их проектирования. Более того, необходимо обучать этому студентов максимально рано, примерно в то же время, когда им объясняют, почему«goto — это плохо».
Michael McCool, Arch Robison, James Reinders. Structured Parallel Programming — 2012 — parallelbook.com.
В книге, в частности, показаны решения одних и тех же задач с использованием нескольких библиотек/языков параллельного программирования: Intel Cilk Plus, OpenMP, Intel TBB, OpenCL и Intel ArBB. Это позволяет сравнить выразительность и эффективность указанных подходов в различных условиях практических задач.