Безопасная синхронизация потоков без динамического выделения памяти

Заключение

Автор считает новым описание множества периодов периодической функции с учётом вырожденного случая периодичности - постоянства функции. Ранее в работах по этой тематике [7] рассматривались периодические функции, имеющие множеством периодов n -мерные решётки, где n - размерность вектора независимых переменных. Также новым, по мнению авторов, являются результаты исследования соотношения между периодичностью функции нескольких переменных в смысле определения 1 и её периодичности по отдельным переменным. Представленная работа является расширением и продолжением исследований, проведенных в [10, 11]. В настоящее время изучается проблема периодичности суммы и произведения периодических функций нескольких переменных. Для периодических функций одной переменной разные аспекты этой проблемы изучены в работах [5, 6].

Литература

1. Ашкрофт Н., Мермин Н. Физика твёрдого тела. - М.: Мир, 1979. Т. 1. 400 с.

2. Ахиезер Н.И. Элементы теории эллиптических функций. - М.: Наука, 1970. - 304 с.

3. Будак Б.М. Фомин С.В. Курс высшей математики и математической физики. Кратные интегралы и ряды. - М.: Наука, 1965. 608 с.

4. Negishi T. On periodic decomposition of entire functions of several variables // Aequationes Math. 2014. Vol. SS. № 1-2. P. 1-34.

5. Mirotin A.R., Mirotin E.A. On Sums and Products of Periodic Functions // Real Anal. Exchange. 2008. Vol. 34. № 2. P. 347-358.

6. Эвнин А.Ю. Период суммы двух периодических функций // Вестн. Южно-Ур. гос. унта. Сер. Математика. Физика. Химия. 2005. № 2. С. 56-61.

7. Скриганов М.М. Геометрические и арифметические методы в спектральной теории многомерных периодических операторов // Труды МИАН СССР. 1985. Т. 171. С. 3-122.

8. Седлецкий А.М. Разложение аналитической функции на сумму периодических // Изв. АН СССР. Сер. матем. 1984. Т. 48. № 4. C. S33-853.

9. Fine N.J., Wilf H.S. Uniqueness Theorems for Periodic Functions // Proc. Amer. Math. Soc. -1965. Vol. 16. № 1. P. 109-114.

10.Соколова Г.К., Орлов С.С. Об основном периоде периодической функции нескольких переменных // Материалы междунар. конф. «Воронежская зимняя математическая школа С. Г. Крейна - 2018». - Воронеж: ИПЦ «Научная книга», 2018. С. 312-315.

11.Соколова Г.К., Орлов С.С. Об основных периодах периодической функции нескольких переменных // Материалы 19-й междунар. Саратовской зимн. шк. «Современные проблемы теории функций и их приложения». - Саратов: Научная книга, 2018. С. 294-297.

Сведения об авторе

Галина Константиновна Соколова

студент

Иркутский государственный университет Эл. почта: 98gal@mail.ru Россия, Иркутск

About the author

Galina Konstantinovna Sokolova

student

Irkutsk State University E-mail: 98gal@mail.ru Russia, Irkutsk

УДК 004.451.23 Т.И. Вишневская, Ю.И. Килочек

МГТУ им. Н.Э. Баумана

БЕЗОПАСНАЯ СИНХРОНИЗАЦИЯ ПОТОКОВ БЕЗ ДИНАМИЧЕСКОГО ВЫДЕЛЕНИЯ ПАМЯТИ

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

Ключевые слова: многопоточность, примитивы синхронизации, future и promise, динамическое выделение памяти, производительность.

T.I. Vishnevskaya, Yu.I. Kilochek

Bauman Moscow State Technical University

ROBUST THREAD SYNCHRONIZATION WITHOUT DYNAMIC MEMORY ALLOCATION

This paper introduces a novel design for future and promise style thread synchronization, which doesn't require dynamic memory allocation. We benchmarked our implementation alongside existing mature implementations that follow traditional approach. Compared to those, ours demonstrates significant performance improvements.

Keywords: multithreading, synchronization primitives, future and promise, dynamic memory allocation, performance

Введение

