Что такое поток в программировании
Перейти к содержимому

Что такое поток в программировании

  • автор:

[C++] часть 1: многопоточность, конкурентность и параллелизм: ОСНОВЫ

Простое руководство по изучению многопоточности, конкурентности и параллелизма в C++

Вначале, когда ещё только состоялось моё знакомство с многопоточностью в C++, многое было мне непонятным и сбивало с толку. Сложность программы расцветала буйным цветом (именно так: подобно прекрасному цветку), конкурентность и параллелизм с их недетерминированным поведением меня просто убивали, и всё было как в тумане. Так что мне легко понять всех приступающих к изучению этих понятий. Спешу избавить вас от мучений и предлагаю вашему вниманию это простое руководство по изучению конкурентности, параллелизма и многопоточности в C++ (в конце данной статьи расписан план, в соответствии с которым мы будем двигаться дальше).

А пока освежим в памяти основные понятия и попробуем на вкус код, выполняемый в многопоточной среде.

1. Что такое поток?

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

2. Что такое конкурентность/параллелизм

Планировщик распределяет процессорное время между разными потоками. Это называется аппаратным параллелизмом или аппаратной конкурентностью (пока что считаем здесь параллелизм и конкурентность синонимами): когда несколько потоков выполняются на разных ядрах параллельно, причём каждый занимается конкретной задачей программы.
Примечание: чтобы определить количество задач, которые реально можно выполнять в многопоточном режиме на том или ином компьютере, используется функция std::thread::hardware_concurrency() . Если число потоков будет превышать этот лимит, может начаться настоящая чехарда с переключением задач (когда слишком частые переключения между задачами — много раз в секунду — создают лишь иллюзию многопоточности).

3. Основные операции с потоками с помощью std::thread

  • Заголовочный файл| #include <thread>
  • Запуск потока| std::thread t(callable_object, arg1, arg2, ..)
    Создаёт новый поток выполнения, ассоциируемый с t, который вызывает callable_object(arg1, arg2) . Вызываемый объект (т.е. указатель функции, лямбда-выражение, экземпляр класса с вызовом функции operator ) немедленно выполняется новым потоком с (выборочно) передаваемыми аргументами. Они копируются по умолчанию. Если хотите передать по ссылке, придётся использовать метод warp к аргументу с помощью std::ref(arg) . Не забывайте: если хотите передать unique_ptr, то должны переместить его ( std::move(my_pointer) ), так как его нельзя копировать.
  • Жизненный цикл потока| t.join() и t.detach()
    Если основной поток завершает выполнение, все второстепенные сразу останавливаются без возможности восстановления. Чтобы этого не допустить, у родительского потока имеются два варианта для каждого порождённого:
    → Блокирует и ждёт завершения порождённого потока, вызывая на нём метод join .
    → Прямо объявляет, что порождённый поток может продолжить выполнение даже после завершения родительского, используя метод detach .
  • Запомните: объект потока можно перенести, но нельзя копировать.

Здесь вы можете найти пример кода, иллюстрирующий практически всё, что написано выше.

4. Зачем нужна синхронизация?

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

  • Память — дом с привидениями
    Память никогда больше не будет обычным хранилищем данных — теперь это обитель привидений. Представьте: поток смотрит Netflix, уютно устроившись перед Smart TV, и тут вдруг экран мигает и выключается. В панике поток набирает 112, а в ответ… «Доставка пиццы, спасибо, что позвонили». Что происходит? А то, что в доме полно привидений (где в роли привидений другие потоки): они все в одной комнате и взаимодействуют с одними и теми же объектами (это называется гонка данных), но друг для друга они привидения.

