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

Терминология

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

ECMAScript - это язык или спецификация? Из спецификации: "Cтандарт ECMA определяет язык ECMAScript."

JavaScript - это JIT‑компилируемый и интерпретируемый скриптовый язык программирования, который используется для выполнения вычислений и управления вычислительными объектами среды выполнения, являющийся реализацией спецификации ECMAScript.

Среда выполнения (или host-среда) - это вычислительное окружение, необходимое для выполнения JavaScript программы.

Встраиваемый?

Исполнение JavaScript кода обеспечивается host‑средой.

В качестве host‑среды могут выступать:

  • Серверные платформы: Node.js (v8), Deno (v8), Bun.js (JavaScriptCore);

  • Экосистема для программирования микроконтроллеров: Espruino;

  • Веб-Браузеры: Google Chrome (v8), Mozilla Firefox (SpiderMonkey), Safari (JavaScriptCore);

Их множество, поэтому цель спецификации формализовать работу языка настолько, насколько это возможно, чтобы поведение JavaScript в host‑средах было унифицировано и эквивалентно поведению описанному в спецификации. Реализация стандарта может быть разной, но они соответствуют спецификации.

Расширяемый?

Из спецификации: ‘Каждый веб-браузер и сервер, поддерживающий ECMAScript, предоставляет свою собственную среду выполнения, дополняя среду выполнения языка ECMAScript.’

JavaScript не должен быть самодостаточным в вычислительном отношении. Ожидается, что host‑среда будет предоставлять не только объекты и средства, описанные в спецификации, но также специфичные для среды объекты, описание и поведение которых выходят за рамки ECMAScript спецификации.

Очевидный, но наглядный пример: Chrome и Node.js работают на базе движка v8, но в Chrome отсутствуют такие модули как: fs, path, os, worker_threads. Так же, как и в Node.js отсутствуют: XHR, Worker, navigator и т. д.

console, setTimeout, setInterval — это API также предоставляемые host‑средой. Их работу специфицирует HTML5.

Что интереснее, Event Loop за счет которого реализуется асинхронность тоже не является частью движка и никоим образом не упоминается в ECMAScript спецификации. Это специфическое свойство host‑среды. Его поведение регламентирует HTML5 спецификация, которую реализуют веб‑браузеры.

При вызове асинхронной операции происходит обращение к API Libuv, либо к внутреннему API Chrome, в зависимости от того, в какой среде выполняется скрипт.

Важно отметить, что ECMAScript не обязывает JavaScript быть асинхронным или многопоточным, но включает необходимые для этого стандарты. Выполнение асинхронных операций, как и создание потоков это задачи host‑среды, в которой выполняется JavaScript.

Таким образом асинхронным или многопоточным JavaScript делает именно host‑среда.

Внутреннее устройство host-среды

Рассмотрим внутреннюю работу на примере наиболее популярных.

Chrome

До 2015 года браузеры работали в одном потоке и разделяли вычислительные ресурсы между всеми открытыми вкладками — single‑threaded execution. То есть один EventLoop мог выполнять задачи от разных агентов, которые занимались выполнением скриптов вкладок. Если на одной вкладке выполнялся ресурсоемкий алгоритм, это отражалось на безопасности и работе других вкладок. Об этом подробнее тут.

Современные браузеры улучшили эту ситуацию за счет многопоточности. Каждой вкладке или плагину в большинстве случаев, соответствует отдельный процесс, что также положительно сказывается на безопасности, за счет изоляции. Также используются отдельные потоки для выполнения парсинга, стилизации, компоновки и отрисовки элементов на странице. Garbage collection в v8 работает параллельно в несколько потоков.

Сетевые запросы во внутреннем API host-среды выполняются в отдельных потоках. Продемонстрировать это наглядно можно с помощью следующего кода:

    const TODO_API = 'https://jsonplaceholder.typicode.com/todos/';

    const xhr1 = new XMLHttpRequest();
    xhr1.open("GET", TODO_API + 1, false); // Третий аргумент делает запрос синхронным
    xhr1.onload = () => {
        if (xhr1.readyState !== 4) {
            return;
        }
        if (xhr1.status === 200) {
            console.log(xhr1.responseText);
        } else {
            console.error(xhr1.statusText);
        }
        console.log(performance.now())
    }
    xhr1.onerror = () => console.error(xhr1.statusText);
    xhr1.send(null);

    const xhr2 = new XMLHttpRequest();
    xhr2.open("GET", TODO_API + 2, false);
    xhr2.onload = () => {
        if (xhr2.readyState !== 4) {
            return;
        }
        if (xhr2.status === 200) {
            console.log(xhr2.responseText);
        } else {
            console.error(xhr2.statusText);
        }
        console.log(performance.now())
    }
    xhr2.onerror = () => console.error(xhr2.statusText);
    xhr2.send(null);

Результат:

выполнение двух синхронных запросов
выполнение двух синхронных запросов

Соответственно, запросы выполнились последовательно.

Однако, при этом изучив исходники Chrome, вы узнаете, что даже выполнение синхронных запросов происходит в отдельных потоках.

Теперь выполним их в асинхронном режиме передав true в метод .open() третьим аргументом.

выполнение двух асинхронных запросов
выполнение двух асинхронных запросов

Результат показывает, что они выполнились параллельно:

Теперь выполним 4 запроса:

выполнение четырех асинхронных запросов
выполнение четырех асинхронных запросов

Они все также выполнились параллельно, в отдельных потоках.

Реализация данной функциональности в Chrome:

Выделяем отдельный поток выполнения для запроса, по завершению которого будет вызван GarbageCollector для очистки памяти.

запуск XmlHttpRequest в отдельном потоке
запуск XmlHttpRequest в отдельном потоке
ThreadableLoader - класс для управления потоками
ThreadableLoader - класс для управления потоками

Для изучения архитектуры host-среды Chrome:

Threading and tasks

Multi-process architecture

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

Node.js

Про node.js однозначно можно сказать, что это многопоточное приложение.

Обзор архитектуры Libuv

По умолчанию Libuv использует 4 системных потока.

Продемонстрируем на примере криптографических алгоритмов:

const crypto = require('crypto');

crypto.pbkdf2('12345', '5', 100000, 64, 'sha512', () => {
    console.log('1 - ', performance.now())
})
crypto.pbkdf2('12345', '5', 100000, 64, 'sha512', () => {
    console.log('2 - ', performance.now())
})
crypto.pbkdf2('12345', '5', 100000, 64, 'sha512', () => {
    console.log('3 - ', performance.now())
})
crypto.pbkdf2('12345', '5', 100000, 64, 'sha512', () => {
    console.log('4 - ', performance.now())
})
crypto.pbkdf2('12345', '5', 100000, 64, 'sha512', () => {
    console.log('5 - ', performance.now())
})

Результат выполнения демонстрирует, что первые четыре асинхронных вызова алгоритма выполнялись параллельно, выполнение пятого произошло после первого освободившегося потока.

результат выполнения потоков
результат выполнения потоков

Также начиная с версии 10.5.0 в Node.js доступен модуль worker_threads, который позволяет самостоятельно запускать рабочие потоки.

Event Loop в Node.js работает на основе паттерна Реактор и Демультиплексора, чтобы эффективно обрабатывать множество параллельных операций I/O.

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

Демультиплексор используется для прослушивания событий от нескольких источников (например: сокетов, файловых дескрипторов) и выбора доступного события для обработки. Это позволяет максимально эффективно использовать системные ресурсы.

Асинхронность

Асинхронное программирование подразумевает инициацию выполнения некоторой операции, об окончании которой главный поток выполнения узнает через некоторое время.

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

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

Из чего следует, что асинхронность это не многопоточность, но многопоточность может быть способом организации асинхронности.

В современных host‑средах выполнение асинхронных операций в большинстве случаев выполняются параллельно в отдельных потоках (или процессах), кроме Mutation Observer, Promise .then/catch/finally, queueMicrotask(()=>{}). Таким образом операции не блокируют выполнение последующих операций и могут быть запущены одновременно с другими, что позволяет увеличить производительность и продолжать реагировать на пользовательские действия, при этом более эффективно и безопасно задействовать вычислительные ресурсы устройства.

В истории JavaScript были различные механизмы и паттерны для работы с асинхронностью.

Callback

Использовались для обратного вызова функций после завершения асинхронной операции. Это простой и самый быстрый способ взаимодействовать с асинхронным API, однако у него возникли недостатки в виде:

  • Callback Hell ‑ ситуация, когда множество асинхронных операций вложены друг в друга и приводят к сложному для чтения и поддержки коду.

  • Zalgo ‑ ситуация, когда трудно определить как будет вызвана функция, синхронно или асинхронно.

  • Передача ответственности за выполнение функции обратного вызова ненадежному коду, который не гарантирует нам правильный порядок выполнения.