Экспоненциальный рост количества транзисторов в процессорах, явление, известное как «Закон Мура» [1], завершается в связи с приближением к фундаментальным физическим ограничениям [2,3]. Если не произойдет революционной смены технологической базы, основным направлением повышения производительности процессоров будет использование параллелизма на всех уровнях.

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

Термин promise введен Дэниэлом Полом Фридманом (Daniel Paul Friedman) и Дэвидом Вайзом (David Wise) в 1976 году [4]. Схожая сущность под названием future была описана Генри Бейкером (Henry Baker) и Карлом Хьюиттом (Carl Hewitt) в 1977 году [5]. Концепция изначально зародилась в функциональном программировании, и её назначение заключается в отделении некоторого значения от того, как это значение было вычислено. В частности, вычисление этого значения может быть прозрачно распараллелено.

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

Гибкость, универсальность и простота использования способствовали тому, что future и promise нашли широкое применение в различных областях, от веб-приложений [6] до критических систем управления и жизнеобеспечения [7]. Однако эти достоинства даются ценой относительно низкой производительности. Анализ существующих реализаций future и promise в различных языках программирования показал, что все они устроены схожим образом. В частности, все они используют динамическое выделение памяти для хранения внутреннего состояния. В данной работе, предлагается метод реализации, лишенный этого недостатка за счет более сложного алгоритма внутренней синхронизации.

Анализ существующих реализаций для синхронизации потоков

В ходе данной работы были проанализированы пять реализаций future и promise в трёх популярных языках программирования:

1. java.util.concurrent.Future и java.util.concurrent.CompletableFuture (Java 8, OpenJDK 10) 1 используют атомарную операцию типа compare-and-swap (CAS) для атомарной установки поля результата. Дополнительно организована блокировка потоков, которые пытаются получить еще не готовый результат, во избежание продолжительного активного ожидания. Так как

1 https://github.com/dmlloyd/openjdk/blob/jdk/jdk10/src/java.base/share/classes/ java/util/concurrent/Com-pletableFuture.java

java.util.concurrent.Future является лишь интерфейсом, вся логика целиком реализована в классе j ava.util. concurrent.CompletableFuture.

