Чем многопоточность отличается от многозадачности
Перейти к содержимому

Чем многопоточность отличается от многозадачности

  • автор:

5. МНОГОЗАДАЧНОСТЬ И МНОГОПОТОЧНОСТЬ

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

5.2. Указания по подготовке к выполнению лабораторной работы Многозадачность на сегодняшний день – одна из определяющих особенностей операционных систем. При подготовке к лабораторной работе необходимо ознакомится с теоретическим описанием принципа многозадачности и особенностями ее аппаратной реализации. Внимательно проработать вопросы обеспечения многозадачности (multitasking) и многопоточности (multithreading) при- ложений Windows. При подготовке к работе необходимо изучить конспект лекций по указанной теме, методические указания, а также разделы, указанные в [16, c.763-809], [17, c.59-87]. 5.3. Обзор темы работы Многозадачность (multitasking) – это способность операционной системы выполнять несколько программ одновременно. В основе этого принципа лежит использование операционной системой аппаратного таймера для выделения отрезков времени для каждого из одновременно выполняемых процессов. Если эти отрезки времени достаточно малы, и машина не перегружена слишком большим числом программ, то пользователю кажется, что все эти программы выполняются параллельно. Многопоточность – это возможность программы самой быть многозадачной. Программа может быть разделена на отдельные потоки выполнения, которые, как кажется, выполняются параллельно. В лабораторной работе изучаются функции порождения и завершения процесса CreateProcess, ExitProcess, TerminateProcess, создания и завершения потока CreateThread, ExitThread. Особое внимание в лабораторной работе уделяется исследованию возможностей синхронизации процессов и потоков. Существует большой класс задач (например, в управлении базами данных, параллельных вычислениях), в которых параллельно функционирующие программы (или их модули) нуждаются в обмене информации или порядок выполнения одних из программных модулей зависит от выполнения других. Windows предоставляет две специальные возможности синхронизировать параллельно выполняемые задачи – это семафоры и события. Семафор действует как обычных флаг, и используется для того, чтобы определить свободен или нет в настоящее время требующийся потоку или процессу ресурс. Пользователь может определять для семафора количество ресурсов, доступных для использования параллельными задачами. При занятии потоком какого-либо количества свободных ресурсов происходит декрементация количества ресурсов, и если, оставшееся число ресурсов недостаточно следующему потоку, он приостанавливается до момента освобождения необходимого числа ресурсов. Для управления семафорами используются функции CreateSemaphore, ReleaseSemaphore, OpenSemaphore, WaitForSingleObject. События являются самой примитивной разновидностью объектов синхронизации. Они используются для того, чтобы уведомить поток о том, что наступило ожидаемое событие. Эти объекты обычно используются для того, чтобы

синхронизировать потоки, которые работают по принципу конвейера. Например, один поток опрашивает датчики и загружает считанные значения в буфер. Другой поток считывает эти данные из буфера и производит их обработку. Первый поток может сигнализировать второму о том, что событие – заполнение буфера – наступило. Второй поток может сигнализировать первому о том, что наступило другое событие – данные из буфера считаны, ожидается новая порция данных. Событие может иметь два состояния – занятое и свободное. Работа с событиями осуществляется посредством следующих функций: CreateEvent, ResetEvent, PulseEvent, SetEvent. 5.4. Задание на лабораторную работу Вариант 1 Написать программу, порождающую четыре потока, каждому из которых выделяется четвертая часть окна приложения. Первый поток выводит в свою область возрастающую числовую последовательность 0,1,2,…, второй – последовательность чисел Фибоначчи. Третий поток заполняет свой участок окна прямоугольниками случайного размера и цвета, четвертый поток фиксирует в трех переменных и выводит их в своей области окна число запусков каждого из предыдущих трех потоков. Вариант 2 Написать программу, порождающую поток по нажатию одной из клавиш клавиатуры. Каждому созданному таким образом потоку соответствует окружность в окне приложения, которая появляется в случайном месте окна приложения и движется либо во вертикали, либо по горизонтали. При достижении границы окна, окружность меняет направление своего движения на противоположное. Вариант 3 В программе создать два потока. Назначение одного из них – периодиче- ское чтение системного времени и заполнение глобальной структуры (часы, минуты, секунды), второго – вывод данной структуры на экран. При помощи критического раздела организовать раздельный доступ потоков к структуре данных. Вариант 4 Написать программу, содержащую два потока, каждый из которых управ- ляет движением одного из двух шаров. Первый шар двигается горизонтально, второй – вертикально. Скорость шаров различна. При достижении границы клиентской области окна, шар меняет направление движения на противоположное. При помощи объектов синхронизации (семафоров или событий) реализовать алгоритм движения шаров без столкновений. Вариант 5 Написать программу, которая запускает новый поток при нажатии левой клавиши мыши. Поток начинает выводить возрастающую числовую последовательность в текущую позицию курсора мыши. При нажатии левой клавиши мыши программа удаляет поток, координаты которого ближе всего к положению мыши.

