Память и параллельные вычисления в JS. Часть 3
Часть 1 и 2 в моем телеграмм канале - https://t.me/kobezzza_channel
Продолжаю писать про память и параллельные вычисления в JS. В прошлом посте мы познакомились со структурой данных SharedArrayBuffer и API Atomics.
Давайте быстренько вспомним: SharedArrayBuffer в общем то обычный ArrayBuffer кроме того нюанса, что он может предоставлять одновременный доступ к себе сразу нескольким потокам. Разумеется, если хотя бы один из потоков начинает писать в те ячейки этого буфера, которые читаются из других потоков, то немедленно возникает гонка.
Выносим важную мысль: самое по себе изменяемое состояние не приводит к проблемам - проблемы возникают, когда у нас есть одновременно писатель и читатель.
Вообще, существует специальный контейнер RWLock, который предоставляет к разделяемым данным безопасный интерфейс по контракту: мы можем иметь неограниченное количество читателей, если нет писателя; мы можем иметь одного и только одного писателя, но не читателей. Как видите, выполнение этих двух пунктов гарантирует нам отсутствие гонок при изменении данных. Но как создать такой контейнер?
А вот тут нам и понадобятся Atomics. Давайте напишем его!
Для начала опишем контракт конструктора: у нас есть сами данные (некоторый SharedArrayBuffer или его проекция) и замок, причем замок - это тоже SharedArrayBuffer размером в 8 байт. Количество читателей будет храниться в первых 4-х байтах замка как Int32, а количество писателей в следующих 4-х байтах.
Теперь нам надо научиться передавать данные из одного потока в другой. Важно, чтобы данные были неотделимы от замка, поэтому заведем геттер, который будет возвращать кортеж.
Этот трансфер нужно будет передавать в postMessage нашего потока, например
А в файле нашего потока, для работы с этими данными, нам также нужно будет создать RWLock.
Осталось дело за малым - написать API для получения изменяемого и неизменяемого доступа.
Давайте начнем с неизменяемого доступа. Введем метод get, который будет возвращать Promise с контейнером внутри.
Этот контейнер будет содержать свойство proxy, в котором лежат защищаемые данные и метод free для освобождения ресурсов. Важный момент, что данные в proxy должны быть неизменяемыми, а после вызова free доступ к ним должен теряться. Для решения этих проблем нам понадобится Proxy.revocable, который позволит создать контейнер над данными запрещающий их изменения, а после вызовы revoke связь между Proxy и данными будет теряться.
Почти успех. Теперь нам надо гарантировать, что при получении чтения нет писателей, а если они есть - то ждать освобождения ресурсов. Также, при получении и освобождении данных на чтение нам нужно менять счетчик в #readers.
Но т.к. наш замок является разделяемым, то для безопасной работы нам нужны Atomics.
Главное, на что тут надо обратить внимание - это то, что я проверяю наличие писателей после добавления читателя.
Дело в том, что код в потоке может исполняться одновременно с нашим кодом и выполниться быстрее, т.е. кто-то получил изменяемы доступ раньше чем мы увеличим счетчик читателей. Такие нюансы нужно учитывать в многопоточном коде.
Ну, и теперь напишем код получения изменяемого доступа.
В этом методе стоит обратить внимание на compareExchange. Данный метод позволяет атомарно установить новое значение при условии, что предыдущее значение равно заданному. Возвращает этот метод старое значение вне зависимости было ли установлено новое или нет.
Получается, что мы ставим #writers в 1, только если старое значение 0, а иначе ждем, пока писателей снова не станет 0. После захвата блокировки на #writers мы ждем, пока не будут освобождены все читатели. Причем, мы точно знаем, что новые добавлены уже не будут, т.к. мы захватили блокировку в #writers и код в get учитывает это. Также, нам по-прежнему нужен Proxy.revocable, чтобы запретить доступ к данным после вызова free.
Вот мы с вами и написали один из важнейших примитивов при работе с общими изменяемыми данными, который может разделяться между потоками.
Разумеется есть и другие контейнеры для таких задач, например, Arc, Mutex или Semaphore. Возможно я опишу работу с ними в своих следующих постах. Но если эта тема оказалась интересной тебе и ты хочешь разобраться в ней куда глубже, то приглашаю на свой авторский курс "CS во Frontend" - https://kobezzza.ru.
Ссылка на код класса - https://gist.github.com/kobezzza/a859e6566c2b1d17a75148b18a038793#file-rwlock-js.
Андрей Кобец
Руководитель отдела фронтенд разработки сервиса Едадил.
Телеграмм- https://t.me/kobezzza_channel
Инстаграмм - https://instagram.com/kobezzza.channel
Youtube - https://youtube.com/@kobezzza
GitHub - https://github.com/kobezzza