2. System.Threading.Tasks.Task и System. Threading.Tasks.TaskCompletionSource (C# 7, .NET 4.7) 2 также используют атомарную CAS-операцию, но применительно к битовой маске состояния задачи в цикле до успешного его завершения. Этот подход можно рассматривать как специализированный вариант циклической блокировки (spinlock). После этого устанавливается поле результата и пробуждаются заблокированные потоки. С ожидающей стороны потоки блокируются только если это необходимо для исключения продолжительного активного ожидания. Стоит отметить что вся логика реализована внутри Task, а TaskCompletionSource является лишь тонкой оберткой ссылки на Task.

3. boostfture и boost::promise (C++, Boost 1.66.0) 3 реализованы как обертки над boost::shared_ptr, которые ссылаются на внутренний объект общего состояния, содержащего поле результата. Потокобезопасный доступ к нему осуществляется с помощью boost::mutex и boost::condition_variable. При этом boost::shared_ptr на большинстве платформ реализован через два атомарных счётчика. boost::mutex и boost::condition_variable на POSIX системах оборачивают соответствующе pthread примитивы. На Windows boost::mutex реализован через атомарную CAS-операцию и события (events) для блокирующего ожидания, а boost::condition_variable через CAS-операцию и семафоры.

4. stdfture и std::promise (C++, libc++ 6.0) 4 реализованы так же, как их аналоги в Boost, за исключением того, что что вместо shared_ptr используется специализированный, более эффективный счетчик ссылок.

5. stdfture и std::promise (C++, libstdc++ 7.3.0) 5 также реализованы аналогично Boost, за исключением того, что что для взаимного исключения напрямую используются фьютекс (с эмуляцией через мьютекс и условную переменную на платформах не поддерживающих фьютексы).

Рассмотренные реализации выполняют взаимное исключение различными способами: с помощью атомарных операций, мьютексов, условных переменных, событий, семафоров, фьютексов или их комбинаций. Стоит отметить что java.util.concurrent.CompletableFuture полагается на тот факт, что все объекты в языке Java имеют семантику ссылок. Это позволяет производить установку возвращаемого значения любого типа с помощью атомарной инструкции. Это самый эффективный способ, но требует выделения объекта возвращаемого значения в динамической памяти. Однако, в Java это неизбежно в любом случае, и существенным недостатком не является.

Реализации System.Threading.Tasks.Task необходимо поддерживать структуры языка C#, которые обладают семантикой значений. Установка значений структур с помощью атомарных инструкций, как это сделано в java.util.concurrent.CompletableFuture, в общем случае не возможна. Это обусловлено тем, что структуры, в отличие от ссылок, могут не помещаться в операнды атомарных инструкций. Также, помимо базового механизма отложенного результата, Sys-tem.Threading.Tasks.Task поддерживает обширную дополнительную функциональность. По этим причинам, атомарная инструкция применяется к сложной битовой маске состояния, а не к самому значению.

Рассмотренные реализации на языке C++ выделяют общее состояние в динамической памяти и используют атомарный счётчик ссылок для управления его временем жизни. Низкоуровневые примитивы синхронизации, используемые для организации безопасного многопоточного доступа к внутреннему состоянию, отличаются не существенно. На всех платформах, на нижнем уровне абстракции, все сводится к фьютекс-подобной схеме. А именно: быстрый захват примитива взаимного исключения в адресном пространстве пользователя в случае низкой конкуренции, а иначе — системный вызов с блокировкой потока.

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

2 https://referencesource.microsoft.com/#mscorlib/system/threading/Tasks/Future.cs

3 https://github.com/boostorg/thread/blob/boost-1.66.0/include/boost/thread/future.hpp

4 https://github.com/llvm-mirror/libcxx/blob/release_60/include/future

5 https://github.com/gcc-mirror/gcc/blob/gcc-7_3_0-release/libstdc++-v3/include/std/future

Future и promise же являются либо непосредственно ссылками на объект общего состояния, либо простыми обертками, предоставляющими необходимый интерфейс, над такими ссылками (рис. 1).

общее состояние

объекты ниэкоурс-ймс-аой синхронизации

состояние результата

* устлчопг*«

слот для значения результата Рис. 1. Классическая реализация future и promise

Динамическое выделение памяти сопряжено с существенными накладными расходами:

1. Обход сложных структур данных, что подразумевает инвалидацию кэша.

2. Возможна необходимость дополнительной синхронизации внутри аллокатора для выделения дополнительной памяти потоку (а если аллокатор не применяет кэширование аллокаций в локальной памяти потоков, то это необходимо при каждой аллокации).

3. Возможна необходимость системного вызова для выделения дополнительной памяти процессу, если локальный кэш пуст (таких как mmap или VirtualAlloc).

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

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

Метод синхронизации потоков без динамического выделения памяти

Основная идея предлагаемого метода состоит в том, чтобы разбить объект общего состояния на две части и поместить их внутри future и promise. Переместим содержимое общего состояние во future, а ссылки на общее состояние заменим на взаимные ссылки между future и promise. При этом promise необходимо снабдить его собственным низкоуровневым примитивом синхронизации для взаимного исключения (например, мьютексом) (рис. 2).

producer

ptgmisa

seHvarue: Value) mute«., spantoek или Futax

yiSMiwb не fulune

consumer

fulune

mute* * ctwicfrtion variable или fule*

указатель на promise

де1{): Value

слот для значения результата

Рис. 2. Реализация future и promise, не требующая динамического выделения памяти

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

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

Алгоритм 1. Захват двух мьютексов (со стороны future) по методу асимметричных попыток с откатом

1. proc F_LOCK(future: Future) do

2. while true do

3. lock

4. let success: Boolean = try_lock

5. if success then

6. break

7. end if

8. unlock

9. end while

10.end proc

Со стороны promise же осуществляется простой последовательный захват мьютексов внутри promise и внутри future (алг. 2). Таким образом, в случае конфликта двух потоков предпочтение будет отдано тому, который пытается произвести захват со стороны promise.