Вариант 6 Создать многопоточную программу, формирующую потоки трех типов. Каждый из потоков запускается соответствующим пунктом меню и захватывает соответственно 1,2,3 ресурса (максимальное число ресурсов по умолчанию – 8 и может меняться пользователем в окне диалога, вызываемом через меню). Количество, вид потоков, а также их состояние выводится на экран. Если число ресурсов не позволяет работать потоку, он находится в состоянии ожидания. Удаление потоков осуществляется через меню в порядке запуска (первым удаляется поток, запущенный первым). Вариант 7 Написать программу, которая позволяет запускать процессы, используя для этого выбранные на диске файлы. Пользователь может задавать имя запускаемого файла и командную строку. Программа следит за всеми запущенными ею процессами и выводит по требованию пользователя следующую информацию: имя процесса, значение указателя и идентификатора процесса, время выполнения процесса. Вариант 8 Написать программу, которая читает с диска *.bmp файл и выводит его в окно приложения. При помощи потока организовать поворот изображения на 90 градусов. Операцию можно прервать при помощи диалогового окна, возникающего на время выполнения операции. Вариант 9 Написать программу, которая по нажатию мыши создает потоки: по нажа- тию правой клавиши – поток, производящий вывод возрастающего ряда в позицию курсора, левой – поток с убывающим рядом. Поток выгружается из памяти по окончанию счета. Число потоков ограничивается пользователем через контекстное меню и находится в диапазоне [4,8]. Вариант 10 Написать три программы. Первая из них формирует метофайл, содержа- щий 10 прямоугольников, вторая – открывает созданный метофайл и дорисовывает в него 10 эллипсов. Третья программа – отображает конечный метофайл в клиентской области окна. Все три программы работают циклически, в одно и то же время может работать только одна программа. 5.5. Контрольные вопросы и задания 1. Поясните принцип многозадачности современных операционных систем. 2. Чем отличается многозадачность от многопоточности? 3. Каким образом аппаратно решаются задачи обеспечения многозадачности? 4. В чем заключаются особенности невытесняющей многозадачности? 5. Приведите примеры использования многопоточности в прикладных программах. 6. Каким образом осуществляется синхронизация потоков?

7. Многозадачность и многопоточность.

Многозадачность (англ. multitasking) — свойство операционной системы или среды выполнения обеспечивать возможность параллельной (или псевдопараллельной) обработки нескольких задач. Истинная многозадачность операционной системы возможна только в распределённых вычислительных системах.

Существует 2 типа многозадачности:

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

Многопоточность — специализированная форма многозадачности.

Свойства многозадачной среды Примитивные многозадачные среды обеспечивают чистое «разделение ресурсов», когда за каждой задачей закрепляется определённый участок памяти, и задача активизируется в строго определённые интервалы времени.

Более развитые многозадачные системы проводят распределение ресурсов динамически, когда задача стартует в памяти или покидает память в зависимости от её приоритета и от стратегии системы. Такая многозадачная среда обладает следующими особенностями:

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

Типы псевдопараллельной многозадачности

Простое переключение

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

Преимущества: можно задействовать уже работающие программы, написанные без учёта многозадачности.

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

Совместная или кооперативная многозадачность

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

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

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

Вытесняющая, или приоритетная, многозадачность (режим реального времени)

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

