Во-первых, я должен сказать, что эта статья — просто куча разных технологий и подходов, которые я попытался объединить в один текст. Если вы не боитесь, продолжайте читать :).

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

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

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

Блоккаин — довольно простая концепция. Блокчейн можно описать как связанный список подписанных объектов (блоков). Подписанный означает, что блок может быть добавлен в цепочку только после того, как его хеш будет правильно сгенерирован, например, он должен содержать точное количество нулей в начале. Процесс генерации такого подписанного хэша обычно называют майнингом, и если вы интересуетесь технологиями блокчейна или криптовалютой, вы, возможно, также слышали о Proof-of-Work (PoW), то есть именно о процессе майнинга. Это описание очень примитивно, но может дать общее представление о том, что я планирую реализовать и почему.

Итак, как было сказано, блокчейн состоит из двух основных элементов: Блока и Цепи. Давайте начнем с самого начала.

Блок — довольно простая вещь, которая содержит несколько свойств:
1. data — для хранения данных блока, это может быть что угодно, переданное в блок;
2. hash — сформулированный хэш Блока;
3. prevHash — предыдущий блок в хэше цепочки;
4. nonce — служебная переменная, которая поможет нам на самом деле добыть хэш, изменив его значение;
5. timestamp — время объекта созданный.

Теперь несколько слов о методах. Есть только два из них, которые являются общедоступными: generateHash() — отвечает за создание хэша на основе данных, одноразового номера, временной метки и других вещей, которые нам нужны. В моем случае хеш будет генерироваться также на основе значений prevHash, но это не обязательно. И, конечно же, метод mine(), который будет генерировать хэш до тех пор, пока он не станет подписанным (он должен начинаться с точного количества нулей).

Пришло время немного кодирования!

Перед тем, как углубиться в код, следует упомянуть, что почти все созданные методы будут асинхронными, а это значит, что мы будем иметь дело с промисами. Итак, давайте перейдем к коду. На первый взгляд, здесь у нас немного сложный код, поэтому рассмотрим его построчно. Сначала мы должны использовать TextEncoder.. Интерфейс TextEncoder принимает на вход поток кодовых точек и выдает поток байтов UTF-8. Затем мы вызываем метод encode TextEncoder для получения Uint8Array, который будет содержать 8-битные целочисленные значения без знака, созданные из предоставленных данных, prevHash и nonce. Уф... Давай дальше! На самом деле нам нужен этот типизированный массив, чтобы передать его в следующую функцию, которая будет генерировать хеш, так как она может работать только с ним. Затем мы вызываем crypto.subtle.digest, который получает алгоритм хеширования и типизированный массив в качестве параметров и возвращает промис. Выполненное обещание вернет ArrayBuffer. Чтобы на самом деле вытащить строку из этого ArrayBuffer, мы должны сначала обернуть его типизированным массивом Uint8Arry, а затем мы сможем поместить его значения в более общий «обычный» массив. Но это не все, что нам нужно. Наконец, мы конвертируем массив в строку. И вот процесс генерации хеша завершен. С этого момента каждый раз, когда мы вызываем generateHash(),и не меняем никакие параметры, переданные алгоритму хэширования, мы всегда должны получать один и тот же хэш.

Следующим шагом является создание метода mine().

Итак, идея здесь довольно проста. Мы рекурсивно выполняем функцию, пока не получим нужный нам хеш. На каждой итерации мы должны обновлять nonce, чтобы функция generateHash()возвращала новое значение при каждом вызове. Когда хэш приемлем, он возвращается.

Итак, это были ключевые моменты реализации блока, теперь давайте перейдем к цепочке.

Цепочка — это простая сущность с несколькими свойствами и одним общедоступным методом. chain хранит весь блок цепи, а difficulty будет использоваться в процессе майнинга. Метод Add() служит для добавления новых блоков в цепочку. Итак, приступим к реализации!

Итак, у нас есть код, который генерирует хэш блока, вызывая метод mine(). И после того, как хеш был сгенерирован, блок помещается в цепочку. Очень просто, не так ли? Да, но здесь возникает проблема. Что, если мы добавим много блоков почти одновременно. Это вызовет проблемы с вычислением хэша. В данном конкретном случае мы пытаемся получить доступ к одному общему ресурсу из разных мест. Эта проблема на самом деле очень распространена в распределенных системах и, как следствие, в параллельном программировании. И решение для этого также исходит из параллельного программирования. Самый простой вариант, который у нас есть, это реализовать семафор. Ты боишься? Но в принципе это очень простая вещь. Давай двигаться.

семафор — это сигнальный механизм. … . Семафоры используются для синхронизации (между задачами или между задачами и прерываниями) и управления выделением и доступом к общим ресурсам.https://open4tech.com/rtos-mutex-and-semaphore -основы/

Давайте посмотрим на реализацию семафора в JavaScript:

export class Semaphore {
    _maxConcurrRequests
    runningRequests = 0
    requests = []
    
    constructor(concurrRequests = 1) {
        this._maxConcurrRequests = concurrRequests
    }
    
    $call(fn, ...args) {
        return new Promise((resolve, reject) => {
            this.requests.push({
                resolve,
                reject,
                fn,
                args
            })
            this.#next()
        })
    }
    
    #next() {
        if (!this.requests.length) {
            return;
        }

        if (this.runningRequests < this._maxConcurrRequests) {
            let {resolve, reject, fn, args} = this.requests.shift();
            ++this.runningRequests;
            let req = fn(...args);
            req.then((res) => resolve(res))
                .catch((err) => reject(err))
                .finally(() => {
                    --this.runningRequests;
                    this.#next();
                });
        }
    }
}

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

Бонус

Исходный код списка задач на основе блокчейна можно найти здесь:
https://github.com/kubarskii/blockchain-todo