В информатике синхронизация — это задача координации множества процессов для объединения или установления связи в определенный момент с целью достижения соглашения или выполнения определенной последовательности действий.
Потребность в синхронизации возникает не только в многопроцессорных системах, но и в любых параллельных процессах; даже в однопроцессорных системах. Ниже упомянуты некоторые основные потребности в синхронизации:
Разветвления и объединения . Когда задание достигает точки разветвления, оно разбивается на N подзаданий, которые затем обслуживаются n задачами. После обслуживания каждое подзадание ожидает завершения обработки всех остальных подзаданий. Затем они снова присоединяются и покидают систему. Таким образом, параллельное программирование требует синхронизации, поскольку все параллельные процессы ожидают выполнения нескольких других процессов.
Производитель-потребитель. В отношениях производитель-потребитель процесс-потребитель зависит от процесса-производителя до тех пор, пока не будут получены необходимые данные.
Эксклюзивное использование ресурсов: когда несколько процессов зависят от ресурса и им необходимо получить к нему доступ одновременно, операционная система должна гарантировать, что только один процессор обращается к нему в данный момент времени. Это уменьшает параллелизм.
Синхронизация потоков определяется как механизм, который гарантирует, что два или более параллельных процесса или потока не выполняют одновременно какой-либо конкретный сегмент программы, известный как критический раздел . Доступ процессов к критической секции контролируется с помощью методов синхронизации. Когда один поток начинает выполнение критического раздела (сериализованного сегмента программы), другой поток должен дождаться завершения первого потока. Если не применяются надлежащие методы синхронизации [1] , это может вызвать состояние гонки , когда значения переменных могут быть непредсказуемыми и варьироваться в зависимости от времени переключения контекста процессов или потоков.
Например, предположим, что есть три процесса, а именно 1, 2 и 3. Все три из них выполняются одновременно, и им необходимо использовать общий ресурс (критическую секцию), как показано на рисунке 1. Здесь следует использовать синхронизацию, чтобы избегайте любых конфликтов при доступе к этому общему ресурсу. Следовательно, когда процесс 1 и 2 пытаются получить доступ к этому ресурсу, его следует назначить только одному процессу одновременно. Если он назначен Процессу 1, другому процессу (Процессу 2) придется подождать, пока Процесс 1 освободит этот ресурс (как показано на рисунке 2).
Еще одно требование синхронизации, которое необходимо учитывать, — это порядок, в котором должны выполняться определенные процессы или потоки. Например, нельзя сесть в самолет до покупки билета. Аналогично, нельзя проверять электронную почту до проверки соответствующих учетных данных (например, имени пользователя и пароля). Точно так же банкомат не будет предоставлять никаких услуг, пока не получит правильный PIN-код.
Помимо взаимного исключения, синхронизация также касается следующего:
Одной из задач разработки экзафлопсных алгоритмов является минимизация или уменьшение синхронизации. Синхронизация занимает больше времени, чем вычисления, особенно в распределенных вычислениях. Уменьшение синхронизации привлекало внимание ученых-компьютерщиков на протяжении десятилетий. Принимая во внимание, что в последнее время это становится все более серьезной проблемой, поскольку разрыв между улучшением вычислений и задержкой увеличивается. Эксперименты показали, что (глобальная) связь благодаря синхронизации на распределенных компьютерах занимает доминирующую долю в разреженном итеративном решателе. [2] Этой проблеме уделяется все больше внимания после появления новой эталонной метрики, сопряженного градиента высокой производительности (HPCG), [3] для ранжирования 500 лучших суперкомпьютеров.
Ниже приведены некоторые классические проблемы синхронизации:
Эти задачи используются для проверки почти каждой вновь предложенной схемы или примитива синхронизации.
Многие системы обеспечивают аппаратную поддержку кода критической секции .
Однопроцессорная или однопроцессорная система может отключить прерывания , выполняя текущий код без вытеснения , что очень неэффективно для многопроцессорных систем. [4] «Ключевой способностью, которая нам необходима для реализации синхронизации в мультипроцессоре, является набор аппаратных примитивов с возможностью атомарного чтения и изменения ячейки памяти. Без такой возможности стоимость создания базовых примитивов синхронизации будет слишком высокой и будет увеличиваться по мере увеличения количества процессоров.Существует ряд альтернативных формулировок основных аппаратных примитивов, каждый из которых обеспечивает возможность атомарного чтения и изменения местоположения, а также некоторый способ определить, выполнялись ли чтение и запись атомарно. Эти аппаратные примитивы являются основными строительными блоками, которые используются для создания широкого спектра операций синхронизации на уровне пользователя, включая такие вещи, как блокировки и барьеры . В общем, архитекторы не ожидают, что пользователи будут использовать базовые аппаратные примитивы, а вместо этого ожидают, что примитивы будут использоваться системными программистами для создания библиотеки синхронизации, а этот процесс часто бывает сложным и непростым». [5] Многие современные аппаратные средства предоставляют такие атомарные инструкции, двумя распространенными примерами являются: test-and-set , которая работает с одним словом памяти, и Compare-and-swap , которая меняет местами содержимое двух слов памяти.
В Java одним из способов предотвращения помех потоков и ошибок согласованности памяти является добавление к сигнатуре метода ключевого слова Synchronized ; в этом случае для обеспечения синхронизации используется блокировка объявляющего объекта. Второй способ — поместить блок кода в раздел Synchronized(someObject){...} , который обеспечивает более детальное управление. Это заставляет любой поток получить блокировку someObject , прежде чем он сможет выполнить содержащийся блок. Блокировка автоматически снимается, когда поток, получивший блокировку, покидает этот блок или переходит в состояние ожидания внутри блока. Любые обновления переменных, выполненные потоком в синхронизированном блоке, становятся видимыми для других потоков, когда они аналогичным образом получают блокировку и выполняют блок. В любой реализации любой объект может использоваться для обеспечения блокировки, поскольку все объекты Java имеют встроенную блокировку или блокировку монитора , связанную с ними при создании экземпляра. [6]
Синхронизированные блоки Java , помимо обеспечения взаимного исключения и согласованности памяти, позволяют передавать сигналы, то есть отправлять события из потоков, которые получили блокировку и выполняют блок кода, тем, которые ожидают блокировки внутри блока. Таким образом, синхронизированные разделы Java сочетают в себе функциональность мьютексов и событий для обеспечения синхронизации. Такая конструкция известна как монитор синхронизации .
.NET Framework также использует примитивы синхронизации. [7] «Синхронизация спроектирована так, чтобы быть совместной, требуя, чтобы каждый поток следовал механизму синхронизации перед доступом к защищенным ресурсам для получения согласованных результатов. Блокировка, сигнализация, облегченные типы синхронизации, операции ожидания и блокировки — это механизмы, связанные с синхронизацией в .NET». [8]
Многие языки программирования поддерживают синхронизацию, и для разработки встроенных приложений написаны целые специализированные языки , где строго детерминированная синхронизация имеет первостепенное значение.
Другой эффективный способ реализации синхронизации — использование спин-блокировок. Прежде чем получить доступ к любому общему ресурсу или фрагменту кода, каждый процессор проверяет флаг. Если флаг сброшен, процессор устанавливает его и продолжает выполнение потока. Но если флаг установлен (заблокирован), потоки будут продолжать вращаться в цикле и проверять, установлен ли флаг или нет. Но спин-блокировки эффективны только в том случае, если флаг сброшен для младших циклов, в противном случае это может привести к проблемам с производительностью, поскольку на ожидание тратится много процессорных циклов. [9]
Барьеры просты в реализации и обеспечивают хорошую оперативность. Они основаны на концепции реализации циклов ожидания для обеспечения синхронизации. Предположим, что три потока выполняются одновременно, начиная с барьера 1. По истечении времени t поток 1 достигает барьера 2, но ему все равно приходится ждать, пока потоки 2 и 3 достигнут барьера 2, поскольку у него нет правильных данных. Как только все потоки достигают барьера 2, все они начинаются заново. По истечении времени t поток 1 достигает барьера 3, но ему придется снова ждать потоков 2 и 3 и правильных данных.
Таким образом, при барьерной синхронизации нескольких потоков всегда будет несколько потоков, которые в конечном итоге будут ждать других потоков, как в приведенном выше примере поток 1 продолжает ожидать потоки 2 и 3. Это приводит к серьезному снижению производительности процесса. [10]
Функцию ожидания барьерной синхронизации для i- го потока можно представить как:
(Wбарьер)i=f ((Tбарьер)i, (Rthread)i)
Где Wbarrier — время ожидания потока, Tbarrier — количество прибывших потоков, а Rthread — скорость прибытия потоков. [11]
Эксперименты показывают, что 34% общего времени выполнения тратится на ожидание других, более медленных потоков. [10]
Семафоры — это механизмы сигнализации, которые могут позволить одному или нескольким потокам/процессорам получить доступ к разделу. Семафор имеет флаг, с которым связано определенное фиксированное значение, и каждый раз, когда поток желает получить доступ к разделу, он уменьшает этот флаг. Аналогично, когда поток покидает раздел, флаг увеличивается. Если флаг равен нулю, поток не может получить доступ к разделу и блокируется, если решит подождать.
Некоторые семафоры допускают только один поток или процесс в разделе кода. Такие семафоры называются бинарными семафорами и очень похожи на мьютексы. Здесь, если значение семафора равно 1, потоку разрешен доступ, а если значение равно 0, доступ запрещен. [12]
Синхронизация изначально представляла собой концепцию, основанную на процессах, посредством которой можно было получить блокировку объекта. Его основное использование было в базах данных. Существует два типа блокировки (файлов) ; только чтение и чтение-запись. Блокировки только для чтения могут быть получены многими процессами или потоками. Блокировки чтения-записи являются эксклюзивными, поскольку они могут использоваться только одним процессом/потоком одновременно.
Хотя блокировки были созданы для файловых баз данных, данные также совместно используются в памяти между процессами и потоками. Иногда одновременно блокируется более одного объекта (или файла). Если они не заблокированы одновременно, они могут перекрываться, вызывая исключение взаимоблокировки.
Java и Ada имеют только монопольные блокировки, поскольку они основаны на потоках и полагаются на инструкции процессора сравнения и замены .
Абстрактную математическую основу примитивов синхронизации дает моноид истории . Существует также множество теоретических устройств более высокого уровня, таких как исчисление процессов и сети Петри , которые можно построить поверх моноида истории.
Ниже приведены некоторые примеры синхронизации для разных платформ. [13]
Windows обеспечивает:
Linux обеспечивает:
Включение и отключение вытеснения ядра заменило спин-блокировки в однопроцессорных системах. До версии ядра 2.6 Linux отключал прерывания для реализации коротких критических участков. Начиная с версии 2.6 и более поздних версий Linux является полностью вытесняющим.
Солярис обеспечивает:
Pthreads — это независимый от платформы API , который обеспечивает:
{{cite journal}}
: CS1 maint: несколько имен: список авторов ( ссылка ){{cite book}}
: CS1 maint: несколько имен: список авторов ( ссылка )