возможность полной реализации многозадачного ввода-вывода в ядре ОС, когда ожидание завершения ввода-вывода одной программой позволяет процессору тем временем исполнять другую программу; cильное повышение надежности системы в целом, в сочетании с использованием защиты памяти — идеал в виде «ни одна программа пользовательского режима не может нарушить работу ОС в целом» становится достижимым хотя бы теоретически, вне вытесняющей многозадачности он не достижим даже в теории. возможность полного использования многопроцессорных и многоядерных систем.

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

Конспектики

  • by kvckr
  • kvckrr@gmail.com

Что такое многозадачность и нужно ли её развивать

Объясняем, почему работа над несколькими задачами сразу — плохая идея.

Кадр: мультипликационный фильм «Аладдин» / Walt Disney Pictures

Алина Кизьякова

Алина Кизьякова

Журналист. Выросла на Камчатке и выучилась в Петербурге. Пишет про культуру, бизнес и психологию. Любит камерные театры и ситкомы.

В статье рассказываем:

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

Мы завели телеграм-канал «Ты как?». Будем в удобном формате рассказывать о саморазвитии, психологии и о том, как эффективно учиться и строить карьеру в любом возрасте. Подписывайтесь!

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

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

Психолог Анна Рознатовская говорит, что многозадачность не только увеличивает риск ошибок, но и быстрее выматывает мозг. Ведь на переключение уходят дополнительные силы. Кроме того, многозадачность мешает человеку совершать более осознанные поступки, потому что у него не остаётся времени на то, чтобы проанализировать происходящее вокруг и принять обдуманное решение.

Примеры многозадачности

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

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

Но, во-первых, существуют исследования, которые показывают, что если при выполнении задачи переключаться на другие дела минимум один раз в 23 минуты, то сделать её на 100% хорошо не удастся. А во-вторых, работа в постоянном режиме многозадачности может привести к стрессу и выгоранию сотрудника.

Чем вредна мультизадачность?

33% россиян считают, что многозадачность — это тот навык, который поможет им оставаться востребованными специалистами. Но так ли это?

Многозадачность действительно может быть полезной при выполнении рутинных дел. Но в работе и учёбе от неё лучше отказаться, потому что:

  • Снижается продуктивность. Когда человек пытается одновременно слушать лекцию и писать электронное письмо, его внимание рассеивается и продуктивность падает.
  • Повышается риск совершить ошибку, а выполнение задания занимает больше времени. Исследователи выяснили, что при переключении между делами увеличивается число ошибок, а смена задач занимает до 40% продуктивного времени.
  • Растёт уровень стресса. Например, менеджер чувствует нехватку времени, пока общается с клиентом, параллельно тестирует приложение и заполняет документацию. Много дел с пометкой «срочно» могут быстро привести к выгоранию. Чем разнороднее задачи, тем выше будет стресс и тем серьёзнее последствия.
  • Накапливается усталость. Если с утра сотрудник загружает себя несколькими проектами, пытаясь успеть везде по чуть-чуть, во второй половине дня у него может не хватить сил на оставшиеся дела.
  • Снижается качество работы. При частом переключении между задачами внимание рассеивается, поэтому результаты выходят поверхностными.

Стоит ли учиться многозадачности?

Лучше учиться не многозадачности, а планированию �� С действительно продуктивным мультитаскингом могут справиться всего 2,5% людей.

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

  • Оцените задачи и рассортируйте их на те, с которыми вы хорошо знакомы, и те, которые требуют глубокого погружения в процесс. Выстраивая план работы, чередуйте сложные и простые задачи, чтобы мозг успевал хоть немного отдыхать.
  • Сделайте инструкции и памятки для коллег, чтобы вас не отвлекали по мелочам. Например, вы занимаетесь оформлением договоров. Если к вам будут постоянно приходить коллеги и спрашивать, какие документы нужны и в какой срок вы подготовите договор, то времени на само оформление у вас может не остаться. Сделайте файл с часто задаваемыми вопросами и отправляйте его при первом разговоре с коллегой. Вы удивитесь, насколько спокойней станет ваш рабочий день.
  • Составьте график периодов, когда может потребоваться многозадачность. Если вы можете спрогнозировать, когда работы будет особенно много, то сможете разгрузить другие сферы жизни. Например, в период отчётности не планируйте ничего, требующего вашего пристального внимания дома. Можно сойти с ума, если разрываться на работе днём, а по вечерам делать ремонт в квартире или писать диплом.
  • Не геройствуйте и не набирайте задач, если чувствуете, что вам сложно. Иногда многозадачность — это наша инициатива, а не обязанность. Если вас нагружают работой, а вы считаете, что ваш отказ создаст плохое впечатление о вас, то прекратите так думать и встаньте на свою сторону. Ваше здоровье и спокойствие — это самое главное.
  • Делегируйте и не стесняйтесь просить помощи. Если никто не может вам помочь с работой, то попросите помощи у семьи с домашними делами, чтобы вы могли полноценно отдыхать хотя бы по вечерам и на выходных.
  • Ведите календарь или список дел по часам. Причём записывайте в него не только рабочие дела, но и личные. Так вы поймёте, что в вашем дне всего 24 часа, а маховик времени, как у Гермионы Грейнджер, ещё не изобрели ��
  • Не жалейте денег и времени на отдых. Тот, кто хорошо работает, должен так же хорошо отдыхать. Если у вас была напряжённая неделя, запланируйте себе шикарный мини-отпуск с походом в спа-салон и загородной поездкой. В конце концов, ради чего вы так усердно трудитесь?