Алгоритм 2. Захват двух мьютексов

1. proc P_LOCK(promise: Promise) do

2. lock

3. lock

4. end proc

Экспериментальная оценка производительности метода

Для предложенного метода разработана программная библиотека на языке C++. Проведена экспериментальная оценка эффективности данного метода в сравнении с существующими реализациями, а именно Boost и libstdc++. Для оценки производительности метода измерялись значения следующих параметров: Tinit - время инициализации (создания) связанной пары future и promise, Tset - время установки значения через promise и Tget - время извлечения значения через future. Также, для наглядности, вычисляется суммарная величина, "общее время" - Ttotal.

^total =Tinit+Tset +Tget

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

Таблица

Реализации для синхронизации потоков Tinit, нс Tset, нс Tget, нс Ttotal, нс

libstdc++ 8.1 101.4 335.9 390.5 827.9

Boost 1.67 255.6 71.5 237.5 564.7

Предложенная реализация 100.6 69.8 99.9 270.4

Из таблицы несложно заметить, что время инициализации Tinit почти такое же как у libstdc++8.1, а время установки значения Tset, не многим лучше Boost. Тем не менее, предложенная реализация существенно опережает существующие классические реализации по остальным параметрам.

Заключение

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

1. Новый, более эффективный по производительности метод реализации future и promise

не требующий динамического выделения памяти. Общее время выполнения операций синхронизации сократилось на 20-30%.

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

3. Сохранена защита от некорректного использования.

4. Многопоточные системы в настоящее время широко используются и представленный метод уже имеет применение в сетевой подсистеме коммерческого ПО для распределенной визуализации 3D сцен.

Литература

1. Moore G.E. Cramming more components onto integrated circuits // Electronics. 1965. Т. 38. № 8. C.114-117.

2. Kumar S. Fundamental Limits to Moore's Law // arxiv:1511.059561. 2015. [Электронный ресурс]. URL: https://arxiv.org/pdf/1511.05956.pdf (дата обращения: 14.05.2018).

3. WaldropM.M. The chips are down for Moore's law // Nature. 2016. Т. 530. C. 144-147.

4. Friedman D.P., Wise D.S. The Impact of Applicative Programming on Multiprocessing // IEEE Transactions on Computers, 1978. Т. C. 27. № 4. С. 289-296.

5. Baker H.C., Hewitt C. The Incremental Garbage Collection of Processes // Proceedings of the 1977 symposium on Artificial intelligence and programming languages, 1977. Т. 12. № 8. С. 55-59.

7. Hametner R., Medina Duarte O. Asynchronous Programming with Futures in C on a Safety-Critical Platform in the Railway-Control Domain // IEEE International Conference on Emerging Technologies And Factory Automation, 2017.

Сведения об авторах

Татьяна Ивановна Вишневская

к.ф.-м.н., доцент

МГТУ им. Н.Э. Баумана, факультет Информатика и системы управления Эл.почта: iu7vt@bmstu.ru Россия, Москва Юрий Игоревич Килочек студент магистратуры

МГТУ им. Н.Э. Баумана, факультет Информатика и системы управления Эл. почта: yuri.kilochek@gmail.com Россия, Москва

Information about authors

Tatyana Ivanovna Vishnevskaya

Ph.D. Physical and mathematical sciences, Associate Professor

Bauman Moscow State Technical University E-mail: iu7vt@bmstu.ru Russia, Moscow Yuri Igorevich Kilochek

Master's degree student Bauman Moscow State Technical University E-mail: yuri. kilochek@gmail. com Russia, Moscow

УДК 517.929, 517.983.51 В.В. Шеметова

Иркутский государственный университет

ПРИМЕНЕНИЕ ТЕОРИИ РАСПРЕДЕЛЕНИЙ К ДИФФЕРЕНЦИАЛЬНО-ОПЕРАТОРНЫМ УРАВНЕНИЯМ С ЗАПАЗДЫВАНИЕМ

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

Ключевые слова: Дифференциально-операторное уравнение, банахово пространство, распределение, фундаментальное решение.