Опубликован: 23.04.2013 | Доступ: свободный | Студентов: 856 / 185 | Длительность: 12:54:00
Лекция 3:

Процессы и потоки в операционной системе

< Лекция 2 || Лекция 3: 12 || Лекция 4 >

Потоки. Взгляд программиста

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

Когда нужны потоки

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

  • последовательность (блок, составной оператор),
  • выбор (if, switch),
  • цикл (разнообразные формы).

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

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

A(); B(); C();

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

Вернемся, однако, из сказки к задачам программирования и рассмотрим такую прозаическую задачу как вычисление суммы элементов массива. Как нас учат находить сумму? Вначале переменной S нужно дать значение 0, а затем последовательно в цикле прибавлять к S очередной элемент массива. Выполнив N сложений, получим сумму N элементов. Время выполнения - величина порядка O(N). Но ведь этот последовательный классический алгоритм суммирования далеко не единственный. Суммирование можно вести в любом порядке, вычисления можно распараллелить. Если в нашем распоряжении имеется N/2 процессоров, то они параллельно могут вычислить суммы пар элементов, на следующем шаге можно получить суммы четверок. Продолжая этот процесс, можно вычислить сумму элементов за log(N) шагов и тогда за счет параллелизма время вычислений существенно сокращается, являясь величиной порядка O(Log(N)). Конечно, использовать потоки для распараллеливания таких задач, как вычисление суммы элементов, обычно не приходится. Но стоит заметить, что современные мощные процессоры, которые могут иметь несколько арифметических сопроцессоров, на уровне выполнения команд организуют конвейерную обработку, где частично осуществляется распараллеливание вычислений.

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

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

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

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

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

Реентерабельные и потоко-безопасные модули

Несколько слов о терминологии. То, что C# программист называет методом, с точки зрения потока ОС является модулем. Уже говорилось, что один и тот же модуль может быть вызван в разных потоках, так что код модуля будет одновременно выполняться на разных процессорах (ядрах) компьютера. Для гарантирования корректной работы модуля в такой ситуации на него накладываются определенные ограничения. Рассмотрим два важных понятия - реентерабельного и потоко-безопасного модуля.

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

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

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

Итоги

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

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

Создание многопоточных приложений сопряжено с двумя видами трудностей:

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

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

< Лекция 2 || Лекция 3: 12 || Лекция 4 >
Алексей Рыжков
Алексей Рыжков

не хватает одного параметра:

static void Main(string[] args)
        {
            x = new int[n];
            Print(Sample1,"original");
            Print(Sample1P, "paralel");
            Console.Read();
        }

Никита Белов
Никита Белов

Выставил оценки курса и заданий, начал писать замечания. После нажатия кнопки "Enter" окно отзыва пропало, открыть его снова не могу. Кнопка "Удалить комментарий" в разделе "Мнения" не работает. Как мне отредактировать недописанный отзыв?