Как отучить себя от многозадачности

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

  • Пишите план на день. Определите, какие задачи важнее, сконцентрируйтесь в первую очередь на них. Установите время, когда вы разбираете почту, когда отвечаете на звонки и проводите встречи, когда работаете над другими задачами. Если нужно — отводите на решение каких-то вопросов целый день и не отвлекайтесь на другие дела. Программист Станислав Петросян рассказывает, что работает именно так: полностью расписывает рабочий день, иначе сосредоточиться на написании кода просто не получится.
  • Выделите время на задачу. Чёткий дедлайн поможет сконцентрироваться и не прокрастинировать.
  • Используйте методы управления временем. Например, технику Pomodoro: включите таймер на 25 минут и сконцентрируйтесь на работе. Когда время закончится, сделайте перерыв на 5–10 минут. После четырёх-пяти таких циклов можно прерваться на полноценный обед. У нас есть статья «15 популярных техник тайм-менеджмента» — прочитайте её и выберите технику, которая понравится вам больше всего ��
  • Не отвлекайтесь. Выключите уведомления от мессенджеров. Часто нас отвлекают от дел неважные сообщения: кто-то поставил лайк на фотографии или вышел новый выпуск любимого шоу на YouTube. Выделите время на сёрфинг в интернете, не позволяйте спаму отвлекать вас.

Автор книги «Однозадачность. Успевайте больше, фокусируясь только на одном деле» Девора Зак советует развивать осознанность — навык, позволяющий сосредотачиваться на своих мыслях и чувствах.

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

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

Читайте также:

  • Осознанность: что такое, практики и развитие
  • 6 способов повысить концентрацию
  • Клиповое мышление — что это такое и как от него избавиться.

Многозадачность и многопоточность — распространенные заблуждения и недопонимания

Когда я предложил перевести на русский мою последнюю статью Easy Concurrency with Python Shared Objects на английском, поступило предложение «написать в несколько раз короче и понятнее». Просьба более чем обоснована. Поскольку я уже порядка десяти лет пишу многопоточку и БД, то описываемые мной логические связи выглядели самоочевидно, и я ошибочно расчитывал на аудиторию из трех с половиной человек, которые сидят сейчас где-то в яндексе или гугле. Судя по всему, они там и сидят, но тема им не интересна, поскольку в питоне нет настоящих потоков, а значит для этих людей такого языка программирования не существует. Потому я немножко снижаю планку и делаю общий обзор проблематики параллельных вычислений для людей, которые в них разбираются, но не являются экспертами в области.

Из-за чего весь сыр-бор?

a := 1 b := a + 1 print(a) print(b)
Цикл

Два процесса выполняются параллельно и независимо. Если мы возьмем первую инструкцию процесса 1, то параллельно с ней может выполниться любая из четырех команд. Вторая команда процесса 1 аналогично не ограничивает выполнение в процессе 2, потому она может выполняться параллельно с любой из четырех команд. Для простоты допускаем, что одна команда атомарна. Всего число возможных сценариев выполнения первого процесса 4^4 = 256. Если в аналогичных первом и втором процессах по десять инструкций, то число различных вариантов выполнения равно 10 10 , то есть, 10 миллиардов. За пару минут мы можем написать 10 миллиардов программ! Вау, мы крутые! А если серьезно, то нам не хватит никакого времени, чтобы отладить все эти 10 миллиардов сценариев выполнения.

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