Поток должен объявить, что он использует. А затем, прежде чем трогать этот объект, проверить, не использует ли его кто-то ещё. Зелёный поток смотрит ТВ? Значит, никто не должен трогать ТВ (другие могут рядышком сесть и посмотреть, если что). Это можно сделать с помощью мьютекса.

  • Нужны атомарные операции!
    Большинство операций неатомарные. Если операция неатомарная, можно увидеть её промежуточное состояние, так как она не является неделимой. Например: запись 64 битов, 32 бита за один раз. Во время этой операции другой поток может увидеть 32 старых бита и 32 новых, получая совершенно неверный результат. По этой причине результаты таких операций должны казаться атомарными, даже если они такими не являются.
    Примечание: даже инкремент не является атомарной операцией: int tmp = a; a = tmp + 1;
    Самое простое решение здесь — использовать шаблон std::atomic , который разрешает атомарные операции разных типов.
  • Когерентность кеша и выполнение с изменением очерёдности
    Каждое ядро пытается сохранить результаты какой-то работы, помещая недавние значения в локальный кеш. Но несколько потоков выполняются на разных ядрах, и значения, хранящиеся в кеше, больше не могут быть валидными, так что рано или поздно кеш должен обновляться. В то же время изменения не видны другим, пока кеш не очищен. Чтобы распространить изменения и обеспечить корректную видимость памяти, нужны определённые механизмы.
    Кроме того, для повышения эффективности процессор и/или компилятор может поменять очерёдность выполнения команд. Это может привести к непредсказуемому поведению в параллельно выполняемой программе, в связи с чем необходимо гарантировать исполнение критически важных команд в первоначальном порядке.
    Эта работа выполняется примитивами синхронизации, предполагающими использование барьеров доступа к памяти (строки кода, которые не вычеркнуть какими-то операциями) для обеспечения согласованности и предотвращения изменения очерёдности выполнения (инструкции внутри барьеров памяти нельзя вытащить оттуда).

Пример кода

Обратимся к коду. Теперь вы сами можете проверить это недетерминированное поведение многопоточности.

В отличие от однопоточной реализации, каждое выполнение даёт разный и непредсказуемый результат (единственное, что можно сказать определённо: строки А и B упорядочены по возрастанию). Это может вызвать проблемы, когда очерёдность команд имеет значение.

Но что здесь происходит? После того как поток А оценивает «значение» как истинное, поток B меняет его. Теперь мы внутри блока if , даже если нарушены ограничения.

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

Доступ должен быть синхронизирован.

Заключение

Вы можете сказать: «Батюшки! Сколько всего намешано в этой статье!» Просто помните, что не надо пытаться понять всё и сразу, важно ухватить основные идеи.

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

C#. Понятие потока. Архитектура потоков в C#. Потоки с опорными хранилищами. Потоки с декораторами. Адаптеры потоков

Понятие потока. Архитектура потоков в C#. Потоки с опорными хранилищами. Потоки с декораторами. Адаптеры потоков

Содержание

  • 1. Что такое поток в программировании? Понятие потока
  • 2. Архитектура потоков в .NET. Категории потоков
  • 3. Потоки с опорными хранилищами. Обзор
  • 4. Потоки с декораторами. Обзор
  • 5. Адаптеры потоков. Назначение. Обзор

Поиск на других ресурсах:

1. Что такое поток в программировании? Понятие потока

В программировании поток (stream) — это логическое устройство, предусматривающее:

  • потребление (получение) информации. В этом случае определяют термин поток ввода;
  • выработка (передача) информации. В этом случае определяют термин поток вывода.

Поток представляет собой абстракцию, которая обеспечивает ввод/вывод информации в программе. Система ввода/вывода связывает поток с физическим устройством (рисунок 1). Работа потока на ввод или на вывод содержит одинаковый набор команд независимо от физического устройства. Так, например, вывод на принтер или экран осуществляется одинаковыми вызовами функций или вывод на консоль работает так же как и вывод в файл. В свою очередь, одна и та же функция может работать с различными типами физических устройств.

C#. Взаимодействие потока с различными типами физических устройств ввода/вывода (принтер, удаленный компьютер, файл)

Рисунок 1. Взаимодействие потока с различными типами физических устройств ввода/вывода (принтер, удаленный компьютер, файл)

2. Архитектура потоков в .NET. Категории потоков

В технологии .NET потоки делятся на две основные категории (рисунок 2):

  • потоки с опорными хранилищами;
  • потоки с декораторами.

Потоки с опорными хранилищами реализуют конкретный вид хранилища, которым может быть:

  • файл;
  • память;
  • сеть;
  • изолированное хранилище.

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

  • шифрование данных перед отправкой в сети;
  • архивирование данных;
  • сжатие данных и их распаковки известными методами;
  • буферизация данных.

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

C#. Архитектура потоков в .NET

Рисунок 2. Архитектура потоков в .NET

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

3. Потоки с опорными хранилищами. Обзор

