stringtranslate.com

Безопасность потока

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

Существуют различные стратегии создания потокобезопасных структур данных. [3]

Уровни безопасности потока

Разные поставщики используют немного различную терминологию для потокобезопасности, [4] но наиболее часто используемая терминология потокобезопасности следующая: [2]

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

Библиотеки программного обеспечения могут предоставлять определенные гарантии потокобезопасности. [5] Например, параллельные чтения могут быть гарантированно потокобезопасными, но параллельные записи — нет. Является ли программа, использующая такую ​​библиотеку, потокобезопасной, зависит от того, использует ли она библиотеку способом, соответствующим этим гарантиям.

Подходы к реализации

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

Первый класс подходов направлен на избежание общего состояния и включает в себя:

Повторный вход [6]
Написание кода таким образом, чтобы он мог быть частично выполнен потоком, выполнен тем же потоком или одновременно выполнен другим потоком и при этом правильно завершить исходное выполнение. Это требует сохранения информации о состоянии в переменных, локальных для каждого выполнения, обычно в стеке, а не в статических или глобальных переменных или другом нелокальном состоянии. Все нелокальные состояния должны быть доступны через атомарные операции, а структуры данных также должны быть реентерабельными.
Локальное хранилище потока
Переменные локализованы так, что каждый поток имеет свою собственную копию. Эти переменные сохраняют свои значения в пределах подпрограммы и других границ кода и являются потокобезопасными, поскольку они локальны для каждого потока, даже если код, который обращается к ним, может одновременно выполняться другим потоком.
Неизменяемые объекты
Состояние объекта не может быть изменено после создания. Это подразумевает, что совместно используются только данные, доступные только для чтения, и что достигается внутренняя безопасность потока. Изменяемые (неконстантные) операции могут быть реализованы таким образом, что они создают новые объекты, а не изменяют существующие. Этот подход характерен для функционального программирования и также используется реализациями строк в Java, C# и Python. (См. Неизменяемый объект .)


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

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

Примеры

В следующем фрагменте кода Java ключевое слово Java synchronized делает метод потокобезопасным:

класс  Счетчик { частный int i = 0 ;       публичный синхронизированный void inc () { i ++ ; } }      

В языке программирования C каждый поток имеет свой собственный стек. Однако статическая переменная не хранится в стеке; все потоки имеют к ней одновременный доступ. Если несколько потоков перекрываются при выполнении одной и той же функции, возможно, что статическая переменная может быть изменена одним потоком, пока другой находится на полпути к ее проверке. Эта трудно диагностируемая логическая ошибка , которая может компилироваться и работать правильно большую часть времени, называется состоянием гонки . Один из распространенных способов избежать этого — использовать другую общую переменную в качестве «блокировки» или «мьютекса» (от mut ual ex clusion).

В следующем фрагменте кода на языке C функция потокобезопасна, но не реентерабельна:

# включить <pthread.h>  int increment_counter () { static int counter = 0 ; static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER ;             // разрешить увеличивать только один поток за раз pthread_mutex_lock ( & mutex );  ++ счетчик ; // сохраняем значение до того, как любые другие потоки увеличат его еще больше int result = counter ;     pthread_mutex_unlock ( & ​​мьютекс ); вернуть результат ; } 

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

Одну и ту же функцию можно реализовать как потокобезопасную и реентерабельную, используя атомарные методы без блокировок в C++11 :

# включить <атомарный>  int increment_counter () { static std :: atomic < int > counter ( 0 );      // приращение гарантированно выполняется атомарно int result = ++ counter ;     вернуть результат ; } 

Смотрите также

Ссылки

  1. ^ Керриск, Майкл (2010). Интерфейс программирования Linux . No Starch Press . стр. 699, «Глава 31: ПОТОКИ: БЕЗОПАСНОСТЬ ПОТОКОВ И ХРАНЕНИЕ ПО ПОТОКУ»{{cite book}}: CS1 maint: postscript (link)
  2. ^ ab Oracle (2010-11-01). "Oracle: Потокобезопасность". Docs.oracle.com . Получено 2013-10-16 "Процедура является потокобезопасной, если она логически корректна при одновременном выполнении несколькими потоками"; "3 уровень потокобезопасности"{{cite web}}: CS1 maint: postscript (link)
  3. ^ ab Oracle (ноябрь 2020 г.). "Руководство по многопоточному программированию: Глава 7. Безопасные и небезопасные интерфейсы". Docs Oracle . Получено 2024-04-30 ; "Безопасность потоков"{{cite web}}: CS1 maint: postscript (link)
  4. ^ "Классификации безопасности потоков API". IBM. 2023-04-11 . Получено 2023-10-09 .
  5. ^ "Уровни безопасности MT для библиотек". Docs Oracle . Получено 2024-05-17 .
  6. ^ "Повторный вход и потокобезопасность | Qt 5.6". Проект Qt . Получено 20.04.2016 .

Внешние ссылки