Что там сложного? Есть конкуренция за ресурс — повесь мьютекс. Я так всегда делаю.

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

А еще лучше вообще иметь только одну блокировку на все вилки, ложки, философов, и стол одновременно — создатели питона примерно так и подумали.

Если же вы решили использовать множественные блокировки, то неявные и допускающие вложенность блокировки (Rust std::sync::Mutex или Java synchronized) — путь в никуда, поскольку становится очень легко создать неочевидно дедлочащуюся программу. На самом деле, даже без дедлоков детальные блокировки не являются панацеей, поскольку простые блокировки, они же «семафоры Дейкстры», ограничены в своих способностях:

Хотя задачу курильщиков и возможно решить семафорами Дейкстры, такие «семафоры курильщика» дают сложный в понимании и поддержке алгоритм, при том, что задача, казалось бы, элементарна. Потому вторым по популярности механизмом блокировок стали условные переменные и более общий аспект оных — мониторы (в том числе уже упомянутое Java synchronized). Правда, проблемы дедлоков при множественных блокировках они не решают.

Что же делать? Мы все умрем? Да, но проблему дедлоков можно решить. Одно из остроумных решений для задачи обедающих философов предложил сам Дейкстра — брать детальные блокировки только в одном заранее заданном порядке. Следующий забавный прием — брать блокировки всем скопом одной командой либо отменять взятие блокировки. Такое можно реализовать на любой абстракции с примитивом try-lock (например, compare-and-swap), при помощи бесконечного цикла (например, std::lock из библиотеки C++).

Одна из самых последних и перспективных альтернатив блокировкам — это транзакционная память, по сути атомарный compare-and-swap на большом числе ячеек. Идею транзакционной памяти развил в конкретную программную реализацию, STM, мой любимый автор книг и статей по многопоточности — Нир Шавит. Главное преимущество STM — отсутствие дедлоков. В каком-то смысле транзакционная память достаточно стара, если вспомнить, что реляционные СУБД давно умели в транзакции. Программная транзакционная память, как правило, берет блокировки всем скопом, как в описанном выше алгоритме предварительной блокировки, но делает это не до выполнения операций изменения ячеек памяти, а после — таким образом нам не обязательно до начала работы алгоритма знать список нужных нам блокировок и длительность этих блокировок минимальна. Из популярных готовых решений на эту тему можно вспомнить Clojure и GHC.

У наивной реализации STM есть проблема — при интенсивных конфликтах меж потоками приложение большую часть времени крутит откаты транзакций вхолостую: подробнее.

Решение этой проблемы уже придумано за нас — это алгоритм раннего разрешения конфликтов, при котором реализация не ждет окончания транзакции для того, чтобы осознать невозможность применения ее результатов, а сразу прерывает одну из конфликтующих транзакций. По сути это способ организации детальных блокировок с разрешением дедлоков через откат изменений. Такой подход я и перенял при реализации Python Shared Object.

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

Разделяемая память не нужна! Только акторная модель и сообщения (Erlang, Tcl)

Подход, известный также как «у меня болит палец — отрежу себе руку!».

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

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

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

Асинхронная передача сообщений сама по себе не способна обеспечить строгую согласованность состояния/данных на разрозненных узлах. Если быть точным, то в асинхронной системе с отказами узла (или непредсказуемыми задержками ответа) невозможно детерминированное достижение консенсуса (достижение единого согласованного решения между всеми узлами) в соответствии с FLP-недостижимостью, а также производной теоремой CAP. В случае, если мы имеем некую гарантию времени ответа от узла и отсутствие отказов, мы можем достигнуть некоего условного консенсуса за время порядка нескольких круговых задержек передачи сообщения (то есть, периода выполнения запроса и обратного ответа).

Недетерминированность достижения консенсуса в асинхронной системе с отказами значит, что прийти к единому решению в такой системе — это рулетка, полнейшая случайность, хотя шанс «выиграть» в ней и повышается с течением времени, но он никогда не может быть меньше двух круговых задержек сети. По этой причине в реальности передача сообщений не так уж прозрачна и не так бесплатна при увеличении дистанции, например, при переходе от передачи сообщений между потоками одного процесса ОС к передаче сообщений между удаленными датацентрами. В случае большой круговой задержки протоколы организации распределенных БД, как то Raft, Zab, MultiPaxos, минимизируют проблемы координации, ограничивая ее выборами лидера, и применяют случайную задержку при выборах лидера. Аналогично Ethernet использует случайную задержку при конфликте использования канала.

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

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

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

YandexDB, она же Calvin, использует Zookeeper для организации глобально согласованной истории изменений. Сам Zookeeper использует асинхронные сообщения только для выбора лидера — работа с данными происходит синхронно на выбранном эталонном узле (лидере). Создатели экспериментальной СУБД Tango заявляют, что реализовывает ZooKeeper в 300 строчек кода. Однако, сам оригинальный Tango использует нереплицируемый счетчик в качестве опорного средства координации, потому не обеспечивает гарантий отказоустойчивости ZooKeeper. И по итогу для организации сравнимых гарантий в этой «реализации ZooKeeper» на Tango вам нужно реализовать координацию Tango на… ZooKeeper. Это похоже на игру «горячий пирожок».

Почему в этот горячий пирожок играют? Потому что идеальное решение невозможно чисто теоретически (FLP-недостижимость) — все практически реализации являются тем или иным компромиссом. Но есть желающие «идеальное решение» купить, потому используется популярный маркетинговый прием: недостатки существующих решений уже хорошо известны, а недостатки нашей новой системы еще не известны… значит можно сказать, что их нет. Так и развиваются многие Big Data проекты.

Когда же у вас есть хотя бы общий атомарный счетчик, а еще лучше — общий атомарный список транзакций, дальше вы можете наращивать объем «нагрузочных данных» как угодно, реплицировать их в eventual consistency хранилищах, придавать им произвольную форму (как это сделало в том же Tango, давшем пользователю свободу выбора формы данных) — это всё имеет второстепенное значение и легко меняется, хотя многие люди по прежнему обращают внимание на обёртку, а не на суть. То есть, ходовые качества машины определяются двигателем, трансмиссией, рулевой системой, но большинство видит лишь красивый кузов и отделку салона, к сожалению. Я хочу здесь подчеркнуть, что ZooKeeper в случае Tango, YandexDB, Calvin, ClickHouse и прочих аналогичных проектов — это не «просто вспомогательная штука», как его рисуют, а ключевой компонент и большая часть сложности реализации всей СУБД, и этот компонент, к тому же, полностью определяет число транзакций в секунду, которые сможет обработать весь кластер.

Проблем неопределенности в том числе избегает реализация синхронной передачи сообщений, иначе известная как «взаимодействующие последовательные процессы» (Communicating sequential processes, CSP), которые являются давно известным подходом. В частности, этот подход выбрал в качестве своего фундамента популярный язык Go и сильно менее популярные предшественники оного, языки Limbo и Newsqueak. Атомарная операция отправки сообщения через канал гарантирует, что либо сообщение отправлено и отправка одновременно подтверждена, либо оно не отправлено — для обеспечения этой синхронизации используется разделяемое состояние канала скрытое от программиста на Go.

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

Золотая середина

Асинхронные алгоритмы и синхронные алгоритмы, передача сообщений и разделяемое состояние дополняют друг друга. Проблемы возникают только когда одним из подходов злоупотребляют, а второй — игнорируют. Невозможность организации простого эффективного разделяемого состояния на питоне привела к появлению массы многочисленных «костылей» — это и простые РСУБД, как то PostgreSQL и MySQL, и NoSQL вроде Redis/MemCached, и очереди сообщений (RabbitMQ). Примерно на этой волне я и решил написать Python Shared Objects. В упомянутом Erlang уже давно есть специальный модуль для организации разделяемой памяти: подробнее.

Также, сама реализация Erlang/OTP неявно использует разделяемую память. Даже если из питона получится жалкое подобие Erlang — это все равно намного лучше, чем тот безнадежно однозадачный (не путать с зелеными потоками/concurrent IO) интерпретатор питона, который мы имеем сейчас.

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