Потоки с опорными хранилищами связаны с определенным типом хранилища: файлы, память, сеть и тому подобное. Основные потоки с опорными хранилищами представлены следующими классами:

  • FileStream — класс, обеспечивает поток для файла. Класс содержит разнообразные средства обработки файлов. Эти средства обеспечивают как синхронное и асинхронное чтение из файла, так и синхронную и асинхронную запись в файл;
  • IsolatedStorage — абстрактный класс, который служит базовым для классов, реализующих доступ к изолированному хранилищу для файлов;
  • MemoryStream — класс, предназначенный для обработки потоков, которые размещаются в памяти;
  • NetworkStream — класс, содержащий средства представления потока данных в сети.
4. Потоки с декораторами. Обзор

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

  • BufferedStream — класс, содержащий средства буферизации при чтении данных из потока и записи данных в поток. Чтение/запись данных осуществляется через буфер — участок памяти заданного размера;
  • DeflateStream — класс, обеспечивающий методы для сжатия и распаковки потоков данных. Класс использует Deflate-алгоритм сжатия без потерь;
  • GZipStream — класс, реализующий методы и свойства для сжатия/распаковки данных потока на основе спецификации формата данных GZip;
  • CryptoStream — класс, осуществляющий над потоком данных криптографические преобразования.

Потоки с декораторами выделены в отдельный раздел классов в архитектуре .NET. Такое представление дает следующие преимущества:

  • потоки с декораторами отделяют операции шифрования, сжатия и другие от операций, применяемых в потоках с опорными хранилищами;
  • использование потоков с декораторами освобождает потоки с опорными хранилищами от необходимости выполнения шифрования, сжатия и т.д.;
  • декорирование потоков не зависит от изменения интерфейса в программе;
  • потоки-декораторы можно подключать во время выполнения;
  • поддержка паттерна Декоратор дает возможность объединять декораторы в цепочки (например, шифрование + сжатие).
5. Адаптеры потоков. Назначение. Обзор

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

Потоки информации: особенности и разновидности

Программирование предусматривает работу с огромным количеством данных. Чтобы создать приложение, нужно выбрать язык программирования, а также научиться использовать его инструменты и возможности.

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

Определение

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

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

Потоки (данных и не только) – последовательность инструкций, выполняемых параллельно с другими потоками. Каждая программа создает минимум одну такую «направленность».

Существуют различные виды потоков. Классификация может быть по типу движения данных:

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

Также есть разделение по типу используемых данных. Не всегда приложения работают непосредственно с байтами – в языках разработки, включая Java и C++, может использоваться текстовая информация. На основании этого выделяются следующие виды потоков:

  • байтовые;
  • символьные.

Также в разработке ПО различают следующие типы потоков данных:

  1. C++. Здесь работа осуществляется вместе со стандартной библиотекой языка. А именно – при помощи iostream.
  2. Пространство имен System.IO. Этот вариант актуален для всех языков платформы NET.Framework.
  3. Пакет java.io. Такая реализация поддерживается на платформе Java.

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

Способ организации

Поток определяет способ организации работы с электронными материалами в системе или приложении. В данном смысле существует иная классификация. Она включает в себя множество вариантов, но ключевыми в JavaScript и C++ являются такие методы:

  • однонаправленный;
  • двунаправленный (или многопоточный).

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

Однонаправленный вариант

Так называется поток, который определяет всего одну задачу в каждой части приложения – она может или получить данные, или передать. Его направление не будет меняться. Оно функционирует всегда по одному и тому же принципу.

Однонаправленные потоки данных в реальной жизни имеют простой пример-интерпретацию – водопровод. Модуль здесь будет выступать частью трубы:

Здесь сведения будут переходить от одного модуля к другому. Выходные сведения предыдущего становятся входными для последующего «блока».

Выше – пример того, как на практике выглядит соответствующая картина.

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

Двунаправленный вариант

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

Этот вариант чаще всего используется для связи модели и представления. Когда обновление (пример – текст в поле ввода) сразу корректирует остальные сведения в заданной модели – это и есть двунаправленная связь.

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

Асинхронность

Видов потоков данных много – классифицировать их удается по разным характеристикам. Если кто-нибудь попросит «Назовите основные типы потоков», проблем с этим не будет. В плане обработки электронных материалов в операционных системах и разработке ПО преобладает асинхронность.

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

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

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

В JavaScript

JavaScript – язык программирования, который умеет обрабатывать только один поток данных. Задача асинхронности здесь решена при помощи специального инструмента – Worker.

Он подходит для:

  • обработки сложных операций, которые могут выполняться без блокировки работы пользователя;
  • внесение корректировок в отдельные файлы-модули;
  • создание для каждого сформированного модуля объекта Worker, используемого для коммуникации.

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