Лично я так и не понял первые две проблемы. В случае с Callback Hell вместо того, чтобы инициализировать анонимные функции в месте их использования, можно вынести их инициализацию выше и воспользоваться композицией, при этом задав функциям понятный идентификатор. Касательно Zalgo вполне достаточно отделять системный код от прикладного.

Promises

Появляются в ES6 стандарте. Используется для обработки асинхронных вычислений.

Спецификация Promise определяет строгий порядок операций, связанных с их разрешением. Порядок выполнения имеет важное значение для детерминированной и надежной обработки данных и управления потоком выполнения.

HostEnqueuePromiseJob

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

Async/await

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

Важно помнить о рациональном использовании последовательных вызовов асинхронных функций.

Инструменты организации многопоточности

Современная разработка не стоит на месте, и постоянно появляются новые инструменты и возможности, которые делают JavaScript еще более мощным. Однако начиная с 2009 HTML5 спецификация вводит новые возможности в JavaScript для многопоточного выполнения кода.

  • Dedicated Worker объект, который создает отдельный и изолированный контекст выполнения, работающий параллельно с основным потоком, позволяющий распределять нагрузку на несколько ядер процессора и выполнять параллельные вычисления. Это мощный инструмент в основном для выполнения вычислительно интенсивных задач, который, при его рациональном использовании может существенно повысить производительность приложения.

    Запуск воркера в отдельном потоке: Реализация в Chrome.

Запуск воркера в отдельном потоке
Запуск воркера в отдельном потоке
  • Shared Worker объект, который создает общий контекст выполнения, доступный для нескольких окон, вкладок или фреймов. В основном используется для параллельного выполнения кода и обеспечивает общий доступ к состоянию между разными частями приложения.

Также ECMAScript спецификацией представлены следующие объекты:

  • SharedArrayBuffer позволяет разделять и обеспечивает возможность совместного доступа к памяти между различными потоками.

  • Atomics Object обеспечивающий синхронизацию доступа к разделяемому сегменту памяти, что позволяет избежать гонок за ресурсами и обеспечить корректное взаимодействие между потоками.

Поток Worker'a создается в rendering-процессе вкладки, однако он не может работать с DOM и не имеет доступа к структурам памяти других потоков из-за уязвимостей типа Spectre. Браузер должен гарантировать что зловредный код с одной страницы не окажет негативное влияние на другие. Поместить сайт в песочницу сложно и довольно ресурсоемко. Учитывая еще существование iframe и браузерных расширений. Поэтому иногда rendering процесс может переиспользоваться для нескольких вкладок одного и того же сайта. Изоляция воркеров делает использование безопасным.

Если нужно передать данные Worker'y, можно использовать механизм postMessage. При передачи данных между потоками их нужно сериализовать и десерилизовать. В Chrome это делается при помощи structured clone, что не очень быстро. Если объект передается через опцию transferable, данные объекта исчезнут из основного потока.

Самый эффективный способ передачи данных между потоками это использование общей памяти SharedArrayBuffer . Однако, вы не можете его использовать без заголовка cross-origin-isolated, что также продиктовано соображениями безопасности.

Подробнее про модель памяти

Заключение

JavaScript - это встраиваемый язык, выполнение которого обеспечивается исключительно host‑средой (типа node.js или браузера), тем самым он не может иметь своего API для обслуживания событий или создания потоков, поскольку это задача host‑среды. Спецификация языка уже достаточно долгое время (9 лет) содержит в себе необходимые стандарты для параллельного выполнения кода. Если руководствоваться той логикой на основании которой сделан вывод, что JavaScript асинхронный, то ровно таким же образом нужно признать, что он и многопоточный, так как существует все необходимое, чтобы через свое API host‑среда предоставила такую возможность.

Также рекомендую к просмотру:

Комментарии (178)


  1. 19Zb84
    14.01.2024 21:32

    Спасибо. Очень интересная статья. А эта статья основанна на информации из интернета или есть книги, которые использовались для создания статьи ?


    1. Kaban_Kanban
      14.01.2024 21:32
      +3

      Думаю автор пользуется личным опытом


    1. DarkCoder30 Автор
      14.01.2024 21:32

      Благодарю! Эта статья основана на официальной спецификации языка.


  1. green_fenix
    14.01.2024 21:32
    +2

    Так же, как и в Node.js отсутствуют: XHR/Fetch, Worker, navigator и т. д.

    Начиная с v21, fetch в ноде присутствует )


    1. DarkCoder30 Автор
      14.01.2024 21:32

      ????????


    1. vanxant
      14.01.2024 21:32

      Да, зашёл написать, что fetch в ноду завезли, не прошло и 10 лет.


    1. DarkCoder30 Автор
      14.01.2024 21:32

      В любом случае Fetch является частью host-среды.


  1. Sap_ru
    14.01.2024 21:32
    +14

    Какая-то мешанина и точка так и непоставлена. Запросы-то параллельно выполняются, а сам JS от этого параллельным не стал. Если бы вы условным полингом готовность опрашивали в глухом цикле в одном потоке, то получили бы точно такой результат - OS многопоточна, JS-движок нет.
    JS - однозначно однопоточен, но никто не запрещает запустить несколько изолированных окружений, если среза позволят. Эти изолированные окружения могут иметь очень ограниченные средства взаимодействия.
    Даже без всяких worktkers, при условии доступа к необходимым функциям среды (ОС) можно тупо запустить второй интерпретатор и обмениваться с ним через pipes/sockets. И даже весьма просто обернуть это всё так, что от workers оно будет практически неотличимо. Вплоть до того что вообще никак не отличимо - просто несколько независимых интерпретаторов со взаимодействием через queues/mailslots.


    1. nin-jin
      14.01.2024 21:32
      -1

      Я даже больше скажу, JS и дfже HTML c CSS всегда были такими "многопоточными", ибо можно запускать потоки через window.open. Правда домены при этом должны быть разные, иначе несколько окон будут работать в одном потоке со всем вытекающими. Или я не понимаю и это другое?


      1. 19Zb84
        14.01.2024 21:32
        +1

        Даже без всяких worktkers, при условии доступа к необходимым функциям среды (ОС) 

        Когда worker_threds небыло использовали child_process.fork() 

        The child_process.fork() method is a special case of child_process.spawn() used specifically to spawn new Node.js processes.

        https://nodejs.org/api/child_process.html#child_processforkmodulepath-args-options


        Или я что то путаю ?


      1. DarkCoder30 Автор
        14.01.2024 21:32
        +7

        Я даже больше скажу, когда я предлагаю реализовать проект на $mol, на меня косо смотрят.


        1. nin-jin
          14.01.2024 21:32
          -5

          У вас какая-то нездоровая фиксация на $mol. Попробуйте закрыть этот гештальт, чтобы отпустило.


          1. DarkCoder30 Автор
            14.01.2024 21:32

            У меня лично никакой фиксации, вы перепутали.


    1. DarkCoder30 Автор
      14.01.2024 21:32
      +3

      Параллельное выполнение запросов не делает JavaScript параллельным. Таковым, ровно как и асинхронным его делают возможности предоставляемые host-средой, в виде Event Loop и Worker, которые в браузере специфицируются HTML5 стандартом. Примером с запросами я хотел осветить, что JavaScript как платформа уже давно является многопоточным, и асинхронное API работает по многопоточной модели. ‘ограниченные средства взаимодействия.’ - продиктованы Memory Model.


      1. 19Zb84
        14.01.2024 21:32
        +1

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

        Вы так и написали. Я так и понял и думал что это точка.

        По этому не понял это из комментария выше.

        > Какая-то мешанина и точка так и непоставлена. 


        1. DarkCoder30 Автор
          14.01.2024 21:32
          +1

          JavaScript нельзя рассматривать в отрыве от host-среды, поскольку он не самодостаточный в вычислительном плане, так как: встраиваемый и скриптовый.


    1. MountainGoat
      14.01.2024 21:32

      Получается та же фигня, что и в Питоне? Потоки есть, но второй может начать работать только когда первый задумался над системным вызовом, не важно сколько ядер в системе?


      1. ParaMara
        14.01.2024 21:32

        Фигня та же, что и в Питоне - как запустятся потоки зависит от реализации. Если вместе с автором считать что вместо понятного слова «диалект» лучше сага о хостах…

        Написано ясно - в node.ls нормально уже, например.


      1. Sap_ru
        14.01.2024 21:32
        +3

        Буквально и абсолютно та же фигня, но намного хуже: спецификация языка не поддерживает многопоточность и она реализуется запуском отдельных процессов, для чего есть некий относительно стандартный API.
        Сильно хуже потому, что:
        1) Спецификация реализации многопоточности через запуск независимых процессов не входит в спецификацию языка, а входит в спецификацию HTML5, которая вне браузера вообще никого ни к чему не обязывает. А в браузерах функционал и расширяемость спецификации сильно ограничена тем, что она заточено только под браузерное окружение и потому не расширяемо.
        2) Для поддержки любой другой многопоточности нет вообще ничего.

        В Питоне:
        1) Настоящая многопоточность сделана через точно такие же архитектурные костыли, но стандартное API универсальное - какие-то функции на конкретной платформе могут не поддерживаться, но полная спецификация покрывает большинство возможных вариантов использования (в пределах возможностей выбранных костылей).
        2) Есть поддержка псевдомногопоточности (имеется ввиду GIL, хотя на самом деле вместо истинной многопоточности вообще могут крайне хитрые корутины, как это и есть на некоторых реализациях Python). По стандарту так же может не поддерживаться или поддерживаться не полностью, но сам стандарт описывает максимальное полное API для реализации полноценной многопоточности. Т.е. если платформа допускает полноценную многопоточность например (Jython), то API не изменяется и позволяет использовать весь возможных функционал, вплоть до физических потоков ОС. Впрочем, даже при наличии GIL API позволяется работать истинной многопоточностю вплоть до потоков ОС, вопрос лишь в быстродействии и целесообразности.

        Это я не к тому, что JS плохой, а к тому, что JS и всё его сопутствующие спецификации были и есть привязаны к браузерному окружению и ограничены ограничениями этого окружения. Сугубо потому, что единственный плюс его именно в том, что это единый язык и окружение (окружение не единое, но очень близкое) и для фронта и для бэка. А все попытки притулить такой язык к каким-то универсальным задачам упираются в то, что нужно менять и язык и спецификации, а тогда пропадает то самое единственное преимущество единого окружения и оказывается, что ниша давно и плотно занята Java и Python (как два конца спектра крайне портабельных языков и окружений).
        При этом как стандарт языка JS также привязан к своему родному "браузерному" окружению и рассматриваться отдельно от него не может (привет проблеме "первого await"). Вот тут автор категорически неправ. Как и неправ в своём определении многопоточности и асинхронности (там вообще бред какой-то написан)


    1. rubinstein
      14.01.2024 21:32

      Мне кажется, что такое можно про любой язык программирования сказать. Запустите условный while(true) {} в основном потоке любого ЯПа и получите зависание.



      1. Sap_ru
        14.01.2024 21:32

        Этот не при чём. Разговор о том, можно ли вообще использую стандарт языка и стандартну библиотеку запустить второй поток не как независимый процесс (то есть с разделяемой памятью и параллельный исполнением потоков). Если вы можете запустить второй поток, иметь возможность обращаться из него к переменным или данным (к адресному пространству, в общем) других потоков, сделать в нём while( true) {} и программа не зависнет, то многопоточность есть.
        Если этого сделать нельзя, то многопточности нет.


    1. Chelyuk
      14.01.2024 21:32

      Так-то оно так. Но если двигаться в этом направлении. То практически всё сегодня многопоточно. Потому что процессоры многоядерные и зачастую мультитредовые. Тогда можно поставить гипервизор, развернуть N операционных систем, поднять в них любую среду и написать приложение которое будет в этом всём работать. Ну или просто какой-нибудь Kubernetes развернуть. Только всё это будет сделано вовсе вовсе не средствами языка программирования. И некоторые операции станут сильно дорогими в плане времени.


      1. Sap_ru
        14.01.2024 21:32
        +2

        Поэтому поток от процесса (в данном контексте) отличается именно наличием доступа к общему адресному пространству (к переменным программы). А передавать данные между процессами сильно дорого: сериализация, десериализация, копирование - вот это всё.


  1. onets
    14.01.2024 21:32
    +3

    Если уж на то пошло - надо сначала определить признаки многопоточности. И то зависит от ОС и железа.

    На одноядерном проце винда только делает вид, что она многопоточная - в реальности дает каждому процессу по чуть-чуть времени, переключая их. То есть это можно назвать эмуляцией многопоточности. И общепринятые «многопоточные языки» типа плюсов, Явы и си шарпа - работают внутри этой эмуляции.

    С появлением многопроцессорных/многоядерных систем - появилась возможность сделать более настоящую многопоточность. Но только по кол-ву ядер. Если процессов больше чем ядер - опять эмуляция.

    Следующим слоем абстракции идет средства языка по управлению многопоточностью - ну типа можно ли изнутри создать новый процесс или поток, переключать их, завершать и так далее. В общепринятых языках опять же это есть. В яваскрипте я так понимаю этого нет. Не то чтобы плюсы или Ява 100% многопоточные. Но они имеют больше признаков.


    1. DarkCoder30 Автор
      14.01.2024 21:32
      +1

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

      В JS вы можете:

      const worker = new Worker("worker.js"); // создать новый поток
      
      worker.terminate(); // завершить поток


      1. Sap_ru
        14.01.2024 21:32

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


    1. Sap_ru
      14.01.2024 21:32

      С многопоточностью всё относительно просто - должны быть отдельные потоки исполнения кода и возможность доступа к адресному пространству (считай переменным) друг-друга. Если вы загоните один из потоком в бесконечный цикл, то остальные должны продолжать работать


  1. alyxe
    14.01.2024 21:32
    -2

    На приколе? Ничего, что хром работает в штатном режиме?!


    1. DarkCoder30 Автор
      14.01.2024 21:32

      На каком приколе? Что такое штатный режим?


  1. Cryvage
    14.01.2024 21:32
    +8

    Вообще, вот если проводить аналогию с другими, известными мне языками, то обычно бывает 3 вида сущностей, отвечающих за параллельное выполнение: Process, Thread и Task. Терминология может отличаться, но суть та же. Task и Thread делят контекст с основным потоком, а Process это обособленная штука, которая работает в отдельном контексте (собственно, отдельно запущенный процесс). Task является более высокоуровневым по отношению к Thread. Task'и используют Thead'ы под капотом, через Thread Pool. В принципе, когда программист вручную работает с Thread'ами, он тоже должен использовать Thread Pool, если, конечно, хочет делать всё правильно. Да и вообще, по большому счёту, он должен делать всё то, что Task делает под капотом. Иными словами, если в вашем языке есть Task, то необходимость напрямую работать с Thread выглядит сомнительно. Может для каких-то случаев и нужно, тут спорить не возьмусь.

    В JS я вижу порезанный Task (он же Promise), вижу Process (он же Worker), и совсем не вижу Thread. С одной стороны, как уже отмечалось выше, если есть Task, то Thread не особо-то и нужен. Проблема только в том, что Promise в JS является порезанной версией классического Task, т.к. два Promise'а не могут быть выполнены одновременно, что означает, что они и под капотом не используют никакие Thread'ы, и просто выполняются по очереди, в одном потоке. Собственно, в документации к ним так и написано. Именно это имеют в виду люди, когда говорят, что JS однопоточный. Несомненно, он может делать внешние вызовы, которые будут работать параллельно. Но сам JS код всегда работает в контексте одного главного потока. Ну, кроме Worker'ов, но они уже не потоки, а процессы.

    Честно говоря, не знаю, даёт ли спецификация какие-либо указания на тот счёт, могут ли Promise'ы выполняться параллельно, но текущая реализация во всех известных средах выполнения этого не позволяет, и весь существующий код, написанный на данный момент, не рассчитан на то, что Promise'ы могли бы выполниться параллельно. Если в какой-то реализации такое сделают, то существующий код окажется несовместим с ней, т.к. в runtime посыпятся ошибки связанные с синхронизацией потоков, а точнее, с её полным отсутствием в существующем коде. Можно, конечно, добавить другой вид Promise'ов, и назвать их ParallelPromise (или Task, так будет короче). Но в этом случае, речь идёт уже о новой возможности, которая на данный момент просто отсутствует, как на уровне реализации, так и на уровне документации/спецификации. То есть, тут никак не получается притянуть за уши, что JS, дескать, многопоточный, это среды выполнения халтурят.

    Что касается приведённых вами примеров XMLHttpRequest и crypto.pbkdf2, то интереснее было бы увидеть не замеры времени, а принципиальную возможность вызвать гонку за ресурсом, ну там, какое-нибудь падение при попытке одновременно добавить элемент в глобальный массив или премешанный вывод в консоль. А так, сдаётся мне, что только запрос выполняется параллельно, а обработчик события вызывается уже в главном потоке, в порядке очереди. Почему я так думаю? Да потому что я ни разу не видел ни мьютексов, ни критических секций, прочих видов блокировок в JS коде, и при этом всё работает, ничего не падает. Если бы обработчик onload у XMLHttpRequest вызывался в разных потоках, то было бы много веселья. В частности, console.log мог бы запросто перемешать несколько выводов и вывести вам один json внутри другого. Видел подобное неоднократно, только не в JS.

    Из всего сказанного получается, что если давать классификацию, то JS однопоточный, асинхронный, многопроцессный язык. На мой взгляд, тому JS, что работает в браузерах, большего и не надо. С DOM, всё равно, не получится работать из нескольких потоков, и бедные фронтендеры только за зря будут страдать. Посмотрите на любой GUI фреймвёрк в любом ЯП, который поддерживает многопоточность, и вы поймёте о чём я говорю. Ни один из них не работает нормально с многопоточностью, и все они требуют обращаться к GUI из основного потока, иначе всё будет плохо. Именно поэтому, в JS и сделана асинхронность в одном потоке. А вот серверному JS параллельные Promise'ы не помешали бы. Но все серверные реализации эксплуатируют браузерные движки, и видимо там просто архитектурно не заложена такая функциональность, потому до сих пор и не добавили.


    1. Finesse
      14.01.2024 21:32
      +1

      В JS я вижу порезанный Task (он же Promise)

      • Promise это не какая-то магия. Это просто паттерн программирования, альтернатива колбэку.

      • Если говорить вашими терминами, то промис это совсем не Task, потому что он не имеет никакого отношения к Thread. Простая асинхронность в JS реализована через Event Loop; это ещё одна сущность, которую вы не упомянули.

      вижу Process (он же Worker), и совсем не вижу Thread

      Worker в JS это как раз Thread. Process в JS реализован через child_process.fork() (в Node.js).

      два Promise'а не могут быть выполнены одновременно, что означает, что они и под капотом не используют никакие Thread'ы, и просто выполняются по очереди, в одном потоке

      Если сделать так, что задачи внутри асинхронных функций периодически возвращают управление Event Loop, то тот будет чередовать их, и они будут выполняться как будто параллельно.

      А так, сдаётся мне, что только запрос выполняется параллельно, а обработчик события вызывается уже в главном потоке, в порядке очереди

      Верно. В пределах одного потока ни один синхронный кусок JS-кода не может выполняться одновременно с другим таким или прерываться другим таким. Важно осознавать, что ключевое слово await разделяет куски синхронного кода; во время await Event Loop может выполнить какие-нибудь другие куски синхронного кода.

      Да потому что я ни разу не видел ни мьютексов, ни критических секций, прочих видов блокировок в JS коде, и при этом всё работает, ничего не падает

      Описанное выше не отменяет возможность состояний гонки, поэтому в JS есть кастомные мьютексы, например async-mutex.

      А вот серверному JS параллельные Promise'ы не помешали бы

      Пожалуйста, кастомный Task на основе Thread: workly.


      1. chupasaurus
        14.01.2024 21:32

        Worker в JS это как раз Thread. Process в JS реализован через child_process.fork() (в Node.js).

        С каких пор подпроцессы стали нитями?


        1. chupasaurus
          14.01.2024 21:32

          Сам запутался в ваших мыслях.


          1. Finesse
            14.01.2024 21:32

            Это 2 отдельных утверждения, разделённые точкой. Я не утверждаю, что подпроцесс (вы имеете ввиду форк?) является трэдом.


            1. chupasaurus
              14.01.2024 21:32

              Я не сразу понял, что вы исправляете коммент выше. Утро вечера не всегда мудренее (:


      1. mayorovp
        14.01.2024 21:32
        +3

        Worker в JS это как раз Thread.

        У Worker все признаки отдельного процесса если не смотреть в реализацию, в частности отдельное адресное пространство.


        1. Finesse
          14.01.2024 21:32

          Судя по MDN и документации Node.js, воркер это трэд. По умолчанию память раздельная, но можно выделить общую через SharedArrayBuffer (в браузере тоже работает).


        1. DarkCoder30 Автор
          14.01.2024 21:32

          Поделитесь официальным источником на основании которого вы сделали вывод, что это процесс


          1. slonopotamus
            14.01.2024 21:32
            +1

            Не важно, является ли оно по факту процессом или нет. Важно какие способы взаимодействия допустимы.


            1. DarkCoder30 Автор
              14.01.2024 21:32
              -2

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


        1. DarkCoder30 Автор
          14.01.2024 21:32
          -1

          Я же специально приложил ссылку на модель памяти. Изоляция не делает его процессом.


          1. nin-jin
            14.01.2024 21:32
            +3

            Отдельная область памяти с отдельными правами - это буквально определение процесса. То, что это не процесс операционной системы, а процесс виртуальной машины суть не меняет. Управляемая гипервизором общая память тоже не превращает процессы в треды.


    1. SergTsumbal
      14.01.2024 21:32

      XMLHttpRequest, он выполнился асинхронно, а не параллельно. Там в корне непонимание как микротаски и макротаски работают в eventLoop’e.

      crypto.pbkdf2, libuv, это подсистема ввода/вывода ноды, к языку она отношение имеет опосредованное. EventLoop однопоточный что в ноде, что в браузере. Отличаются реализацией, количеством типов очередей и порядком прохода, по сути.


      1. DarkCoder30 Автор
        14.01.2024 21:32
        -1

        У вас в корне неверное понимание того, что такое асинхронность. Асинхронность, как и многопточность это параллельная модель выполнения. Многопоточность это один и способов реализации асинхронности, перечитайте статью.


        1. SergTsumbal
          14.01.2024 21:32
          +2

          Что мне ваша «статья» если это вообще две разные несвязанные концепции. Асинхронность про последовательность, поток там один, это просто запросить задачу в стороннем сервисе и переключится на другую, переодически проверяя выполнилась ли первая. Многопоточность про параллельность, выполнять несколько задач одновременно в разных потоках. Это базовая база


          1. DarkCoder30 Автор
            14.01.2024 21:32

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

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


            1. SergTsumbal
              14.01.2024 21:32
              +1

              Только это будет параллельность, а не асинхронность, асинхронность это IO bound задача, а параллельность CPU bound. Вы ходите чтобы люди себе фейспалмами лбы отбивали?)


              1. DarkCoder30 Автор
                14.01.2024 21:32
                -1

                А кто выполняет CPU-BOUND задачу ?


                1. mayorovp
                  14.01.2024 21:32

                  CPU, а что?


                1. SergTsumbal
                  14.01.2024 21:32

                  Ожидание процессор не нагружает)


        1. Sap_ru
          14.01.2024 21:32
          -1

          Асинхронность в вашем понимание не означает параллельности выполнения!
          Те же корутины (и из реализация в виде промисов JS) выполнятся абсолютно последоватльно!


          1. DarkCoder30 Автор
            14.01.2024 21:32

            Промисы это не корутины, и они выполняются асинхронно в последовательности разрешения, и не параллельно. Статью перечитайте.


            1. Sap_ru
              14.01.2024 21:32
              -1

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

              Так что все спекулации на счёт, что как язык JS однопоточность не постулирует идут мимо кассы, так как он очень даже постулирует, просто не явно.

              Нет семантически корректного когда на JS, который бы демонстрировал истинную многопоточность. У вас всегда есть "главный" await, исполняемый код которого строго задаёт последовательность вообще всех возможных в программе промисов. А всё остальное уже платформозависимое и требует event loop..
              Муа-ха-ха


      1. DarkCoder30 Автор
        14.01.2024 21:32

        Event Loop никакого отношения не имеет к языку.


        1. SergTsumbal
          14.01.2024 21:32

          Так же как и хост-объекты не имеют никакого отношения к языку, только нативные, js и без хост-объектов будет js, так что ваш вывод про «многопоточность» js опираясь на хост-объекты мягко говоря не верен


          1. DarkCoder30 Автор
            14.01.2024 21:32

            1. SergTsumbal
              14.01.2024 21:32
              +1

              И? JavaScript вообще не обязан иметь хост-обьекты, чтоб быть JavaScript’ом, движок v8 вообще не привязан даже к eventLoop’у


        1. Sap_ru
          14.01.2024 21:32

          В JS скорее имеет, чем нет. Простой вопрос - каким образом в JS сделать самый первый await если await на глобальном уровне не допускается? Как запланировать выполнение промиса без await?
          В этой части синтаксис JS без платформы просто не имеем смысла. А все реализации платформы предполагают event loop, и не могут быть реализованы иначе.


          1. DarkCoder30 Автор
            14.01.2024 21:32
            -2

            JS БЕЗ ПЛАТФОРМЫ НЕ ИМЕЕТ СМЫСЛА, ЭТО СКРИПТОВЫЙ ЯЗЫК, БЕЗ СВОЕГО I/O.


            1. Sap_ru
              14.01.2024 21:32

              I/O не имеет отношения к многопоточности.
              У вас в JS на уровне синтаксиса постулируется однопоточность. Напишите пример когда на JS, которых хотя бы потенциально с точки зрения грамматики языка мог бы быть многопоточным.
              Из асинхронности там есть промисы в виде async/await, а сама спецификация ЯЗЫКА говорит о том, что все возможные в программе функции async исполняются строго ПОСЛЕДОВАТЕЛЬНО.


            1. SergTsumbal
              14.01.2024 21:32
              +1

              Он давно не СКРИПТОВЫЙ язык, с появлением ноды


      1. DarkCoder30 Автор
        14.01.2024 21:32

        Скиньте официальный источник, который определяет, что такое «макротаски». Вы апеллируете мифами, которые придуманы в следствии упрощения учебных материалов.


        1. SergTsumbal
          14.01.2024 21:32

          Что извините?


        1. Sap_ru
          14.01.2024 21:32
          -2

          А вы сделайте самодостаточный JS-файл с использованием async, который гарантированно будет работать запускаться хотя бы на уровне синтаксиса. Т.е. в нём должен быть изначальный await.


          1. SergTsumbal
            14.01.2024 21:32

            Top-level await в ноде при использование .mjs с 2021 года. И он в стейджах es. Погуглите, много нового узнаете


            1. Sap_ru
              14.01.2024 21:32
              -1

              Там, где такой ситаксис разрешён, порядок исполнения блоков async и всего возможного дерева исполнения всех возможных в программе блоков async строго определён и постулируется, что все эти блоки будут исполнены последовательно. Т.е. даже теоретически многопоточность возможно только за счёт некоторых дополнительных библиотек конкретного окружения.
              Не говоря уже о том, что на данный момент (скоро всё может измениться, но пока так) это всё будет являться нестандартным платформо-зависимым расширением языка.


            1. Sap_ru
              14.01.2024 21:32
              -1

              Я, собственно к тому, что если вы постараетесь написать синтаксически корректный код на JS, даже с использованием каких-то перспективных версий языка, но без использования расширений, предоставляемых конкретным окружением (и не имеющих отношения к самому языку), то окажется, что порядок исполнения всех блоков async будет последовательным и будет строго задан грамматикой языка. То есть сам язык истинной многопоточности не предоставляет, а уж при помощи какой-то специфичной платформы вы совершенно любой язык может сделать как однопоточным, так и многопоточным либо степени многопоточности.


        1. mayorovp
          14.01.2024 21:32
          +1

          "Макротаска" - это задача (Job), поставленная в очередь операциями 9.5.4 HostEnqueueGenericJob или 9.5.6 HostEnqueueTimeoutJob


      1. speecc
        14.01.2024 21:32

        А откуда вы вообще берете эти 'макротаски' ? Если в спеке нет такого определения


        1. werevolff
          14.01.2024 21:32

          У вас здесь что, секта свидетелей многопоточного JS? https://developer.mozilla.org/en-US/docs/Web/API/HTML_DOM_API/Microtask_guide


          1. DarkCoder30 Автор
            14.01.2024 21:32
            -1

            Единственным официальным источником, с 1998 года является сайт спецификации ECMAScript.


            1. nin-jin
              14.01.2024 21:32
              -1

              1. DarkCoder30 Автор
                14.01.2024 21:32

                Мы рассматриваем фундаментальный вопрос. На чем, кроме как не на официальной спецификации должны строиться наши суждения ?


                1. nin-jin
                  14.01.2024 21:32
                  +1

                  Есть разные источники информации: от базовых знаний компьютерных наук, до откровений разработчиков интерпретаторов и реверс-инженеринга.


            1. werevolff
              14.01.2024 21:32
              +2

              -- Советский Союз официально не распался

              -- Российская Федерация является коммерческой организацией, зарегистрированной в США.

              -- Единственным официальным источником, с 1998 года является сайт спецификации ECMAScript.

              Ничего из вашей персональной Книги Мормона не упустил? Думаю, с вашими безусловными и недоказуесыми высказываниями, вам нужно заниматься политикой, а не IT. Mozilla Foundation уже много лет работает над стандартами JS и HTML. EcmaScript - это основа для языка JS. Сам по себе он не имеет хост-сред для исполнения.

              И, более того, EcmaScript не эквивалентен JavaScript. Когда вы говорите, что JS многопоточен потому, что ES не имеет ограничений на создание потоков ОС, то вы элементарно путаете ES и JS.


              1. mayorovp
                14.01.2024 21:32

                И, более того, EcmaScript не эквивалентен JavaScript.

                Вообще-то эквивалентен

                Вот, например, цитата из актуальной спецификации HTML:

                The term "JavaScript" is used to refer to ECMA-262, rather than the official term ECMAScript, since the term JavaScript is more widely known.


                1. werevolff
                  14.01.2024 21:32
                  -1

                  EcmaScript - это не язык, а спецификация для создания скриптовых языков. JavaScript - это реализация EcmaScript. То есть, когда мы говорим о JavaScript, то мы имеем ввиду EcmaScript + детали реализации. Такие как микро и макро таски. В EcmaScript их нет, а в JavaScript они есть. Но статья про JavaScript, а не про EcmaScript, не так ли? Поэтому, утверждения автора о том, что в JS нет макро и микро тасок только потому, что в спецификации ES их нет, это чистой воды шизофрения. Просто господина техлида в комментах разжаловали до middle, вот он и бесицо. Вы тоже подменяете понятия, или недостаточно хорошо знаете Английский, поскольку is used to refer to переводится как "используется для отсылки на", а не "Эквивалентен".


                  1. mayorovp
                    14.01.2024 21:32

                    EcmaScript - это не язык, а спецификация  [...]

                    Э-э-э, а что у языка есть кроме спецификации-то?

                    Почему какой-нибудь встроенный в приложение lua остаётся lua, встроенный в приложение python остаётся python, а встроенный в браузер EcmaScript превращается в JavaScript?

                    JavaScript - это реализация EcmaScript.

                    citation needed

                    Такие как микро и макро таски. В EcmaScript их нет, а в JavaScript они есть.

                    В EcmaScript они есть, только называются по-другому (Generic Job, Promise Job и Timeout Job).

                    Вы тоже подменяете понятия, или недостаточно хорошо знаете Английский, поскольку is used to refer to переводится как "используется для отсылки на", а не "Эквивалентен".

                    Под эквивалентностью я понимал отсутствие дополнительных смыслов, и то что одно из названий - официальное, а второе "используется для отсылки", этому не противоречит.


                    1. werevolff
                      14.01.2024 21:32

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


                      1. mayorovp
                        14.01.2024 21:32

                        "Набор лексем, которые могут быть переведены в команды для их запуска на вычислительной технике" - это программа, а не язык программирования.


                    1. nin-jin
                      14.01.2024 21:32
                      -1

                      https://en.m.wikipedia.org/wiki/ECMAScript

                      Забавный факт: JavaScript никогда не поддерживал ECMAScript4, в отличие от ActionScript.


          1. Alexandroppolus
            14.01.2024 21:32

            Вопрос был про "мАкротаски"


            1. nin-jin
              14.01.2024 21:32
              +1

              Если есть микро, то всё остальное - макро.


  1. vanxant
    14.01.2024 21:32
    +2

    Хотя в спецификации js воркеры называются потоками (worker threads), на самом деле это процессы по своей сути, поскольку не имеют общей памяти и общаются друг с другом только через сообщения (ну и ещё перехват асинхронных вызовов).

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


    1. DarkCoder30 Автор
      14.01.2024 21:32
      -2

      Они имеют общую память. И то, что они по вашему её не имеют - это не делает их процессами.


      1. SergTsumbal
        14.01.2024 21:32
        +1

        По умолчанию если не шарить память то не имеют они общую память, а если шарить то приходиться решать проблему с синхронизацией


      1. vanxant
        14.01.2024 21:32
        +2

        Нет, конечно, разберитесь сначала о чём пишете. Общая память в потоках это когда вы объявляете произвольную переменную, возможно со словом synchronized, и дальше пользуетесь ей как обычно во всех потоках. Приседания с SharedArrayBuffer - это обычный механизм памяти, разделяемой между процессами.


        1. DarkCoder30 Автор
          14.01.2024 21:32
          -1

          Ну так это было специально сделано, чтобы нивелировать сложности многопоточного программирования.


          1. SergTsumbal
            14.01.2024 21:32
            +2

            Что специально сделано? Многопоточное программирование в той же ноде зачаточное и не простое, race condition, deadlock, livelock, синхронизация, critical section вам знакомы?


  1. Lexicon
    14.01.2024 21:32

    Очень намешали, не соглашусь с заявлением.
    Когда говорят JS "язык X типа", предполагается, что возможности языка определяет спецификация, в этом смысле ecmascript синхронный язык с поддержкой асинхронных операций и работой в одной потоке.

    EvenLoop действительно реализуют почти все среды, но у JS нет власти над спецификой его реализации, вам ничего не помешает взять Java-Nashorn и нагородить беспредел. JS так же остается синхронным, у вас не появляется способа управлять работой потока.

    Workers это отдельная тема, кто-то мог бы заметить, что они мало отличаются от 3 nodejs запущенных параллельно с общением через fs, но даже тогда, они "хуже", медленнее и неэффективнее основного процесса, скажем, в сетевых операциях. Однако, - это и правда многопоточность.

    Делает ли это JS многопоточным? На мой взгляд нет, как класс решения задач с потребностью в многопоточности вы не выберете NodeJS. А вот для веб-сервера выберете и локальные задачи многопоточности решить сможете.

    Имхо семантика вопроса очень скучна и очень жаль, если кто-то читает эту статью, чтобы задавать вопросы или давать ответы на собеседованиях.


    1. DarkCoder30 Автор
      14.01.2024 21:32
      +1

      Вы точно внимательно прочитали статью?
      Повторяю, JavaScript не должен быть самодостаточным в вычислительном плане! Это встраиваемый и скриптовый язык для управления тем, что предоставляет ему HOST-среда. HOST-среда предоставляет возможность создания тредов. Возьмите Java-апплеты и выполните в браузере, и у вас не будет возможности нагородить беспредел.


      1. Lexicon
        14.01.2024 21:32

        А кто вам сказал, что должен, вы комментарий то прочитали, где этот тезис вообще взяли, чтобы его опровергать?

        Я только за эффективность дискутирую, с точки зрения эффективности создания продукта, нода уступает альтернативам в управлении своим или другими потоками. Говоря о V8, Воркеры реализованы заведомо без врожденной NodeJS магии относительно работы http сервера например, это означает, что потоки для этих целей вы поднимать не захотите. В добавок, это все же свойства платформы, а не языка - шагнули в edge-compute, - нужен другой подход.

        К чему пасс про Netflix и Twitch не понял, вы уж раскройте мысль


    1. DarkCoder30 Автор
      14.01.2024 21:32
      -1

      Расскажите разработчикам таких платформ как Netflix или Twitch, что JS - однопоточный.


      1. SergTsumbal
        14.01.2024 21:32
        +3

        Они и так это знают)


  1. debagger
    14.01.2024 21:32

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

    Эти понятия - "однопоточный"/"многопоточный" вообще неприменимы к языкам программирования. Так что, из-за чего вообще весь сыр-бор? Что кто-то интуитивно, и неправильно, пользуется терминами?

    Услышите в следующий раз что кто-то так говорит, попросите привести пример многопоточного ЯП. Будет интересно )))


  1. nameisBegemot
    14.01.2024 21:32

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

    Либо за счёт сложных условных конструкций. Или проще - запретов мнонопотосности.


    1. slonopotamus
      14.01.2024 21:32
      +1

      Ruby, Python. В обоих на уровне дизайна языка никаких ограничений на многопоточность нет (в отличие от JS), и есть апи для создания потоков. И в частности JRuby очень даже многопоточный.


    1. Sap_ru
      14.01.2024 21:32

      А как разница инпретериуются инструкции или байт код? Исполнительные потока Java пока JIT не включился внутри очень и очень похожа на Python. Более того, в Python тоже есть байт код. Единственная разница в том, что Java не требует тащить с собой компилятор и dev-библиотеки. Но в Java можно сделать среду, которая сможет компилировать исполнять произвольных код runtime, а Python никто не запрещает откомпилировать (привет PyPy)


  1. Finesse
    14.01.2024 21:32
    +1

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

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

    Касательно Zalgo вполне достаточно отделять системный код от прикладного.

    Причём здесь степень пракладности кода? Проблема может возникнуть где угодно.


  1. werevolff
    14.01.2024 21:32
    +4

    Таким образом асинхронным или многопоточным JavaScript делает именно host‑среда.

    Вы же сами дальше пишете разные определения асинхронности и многопоточности (причëм, к определению асинхронности есть претензии). Это разные вещи. Но ваше предложение можно интерпретировать так, что это одно и то же.

    Теперь выполним их в асинхронном режиме передав true в метод .open() третьим аргументом.

    Результат показывает, что они выполнились параллельно

    А я вот открыл документацию https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/open и прочитал, что третий аргумент инициирует не параллельный, а именно асинхронный запуск. Вы бы разобрались для начала, чем асинхронное выполнение отличается от многопоточного запуска.

    Например, есть главный поток, и несколько рабочих потоков

    Вы можете рассказать, что такое "поток"?


    1. DarkCoder30 Автор
      14.01.2024 21:32
      +1

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


      1. SergTsumbal
        14.01.2024 21:32
        +5

        Параллельное исполнение

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

        Параллельное исполнение - это строгое подмножество конкурентного исполнения. Это значит, что на компьютере с одним процессором параллельное программирование - невозможно;)

        Многопоточность

        Многопоточность - это один из способов реализации конкурентного исполнения путем выделения абстракции "рабочего потока" (worker thread).

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

        Асинхронное исполнение

        Асинхронность (asynchrony) подразумевает, что операция может быть выполнена кем-то на стороне: удаленным веб-узлом, сервером или другим устройством за пределами текущего вычислительного устройства.

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


        1. DarkCoder30 Автор
          14.01.2024 21:32

          Один процессор имеет не одно ядро. Одно ядро имеет не менее двух потоков. Если мы не говорим об Pentium 1.


          1. SergTsumbal
            14.01.2024 21:32
            +2

            А eventLoop однопоточный, поэтому параллельность в нем невозможна, только асинхронность


            1. DarkCoder30 Автор
              14.01.2024 21:32

              Эмм, я и не утверждаю, что eventLoop многопоточный, это вообще как бы разные вещи.


              1. SergTsumbal
                14.01.2024 21:32
                +2

                Так вы не знаете что eventLoop отвечает за выполнение кода? И как следствие javascript однопоточен по природе?


                1. DarkCoder30 Автор
                  14.01.2024 21:32

                  Вы вообщем о чем ? У нас предмет разговора это многопоточность, а не event loop. Каждый тред имеет свой event loop.


                  1. Sap_ru
                    14.01.2024 21:32

                    А у вас в JS нет тредов :))) А значит нет многооточности :) Именно на уровне языка нет.


                    1. DarkCoder30 Автор
                      14.01.2024 21:32

                      На уровне языка нету и основного потока, который делает его однопоточным. Читайте что такое скриптовый язык…


                      1. Sap_ru
                        14.01.2024 21:32

                        Это уже совсем глупость. Язык может "скриптовым" и быть истинно многопоточным.
                        Вам показать истинную многопоточность на уровне потоков OS на 100% "скриптовом" Python?
                        "Скриптовость" языка это вообще некорректное понятие, имеющее смысл лишь по отношению к конкретной платформе и конкретному окружению. Тот же JS я вам могут откомпилировать в чистый бинарный код, и это никаким образом не повлияет на возможности языка и особенно на много поточность.


                      1. werevolff
                        14.01.2024 21:32

                        Зануда mode On:

                        Откомпилировать в машинный код.

                        Зануда mode Off.


                      1. SergTsumbal
                        14.01.2024 21:32
                        +1

                        Он к скриптовым относился лет 15 назад, нет многопоточной реализации eventLoop’а в природе


              1. werevolff
                14.01.2024 21:32
                +3

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

                Дело в том, что I/O операции не превращаются в CPU-Bound операции. Они просто ждут некоего результата ввода-вывода. Например. если вы запустили таймер, то таска будет ждать, пока не настанет определённый момент, и только тогда сможет продолжиться. Разумеется, если её не блокируют соседние таски. И в вашем примере с вызовом XMLHttpRequest запросы идут не параллельно. Просто каждая таска в EventLoop отправляет запрос и становится на паузу, ожидая ответ сервера. Пока она на паузе, другие таски поочерёдно накидывают запросы на другие эндпоинты и тоже становятся в ожидание. Эти таски исполняются не паралельно, а асинхронно. То есть, в одном потоке, не блокируя друг друга. А вот когда ответ придёт, они выполнятся последовательно, в соответствии с очерёдностью ответов от сервера. Можете сами проверить. Пока одна таска будет обрабатывать свой ответ, другие, или часть других не исполнятся.

                В отличие от асинхронных задач, параллельные задачи могут блокировать друг-друга, обращаясь одновременно к одному и тому же разделяемому ресурсу. А также, они могут конкурировать за процессорное время или не конкурировать, если запускаются на многоядерном процессоре или на нескольких процессорах одновременно. Параллелизм, в отличие от асинхронности, предполагает, что задачи выполняются в пересекающихся промежутках времени. То есть, в асинхронном программировании мы предполагаем, что задача встаёт на паузу и будет выполнена тогда, когда изменятся внешние обстоятельства (прилетит ответ с сервера). А в параллельном или конкурентном программировании мы считаем, что одинаковые задачи выполнятся почти одновременно или полностью одновременно (что, правда, невозможно, но с некоторой погрешностью мы говорим об одновременности выполнения). Разумеется, если они не будут сильно блокировать друг-друга при доступе к разделяемым ресурсам.

                И, в завершение: асинхронность, в большинстве случаев, предполагает запуск задач в одном потоке. Мультипоточность, в большинстве случаев, основана на механизме ОС создания потоков в процессе. Но число потоков в процессе не ограниченно. Поэтому, например, если процессу выделено 2 ядра многоядерного процессора, но он породил 8 потоков, то в один и тот же момент времени активными будут только 2 потока. Проблема в том, что JS не предоставляет средств для того, чтобы забронировать себе ядра процессора. Поэтому, с высокой долей вероятности, для браузера это будет одно ядро на приложение, которое, скорее-всего, будет расшарено с другими системными приложениями и вопрос исполнения будет решаться ОС. В теории, можно попробовать получить доступ к вычислительным ядрам видеокарты, коих там довольно много, и тогда можно добиться истинного параллелизма.


      1. werevolff
        14.01.2024 21:32

        Не согласен. Думаю, что ваше предложение будут так интерпретировать. С утверждением о том, что многопоточность может быть одним из способов реализации асинхронности, согласен лишь частично. Формально, может. Если не рассматривать разницу между операциями, требующими ожидания I/O и операциями, требующими вычислений. Но тогда не совсем понятно, чем параллелизм отличается от асинхронности?


        1. DarkCoder30 Автор
          14.01.2024 21:32

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


          1. werevolff
            14.01.2024 21:32

            Образ ваших мыслей несколько... спутан. Вы путаете асинхронность, параллелизм и конкурентность. Вам бы, простите, с матчастью ознакомиться и внести небольшие изменения в статью.


            1. DarkCoder30 Автор
              14.01.2024 21:32
              -1

              Позвольте, там написано, что асинхронность это не многопоточность. То, что многопоточность может быть как конкурентной, так и параллельной это очевидно еще до матчасти, но это тема отдельной статьи. В данном случае вы фактически в главном потоке запускаете множество IO-Bound операций, выполнение которых распределяется по потокам в рамках которых эти операции являются CPU-bound. Распределением занимается host-среда, что и предоставляет вам асинхронность.


              1. werevolff
                14.01.2024 21:32
                +1

                множество IO-Bound операций, выполнение которых распределяется по потокам в рамках которых эти операции являются CPU-bound

                Это как? Что считает процессор, пока host-среда ждёт ответа от сервера?


                1. SergTsumbal
                  14.01.2024 21:32

                  Ну мало ли, факториалы про запас насчитывает)


                  1. werevolff
                    14.01.2024 21:32

                    Лучше крипту майнить, чтобы ресурс не пропадал. Благо, JS майнеры - не редкость в наши дни.


                    1. SergTsumbal
                      14.01.2024 21:32

                      Тоже промелькнуло


                1. DarkCoder30 Автор
                  14.01.2024 21:32

                  А выполнение самого запроса вы за задачу требующую вычислительных ресурсов не считаете ?


                  1. werevolff
                    14.01.2024 21:32

                    Сам запрос выполняет сервер. Когда приходят ответы, они будут обработаны последовательно. Если только в процессе их обработки не возникнет других задач, требующих ожидания ввода/вывода.


                    1. DarkCoder30 Автор
                      14.01.2024 21:32
                      -1

                      Как бы браузер открывает TCP-соединение с сервером, которое требует вычислительных ресурсов, он пишет в сокет http-запрос, в то время сервер читает этот запрос и пишет в сокет ответ браузеру. Они оба его выполняют.


                      1. werevolff
                        14.01.2024 21:32
                        +2

                        Каких именно вычислительных ресурсов требует поддержание TCP соединения?


                  1. SergTsumbal
                    14.01.2024 21:32

                    Нет, кинуть запрос микросекунды


              1. SergTsumbal
                14.01.2024 21:32
                +1

                Нет конечно, и там нет никакого «выполнения», там ожидание ответа, те же сетевые задержки, что полностью IO, во время ожидания можно выполнить другие задачи, называется неблокирующий асинхронный IO. Поднимать потоки на такое слишком затратно. libuv поднимает пулл на тяжелые синхронные операции, для справки. Легко проверяется оборачиванием любых синхронных функций в Promise.all, нет задержек, нет выигрыша


                1. DarkCoder30 Автор
                  14.01.2024 21:32
                  -2

                  Откровенная чушь, читайте документацию хрома. Больше я на это отвечать не вижу смысла.


                  1. SergTsumbal
                    14.01.2024 21:32
                    +1

                    Уровень у вас не тот чтоб что-то отвечать, это верно. Тут базу надо и матчасть учить, что вам уже многие сказали, и не один раз


                    1. DarkCoder30 Автор
                      14.01.2024 21:32
                      -2

                      Идите учить матчасть.


                      1. mayorovp
                        14.01.2024 21:32
                        +1

                        И что это доказывает?


                      1. DarkCoder30 Автор
                        14.01.2024 21:32

                        Это доказывает, что выполнение асинхронных операций host-среда выполняет в отдельных потоках.


                      1. mayorovp
                        14.01.2024 21:32

                        Это доказывает существование других потоков, но не доказывает что операции выполняются в них блокирующим образом (и уж тем более CPU-bound образом!)


                      1. SergTsumbal
                        14.01.2024 21:32
                        +1

                        Хотите через сборщик мусора запустить параллельно код на дважавскрипте или что?


                      1. DarkCoder30 Автор
                        14.01.2024 21:32
                        -1

                        Вы забыли очки одеть ?)


                      1. werevolff
                        14.01.2024 21:32

                        Правильно говорить "НАдеть".


                      1. MaNaXname
                        14.01.2024 21:32

                        Вы показали исходники браузера. Не V8.


                      1. DarkCoder30 Автор
                        14.01.2024 21:32

                        Я поражен вашей наблюдательностью. Действительно, я показываю исходники браузера, а не v8.


  1. ErshoffPeter
    14.01.2024 21:32
    +2

    Та самая статья из-за которых сижу на Хабре и перелопачиваю сотни постов шлака!

    Спасибо большое автору и комментаторам!

    Сам балуюсь JS время от времени и ломаю голову и над асинхронностью и над всем остальным, что упомянуто в статье.


  1. Akuma
    14.01.2024 21:32
    +1

    Какая-то мешанина всего подряд. Даже древний XMLHttpRequest вспомнили (который кстати не многопоточный, а асинхронный).

    Современный JavaScript позволяет запускать отдельные потоки и выполнять в них код (как в браузере, так и в NodeJS). Если явно этого не делать, он однопоточный. Асинхронность - не многопоточность. Конец.

    Ии...такое определение подходит под любой язык, честно говоря. Не видел ни одного языка, который бы "сам" создавал отдельные потоки. Всегда это делает программист явно. Поэтому вопрос должен бы звучать иначе: "Можно ли в JS создать отдельный поток?" Можно.


    1. DarkCoder30 Автор
      14.01.2024 21:32

      Асинхронность не ровно многопоточность.

      Асинхронность можно реализовать за счет многопоточности. Асинхронность как и многопотночночность это параллельная модель выполнения…


      1. yrub
        14.01.2024 21:32
        +1

        что-то тут все жонглируют терминами и словами.. асинхронность никакого отношения к многопоточности не имеет, асинхронность это про модель control flow вашей программы.

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

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

        Асинхронность как и многопотночночность это параллельная модель выполнения…

        так что нет, асинхронность никакая не модель параллельного выполнения, это просто модель выполнения.

        ЗЫ: к слову ваши замеры времени ничего не доказывают, и по ним ничего конкретно сказать нельзя. 33, 37, 37, 37, 63. - это легко может выполняться в одном потоке, например первый раз код отрабатывал 33, т.к. надо было jit сделать и другие дела, потом фактически время выполнения было 0 и в конце еще раз подвисло, например на сборку мусора. палкой потыкали, но если знаний о внутренности системы 0, но лучше не делать гипотез о ее работе и перформансу, это золотое правило перформанс тестов и оптимизации.


        1. SergTsumbal
          14.01.2024 21:32

          Замеры через new Date весьма наивная штука, в ноде для этого есть perf_hooks, libuv которые пристегнут к v8 и отвечает за io, по дефолту 4 потока использует, только это особенность ноды, а не языка


          1. DarkCoder30 Автор
            14.01.2024 21:32
            -1

            Честно, это не возможно комментировать, так как это фундаментально неверно. Если руководствоваться такой логикой, выходит следующее:

            Тот «один» поток, который делает JavaScript «однопоточным» - это также не особенность языка. Поскольку это скриптовый язык, который не может самостоятельно иметь своего потока выполнения, он представляется host-средой.


            1. SergTsumbal
              14.01.2024 21:32

              Я бы на вашем меньше «фундаментально» удалил статью, чтобы не быть посмешищем, javascript спокойно может работать на своих нативных объектах, он не обязан иметь libuv. И все языки сценариев являются языками программирования, почему вы постоянно подчеркиваете слово “script”, javascript уже очень давно не просто скриптовый браузерный язык


              1. werevolff
                14.01.2024 21:32

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


                1. DarkCoder30 Автор
                  14.01.2024 21:32
                  -2

                  Это мне приходится вам суть разжевывать, что мне вовсе неинтересно делать...


                  1. werevolff
                    14.01.2024 21:32

                    А при чëм тут сборщик мусора? И где здесь создание потока?


                1. SergTsumbal
                  14.01.2024 21:32
                  +1

                  Там фейспалм через предложение, просто надерганы термины, смысл которых до конца не понят, из это собран какой-то мирок на основе которого сделаны какие-то странные выводы. Классический эффект Даннинга-Крюгера


                  1. werevolff
                    14.01.2024 21:32

                    Типичный начинающий миддл. Страдает проблемами неофитства. Начал что-то понимать, но контролировать процесс не может.


                    1. MaNaXname
                      14.01.2024 21:32

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


                1. Sap_ru
                  14.01.2024 21:32

                  Тогда смысл статьи не просто потеряется, а противоположным станет.


              1. DarkCoder30 Автор
                14.01.2024 21:32
                -1

                Исходники научитесь читать хотя бы.


                1. nin-jin
                  14.01.2024 21:32
                  +2

                  Оказывается С многопоточный язык...


                  1. DarkCoder30 Автор
                    14.01.2024 21:32
                    -1

                    Мы доказываем не это…


            1. yrub
              14.01.2024 21:32
              +1

              это также не особенность языка. Поскольку это скриптовый язык, который не может самостоятельно иметь своего потока выполнения, он представляется host-средой.

              и это вы тоже часто пишете с непонятным упором. язык это набор спецификаций (как программа должна выполняться) и правил грамматики (как ее распарсить). далее вопрос что в этих спецификациях, подразумеваю что ничего про параллельность. для примера в java есть отельно документ по спецификациям языка и по спецификациям виртуальной машины (как читать байткод и какой должна быть архитектура виртуальной машины). я вот загуглил, именно в спецификациях языка идет разговор про "happens before" и прочие вещи которые вылезают при конкурентном выполнении программ. т.е. на уровне спецификаций языка оговариваются моменты, которые имеют смысл только если у вас код выполняется в многопоточной среде и  в vm есть настоящие потоки. Так что можно идти от обратного - если в спеках js подобного нет, то его можно считать однопоточным ну или хотя бы условно многопоточным. вообще сделать многопоточную vm не так то и просто как показывает практика, ни руби ни пайтон за 15+ лет так и не осилили убрать из машины GIL, видимо опять таки в спецификациях языка необходимо продумать сложные моменты ну и реализовать еще все это. наверно глядя на фронт работ никто не взялся, понимая что это все равно будет работать раз в 20 медленней чем java и поэтому пошли в другую нишу.


        1. DarkCoder30 Автор
          14.01.2024 21:32

          К слову там рядом ссылка прикреплена на документацию Libuv, которая подтверждает мои слова, поэтому можете не основываться на моем примере.


          1. SergTsumbal
            14.01.2024 21:32

            В браузере libuv нет, а javascript есть, вот ведь задачка)


    1. mayorovp
      14.01.2024 21:32

      Только вот потоки разделяют общую память, а воркеры нет.


      1. Alexandroppolus
        14.01.2024 21:32
        +1

        А как же SharedArrayBuffer?


        1. mayorovp
          14.01.2024 21:32
          +1

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


      1. Akuma
        14.01.2024 21:32
        +1

        А это прям обязательное условие чтобы называться потоком?

        https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers

        Web Workers are a simple means for web content to run scripts in background threads

        Я б не заморачивался. Оно позволяет запускать код в отдельном потоке? Позволяет. Все, можно считать это обычным потоком.


        1. mayorovp
          14.01.2024 21:32
          +1

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

          Я не могу в JS так просто взять и расшарить объект между двумя воркерами, я обязан использовать либо передачу сообщений, либо сырую общую память. С другой стороны, я могу не бояться что кто-то случайно расшарит объект который нельзя шарить.

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


          1. DarkCoder30 Автор
            14.01.2024 21:32
            -1

            Это сделано специально, чтобы не обременять разработчиков трудностями многопоточного программирования. Только что это меняет?


          1. Kahelman
            14.01.2024 21:32

            Erlang вам в помощь. Попробуйте что нибудь расшарить. Он точно многопоточный и все построено на передаче сообщений. Так что не аргумент :)


            1. mayorovp
              14.01.2024 21:32
              +1

              А в том Erlang эти "потоки" случайно не называются процессами?


            1. slonopotamus
              14.01.2024 21:32
              +1

              https://www.erlang.org/doc/getting_started/conc_prog.html

              In Erlang, each thread of execution is called a process.

              (Aside: the term "process" is usually used when the threads of execution share no data with each other and the term "thread" when they share data in some way. Threads of execution in Erlang share no data, that is why they are called processes).


            1. Sap_ru
              14.01.2024 21:32
              -2

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


          1. Akuma
            14.01.2024 21:32
            +1

            Ну можно, например, сделать так https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Transferable_objects с некоторыми объектами.

            Но это же JS. Ну все понимают, что в контексте его многопоточность - это просто запустить что-то в отдельном потоке, чтобы не стопорить основной.


            1. Alexandroppolus
              14.01.2024 21:32
              +1

              Обычные объекты там копируются (сериализуются)


              1. Akuma
                14.01.2024 21:32
                +1

                А ArrayBuffer именно переносится the memory resource that it points to is literally moved between contexts in a fast and efficient zero-copy operation

                JS - это такое вот, ну че поделать.


              1. DarkCoder30 Автор
                14.01.2024 21:32
                -2

                Потому, что они должны копироваться.


            1. mayorovp
              14.01.2024 21:32

              Но это же JS. Ну все понимают, что в контексте его многопоточность - это просто запустить что-то в отдельном потоке, чтобы не стопорить основной.

              Ну вот когда я вижу утверждения вида "js - многопоточный язык", у меня появляется ощущение, что кто-то не понимает что есть многопоточность в контексте js.


              1. Akuma
                14.01.2024 21:32

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

                Учитывая, что эта возможность - проблема на проблеме и неудобством сверху приправлено...она просто есть и все тут. Иногда даже пользуются. Сам язык подразумевает однопоточное выполнение конечно. Для другого он просто не предназначен.


                1. DarkCoder30 Автор
                  14.01.2024 21:32
                  +1

                  «Он не многопоточный, но он позволяет запускать отдельные потоки» это как ?


                  1. Akuma
                    14.01.2024 21:32

                    Вот такой этот JS. Вроде есть, а вроде нет


                    1. DarkCoder30 Автор
                      14.01.2024 21:32

                      Нет, это не JS такой.