Например, оригинальная STM от Нир Шавита использует глобальный атомарный счетчик для координации всех задача, но в остальном «общими» ячейки данных становятся только когда они по-настоящему общие для нескольких задач. Аналогичный подход с общим атомарным счетчиком номера/приоритета транзакции и детальными блокировками также применяет моя реализация STM в виде Python Shared Objects.

Тесты, еще тесты, нужно больше тестов

Как же тестировать и отлаживать многозадачные и распределенные системы? Основная проблема многопоточных, многозадачных, распределенных систем — это недетерминированность их поведения. То есть, ваша система может работать пять лет, а потом резко начать падать из-за какого-то незначительного изменения алгоритма.

В общем случае ответ на вопрос «как тестировать?» — «никак», вы подходите к проблемы с неправильной стороны. Но не расстраивайтесь — это нормально, так делает много кто. По этой причине, например, большинство распределенных СУБД, заявлявших про строгую согласованность данных и отказоустойчивость, в жестких тестах на Jepsen показывают, что на самом деле никогда не обеспечивали как минимум одну из этих гарантий: подробнее.

Тестирование — это способ обнаружить наличие проблемы, но тестированием невозможно доказать отсутствие проблем. Если в качестве отказоустойчивого хранилища согласованных данных вы используете не ZooKeeper и не Etcd, то с большой вероятностью ваша система потеряет/испортит данные или вовсе упадет при потере лидера. Если вы уделите пару часов чтению серии статей по ссылке, то вы узнаете, что какой-нибудь Galera Cluster способен нарушать согласованность даже на полностью исправном кластере, а MongoDB не дает даже «eventual consistency» гарантий при отказах (то есть, в MongoDB ваши данные из подтвержденных транзакций может быть сохранятся, а может быть не сохранятся). Однако, даже если вы построили систему на базе условно надежных ZooKeeper или Etcd, вы не знаете, сколько еще проблем возникло из-за некорректного использования оных в самописном коде.

Единственный более-менее гарантированный способ построить работающую систему — это прежде всего грамотно подойти к ее проектированию и реализации, доказать корректность алгоритма, а не полагаться на популярный нынче метод тестирования и «авось». Вторая линия обороны — это тестирование, но не простое. Разработчики должны понимать, что если ошибка есть, то ее необходимо обнаружить, а не замести под ковер и подпереть костылем. То есть, без участия и сотрудничества самих разработчиков невозможно эффективно произвести тестирование. Необходимо тестировать систему в самых неудобных и опасных режимах, предусмотреть в исходном коде отладочные опции для более частого срабатывания редких фрагментов кода — в том числе такой подход я задействовал для своего проекта Python Shared Objects. В случае отказоустойчивых кластеров обязательно необходимо периодически симулировать отказы, как это делает, например, Яндекс.

Поскольку очень часто проблема происходит только один раз и больше никогда не повторяется, то большую ценность имеет логирование, создание снимков состояния системы во время проблемы. Для последнего, например, есть замечательная утилита rr от Mozilla, которая была разработана специально для отладки многопоточного кода в Firefox. Эта утилита позволяет выполнить код в обратном направлении, точно восстанавливая последовательность выполнения асинхронных потоков, приведшую к ошибке. И хотя облака нынче стали популярным хайпом, выбор инструментов для отладки многозадачного кода и распределенных систем на удивление скудный. Для распределенных систем есть упомянутый Jepsen, для многопоточных есть, например, ThreadSanitizer.

К сожалению, для тестирования моего Python Shared Objects мне так и не удалось найти готового решения для неинтрузивного логирования. Неинтрузивного — поскольку для многопоточного кода имеет место так «эффект наблюдателя» — когда при логировании программа ведет себя совсем не так, как без него. Когда-то давно для закрытой разработки я сам реализовывал неинтрузивное логирование на Delphi, но это было давно и неправда, а на сишные программы этот код довольно плохо налазит.

Коротко: при создании сложных многопоточных, многозадачных, и распределенных систем грамотный подход и такие люди, как я, с большим трудом заменяются тестами и «большой дружной командой».

  • Python
  • multithreading
  • multitasking
  • distributed computing
  • многопоточность
  • многозадачность
  • распределённые вычисления

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

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