Java в плане потоковых вопросов – это более гибкий язык. Он поддерживает многопоточность. Такая синхронизация выполняется за счет ключевого слова synchronized. Оно применяется в классах для определения синхронизированных блоков и методов.

Ключевое слово synchronized не подходит для переменных и атрибутов при определении классов. У Java также есть специальная библиотека, включающая в себя спецклассы для работы с несколькими потоками данных.

Выше – пример синхронизации методов в Java. Здесь можно увидеть больше наглядных примеров работы с потоками.

Стандартная библиотека C++ поддерживает средства, при помощи которых программа обрабатывает потоки данных асинхронно. В ней есть набор классов для работы с соответствующими элементами.

Сейчас существует стандарт SFML. В нем можно создавать потоки при помощи sf::Thread:

Функции main и func выполняются параллельно. Происходит это после того, как была выполнена «операция», которая называется thread.launch(). Результат – текст, выводимый обеими функциями, будет смешиваться в консоли:

  1. Точка входа в поток должна быть передана в sf::Thread.
  2. Sf::Thread старается быть гибким. Он пытался принимать различные точки входа: методы классов или non-member-функции, выражения с аргументами или без них, функторы и так далее.
  3. Заданный пример показывает, как реализовывать функцию-член.

C++ может различными способами обрабатывать потоки данных:

  • get_id – возвращает идентификатор;
  • sleep_for – «спит» на протяжении установленного промежутка времени;
  • sleep_untill – «спит» до конкретного момента;
  • yield – перенастройка текущих потоков путем передачи приоритетов.

Также вместо std::Thread можно воспользоваться std::async. Данный вариант менее мощный, но его намного проще использоваться, если нужно асинхронно запустить ту или иную функцию:

Здесь можно увидеть больше информации о многопоточности непосредственно в C++.

Как быстро освоить направление

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

Но есть и более простое и современное решение. Оно носит название онлайн-курсов. Пример – от образовательного центра OTUS. Дистанционно пользователи смогут освоить одно или несколько IT-направлений и профессий одновременно. На занятиях не только научат создавать и работать с потоками, но и писать сложные коды.

Хотите освоить современную IT-специальность? Огромный выбор курсов по востребованным IT-направлениям есть в Otus! Ниже – один из них:

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

Процесс — экземпляр программы во время выполнения, независимый объект, которому выделены системные ресурсы (например, процессорное время и память). Каждый процесс выполняется в отдельном адресном пространстве: один процесс не может получить доступ к переменным и структурам данных другого. Если процесс хочет получить доступ к чужим ресурсам, необходимо использовать межпроцессное взаимодействие.

Начнем с начала — с определений — Что такое процесс?

Грубо говоря процесс это одно отдельно запущеное приложение.

Процессу выделены какие-те ресурсы и он живет в своем отдельном адресном пространсте и никто в его домик проникнуть не может.

Если ему или другим процессам понадобится обмениваться данными им придется использовать межпроцесные взаимодействия: например файл, сокет, канал, файл в памяти, очередь сообщений или RPC

Что такое поток?

Поток использует то же самое пространства стека, что и процесс, а множество потоков совместно используют данные своих состояний. Каждый поток может работать (читать и писать) с одной и той же областью памяти, в отличие от процессов, которые не могут просто так получить доступ к памяти другого процесса. У каждого потока есть собственные регистры и собственный стек, но другие потоки могут их использовать.

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

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

Как представлен поток в JAVA?

  • Поток — объект у класса которого есть методы run() и start()
  • После вызова метода start(), через какое-то время будет вызван run()
  • Метод run() будет выполнен в своем стеке.

Роль операционной системы

Вот этим «через некоторое время будет запущем метод run()» занимается операционная система, а точнее её планировщик потоков, который переключает контексты исполнения, раскладывая потоки на реальные ядра. То есть, когда мы вызываем у нашего объекта поток метод start(), мы говорим операционной системе:»Запусти пожалуста код, что находится в методе run() в отдельном потоке» и она спустя какое-то время это делает

Роль операционной системы

  • Создает потоки
  • Переключает потоки
  • API для уведомления потока

Запуск потоков

На данной картинке мы видим как происходит запуск потоков во времени. Запускать потоки мы можем в любом месте кода, один поток может породить другой(что здесь и происходит). На этой картинке всё понятно и предсказуемо.

Порядок не определен!

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

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *