Поводом для ревизии данного вопроса стало то, что я по сей день слышу от специалистов (в том числе позиционирующих себя как 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 для очистки памяти.
Для изучения архитектуры host-среды Chrome:
Выделенные системные потоки в большинстве случаев улучшили производительность, до тех пор пока вы не запустите ресурсоемкий алгоритм в главном потоке. Возможности запускать скрипты в отдельных потоках не было, но в 2009 году HTML5 спецификацией было представлено средство призванное решить данную проблему, но об этом далее в соответствующем разделе.
Node.js
Про node.js однозначно можно сказать, что это многопоточное приложение.
По умолчанию 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 определяет строгий порядок операций, связанных с их разрешением. Порядок выполнения имеет важное значение для детерминированной и надежной обработки данных и управления потоком выполнения.
В соответствии с этим порядком, обработчики промисов всегда выполняются асинхронно после выполнения всех предыдущих синхронных задач в текущем потоке выполнения.
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)
green_fenix
14.01.2024 21:32+2Так же, как и в Node.js отсутствуют:
XHR/Fetch, Worker, navigator
и т. д.Начиная с v21, fetch в ноде присутствует )
Sap_ru
14.01.2024 21:32+14Какая-то мешанина и точка так и непоставлена. Запросы-то параллельно выполняются, а сам JS от этого параллельным не стал. Если бы вы условным полингом готовность опрашивали в глухом цикле в одном потоке, то получили бы точно такой результат - OS многопоточна, JS-движок нет.
JS - однозначно однопоточен, но никто не запрещает запустить несколько изолированных окружений, если среза позволят. Эти изолированные окружения могут иметь очень ограниченные средства взаимодействия.
Даже без всяких worktkers, при условии доступа к необходимым функциям среды (ОС) можно тупо запустить второй интерпретатор и обмениваться с ним через pipes/sockets. И даже весьма просто обернуть это всё так, что от workers оно будет практически неотличимо. Вплоть до того что вообще никак не отличимо - просто несколько независимых интерпретаторов со взаимодействием через queues/mailslots.nin-jin
14.01.2024 21:32-1Я даже больше скажу, JS и дfже HTML c CSS всегда были такими "многопоточными", ибо можно запускать потоки через
window.open
. Правда домены при этом должны быть разные, иначе несколько окон будут работать в одном потоке со всем вытекающими. Или я не понимаю и это другое?19Zb84
14.01.2024 21:32+1Даже без всяких worktkers, при условии доступа к необходимым функциям среды (ОС)
Когда worker_threds небыло использовали
child_process.fork()
The
child_process.fork()
method is a special case ofchild_process.spawn()
used specifically to spawn new Node.js processes.https://nodejs.org/api/child_process.html#child_processforkmodulepath-args-options
Или я что то путаю ?
DarkCoder30 Автор
14.01.2024 21:32+7Я даже больше скажу, когда я предлагаю реализовать проект на $mol, на меня косо смотрят.
nin-jin
14.01.2024 21:32-5У вас какая-то нездоровая фиксация на $mol. Попробуйте закрыть этот гештальт, чтобы отпустило.
DarkCoder30 Автор
14.01.2024 21:32+3Параллельное выполнение запросов не делает JavaScript параллельным. Таковым, ровно как и асинхронным его делают возможности предоставляемые host-средой, в виде Event Loop и Worker, которые в браузере специфицируются HTML5 стандартом. Примером с запросами я хотел осветить, что JavaScript как платформа уже давно является многопоточным, и асинхронное API работает по многопоточной модели. ‘ограниченные средства взаимодействия.’ - продиктованы Memory Model.
19Zb84
14.01.2024 21:32+1Язык JavaScript не может и не должен иметь своего API для обслуживания событий или создания потоков, поскольку это задача host‑среды.
Вы так и написали. Я так и понял и думал что это точка.
По этому не понял это из комментария выше.
> Какая-то мешанина и точка так и непоставлена.DarkCoder30 Автор
14.01.2024 21:32+1JavaScript нельзя рассматривать в отрыве от host-среды, поскольку он не самодостаточный в вычислительном плане, так как: встраиваемый и скриптовый.
MountainGoat
14.01.2024 21:32Получается та же фигня, что и в Питоне? Потоки есть, но второй может начать работать только когда первый задумался над системным вызовом, не важно сколько ядер в системе?
ParaMara
14.01.2024 21:32Фигня та же, что и в Питоне - как запустятся потоки зависит от реализации. Если вместе с автором считать что вместо понятного слова «диалект» лучше сага о хостах…
Написано ясно - в node.ls нормально уже, например.
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"). Вот тут автор категорически неправ. Как и неправ в своём определении многопоточности и асинхронности (там вообще бред какой-то написан)
rubinstein
14.01.2024 21:32Мне кажется, что такое можно про любой язык программирования сказать. Запустите условный while(true) {} в основном потоке любого ЯПа и получите зависание.
Sap_ru
14.01.2024 21:32Этот не при чём. Разговор о том, можно ли вообще использую стандарт языка и стандартну библиотеку запустить второй поток не как независимый процесс (то есть с разделяемой памятью и параллельный исполнением потоков). Если вы можете запустить второй поток, иметь возможность обращаться из него к переменным или данным (к адресному пространству, в общем) других потоков, сделать в нём while( true) {} и программа не зависнет, то многопоточность есть.
Если этого сделать нельзя, то многопточности нет.
Chelyuk
14.01.2024 21:32Так-то оно так. Но если двигаться в этом направлении. То практически всё сегодня многопоточно. Потому что процессоры многоядерные и зачастую мультитредовые. Тогда можно поставить гипервизор, развернуть N операционных систем, поднять в них любую среду и написать приложение которое будет в этом всём работать. Ну или просто какой-нибудь Kubernetes развернуть. Только всё это будет сделано вовсе вовсе не средствами языка программирования. И некоторые операции станут сильно дорогими в плане времени.
Sap_ru
14.01.2024 21:32+2Поэтому поток от процесса (в данном контексте) отличается именно наличием доступа к общему адресному пространству (к переменным программы). А передавать данные между процессами сильно дорого: сериализация, десериализация, копирование - вот это всё.
onets
14.01.2024 21:32+3Если уж на то пошло - надо сначала определить признаки многопоточности. И то зависит от ОС и железа.
На одноядерном проце винда только делает вид, что она многопоточная - в реальности дает каждому процессу по чуть-чуть времени, переключая их. То есть это можно назвать эмуляцией многопоточности. И общепринятые «многопоточные языки» типа плюсов, Явы и си шарпа - работают внутри этой эмуляции.
С появлением многопроцессорных/многоядерных систем - появилась возможность сделать более настоящую многопоточность. Но только по кол-ву ядер. Если процессов больше чем ядер - опять эмуляция.
Следующим слоем абстракции идет средства языка по управлению многопоточностью - ну типа можно ли изнутри создать новый процесс или поток, переключать их, завершать и так далее. В общепринятых языках опять же это есть. В яваскрипте я так понимаю этого нет. Не то чтобы плюсы или Ява 100% многопоточные. Но они имеют больше признаков.
DarkCoder30 Автор
14.01.2024 21:32+1Вы правы, но я решил не уходить на такой низкий уровень в контексте JavaScript, ибо эта тема может претендовать на отдельную статью. Под словом поток не всегда подразумевается физической поток, сколько возможность выполнения более одной единицы кода одновременно, или же имитация этого.
В JS вы можете:
const worker = new Worker("worker.js"); // создать новый поток worker.terminate(); // завершить поток
Sap_ru
14.01.2024 21:32Это не поток. Это скорее "процесс", так как вас полностью раздельные адресные пространства - вы не можете обратиться к переменным и объектам других потоков не копируя их через неких механизм общей памяти.
Sap_ru
14.01.2024 21:32С многопоточностью всё относительно просто - должны быть отдельные потоки исполнения кода и возможность доступа к адресному пространству (считай переменным) друг-друга. Если вы загоните один из потоком в бесконечный цикл, то остальные должны продолжать работать
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
'ы не помешали бы. Но все серверные реализации эксплуатируют браузерные движки, и видимо там просто архитектурно не заложена такая функциональность, потому до сих пор и не добавили.Finesse
14.01.2024 21:32+1В JS я вижу порезанный Task (он же
Promise
)Promise это не какая-то магия. Это просто паттерн программирования, альтернатива колбэку.
Если говорить вашими терминами, то промис это совсем не Task, потому что он не имеет никакого отношения к Thread. Простая асинхронность в JS реализована через Event Loop; это ещё одна сущность, которую вы не упомянули.
вижу Process (он же
Worker
), и совсем не вижу ThreadWorker в 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.
chupasaurus
14.01.2024 21:32Worker в JS это как раз Thread. Process в JS реализован через
child_process.fork()
(в Node.js).С каких пор подпроцессы стали нитями?
chupasaurus
14.01.2024 21:32Сам запутался в ваших мыслях.
Finesse
14.01.2024 21:32Это 2 отдельных утверждения, разделённые точкой. Я не утверждаю, что подпроцесс (вы имеете ввиду форк?) является трэдом.
chupasaurus
14.01.2024 21:32Я не сразу понял, что вы исправляете коммент выше. Утро вечера не всегда мудренее (:
mayorovp
14.01.2024 21:32+3Worker в JS это как раз Thread.
У Worker все признаки отдельного процесса если не смотреть в реализацию, в частности отдельное адресное пространство.
Finesse
14.01.2024 21:32Судя по MDN и документации Node.js, воркер это трэд. По умолчанию память раздельная, но можно выделить общую через SharedArrayBuffer (в браузере тоже работает).
DarkCoder30 Автор
14.01.2024 21:32Поделитесь официальным источником на основании которого вы сделали вывод, что это процесс
slonopotamus
14.01.2024 21:32+1Не важно, является ли оно по факту процессом или нет. Важно какие способы взаимодействия допустимы.
DarkCoder30 Автор
14.01.2024 21:32-2Они специально ограничены, чтобы нивелировать сложность многопоточный разработки, собственно и асинхронность для этого используется.
DarkCoder30 Автор
14.01.2024 21:32-1Я же специально приложил ссылку на модель памяти. Изоляция не делает его процессом.
SergTsumbal
14.01.2024 21:32XMLHttpRequest, он выполнился асинхронно, а не параллельно. Там в корне непонимание как микротаски и макротаски работают в eventLoop’e.
crypto.pbkdf2, libuv, это подсистема ввода/вывода ноды, к языку она отношение имеет опосредованное. EventLoop однопоточный что в ноде, что в браузере. Отличаются реализацией, количеством типов очередей и порядком прохода, по сути.
DarkCoder30 Автор
14.01.2024 21:32-1У вас в корне неверное понимание того, что такое асинхронность. Асинхронность, как и многопточность это параллельная модель выполнения. Многопоточность это один и способов реализации асинхронности, перечитайте статью.
SergTsumbal
14.01.2024 21:32+2Что мне ваша «статья» если это вообще две разные несвязанные концепции. Асинхронность про последовательность, поток там один, это просто запросить задачу в стороннем сервисе и переключится на другую, переодически проверяя выполнилась ли первая. Многопоточность про параллельность, выполнять несколько задач одновременно в разных потоках. Это базовая база
DarkCoder30 Автор
14.01.2024 21:32Ну вам и статья не поможет, чтобы осознать очевидные вещи. Я могу в Java создать такую же асинхронность как и в JavaScript основываясь на потоках.
А концепции связаны тем, что подразумевают параллельные вычисления. Читайте общепринятое определение.
SergTsumbal
14.01.2024 21:32+1Только это будет параллельность, а не асинхронность, асинхронность это IO bound задача, а параллельность CPU bound. Вы ходите чтобы люди себе фейспалмами лбы отбивали?)
Sap_ru
14.01.2024 21:32-1Асинхронность в вашем понимание не означает параллельности выполнения!
Те же корутины (и из реализация в виде промисов JS) выполнятся абсолютно последоватльно!DarkCoder30 Автор
14.01.2024 21:32Промисы это не корутины, и они выполняются асинхронно в последовательности разрешения, и не параллельно. Статью перечитайте.
Sap_ru
14.01.2024 21:32-1А это не важно, как они рализованы. Они могут быть реализованы корутинами и ничего не изменится с точки зрения стандарта языка. То есть вообще ничего.
Пусть не корутины. Но они выполняются последовательного и это заложено в дизайн самого языка. Вы не можете реализовать их через потоки, так нарушите соглашения о вызовах. Точнее в случае реализации через истинные потоки вам придётся этими потомками эмулировать корутины. А всё потому, что в JS строго постулируется последовательность исполнения промисов и то, что они исполняются последовательно. Но при этом они несомненно могут исполнять асинхронные операции.
Так что все спекулации на счёт, что как язык JS однопоточность не постулирует идут мимо кассы, так как он очень даже постулирует, просто не явно.
Нет семантически корректного когда на JS, который бы демонстрировал истинную многопоточность. У вас всегда есть "главный" await, исполняемый код которого строго задаёт последовательность вообще всех возможных в программе промисов. А всё остальное уже платформозависимое и требует event loop..
Муа-ха-ха
DarkCoder30 Автор
14.01.2024 21:32Event Loop никакого отношения не имеет к языку.
SergTsumbal
14.01.2024 21:32Так же как и хост-объекты не имеют никакого отношения к языку, только нативные, js и без хост-объектов будет js, так что ваш вывод про «многопоточность» js опираясь на хост-объекты мягко говоря не верен
DarkCoder30 Автор
14.01.2024 21:32SergTsumbal
14.01.2024 21:32+1И? JavaScript вообще не обязан иметь хост-обьекты, чтоб быть JavaScript’ом, движок v8 вообще не привязан даже к eventLoop’у
Sap_ru
14.01.2024 21:32В JS скорее имеет, чем нет. Простой вопрос - каким образом в JS сделать самый первый await если await на глобальном уровне не допускается? Как запланировать выполнение промиса без await?
В этой части синтаксис JS без платформы просто не имеем смысла. А все реализации платформы предполагают event loop, и не могут быть реализованы иначе.DarkCoder30 Автор
14.01.2024 21:32-2JS БЕЗ ПЛАТФОРМЫ НЕ ИМЕЕТ СМЫСЛА, ЭТО СКРИПТОВЫЙ ЯЗЫК, БЕЗ СВОЕГО I/O.
Sap_ru
14.01.2024 21:32I/O не имеет отношения к многопоточности.
У вас в JS на уровне синтаксиса постулируется однопоточность. Напишите пример когда на JS, которых хотя бы потенциально с точки зрения грамматики языка мог бы быть многопоточным.
Из асинхронности там есть промисы в виде async/await, а сама спецификация ЯЗЫКА говорит о том, что все возможные в программе функции async исполняются строго ПОСЛЕДОВАТЕЛЬНО.
DarkCoder30 Автор
14.01.2024 21:32Скиньте официальный источник, который определяет, что такое «макротаски». Вы апеллируете мифами, которые придуманы в следствии упрощения учебных материалов.
Sap_ru
14.01.2024 21:32-2А вы сделайте самодостаточный JS-файл с использованием async, который гарантированно будет работать запускаться хотя бы на уровне синтаксиса. Т.е. в нём должен быть изначальный await.
SergTsumbal
14.01.2024 21:32Top-level await в ноде при использование .mjs с 2021 года. И он в стейджах es. Погуглите, много нового узнаете
Sap_ru
14.01.2024 21:32-1Там, где такой ситаксис разрешён, порядок исполнения блоков async и всего возможного дерева исполнения всех возможных в программе блоков async строго определён и постулируется, что все эти блоки будут исполнены последовательно. Т.е. даже теоретически многопоточность возможно только за счёт некоторых дополнительных библиотек конкретного окружения.
Не говоря уже о том, что на данный момент (скоро всё может измениться, но пока так) это всё будет являться нестандартным платформо-зависимым расширением языка.
Sap_ru
14.01.2024 21:32-1Я, собственно к тому, что если вы постараетесь написать синтаксически корректный код на JS, даже с использованием каких-то перспективных версий языка, но без использования расширений, предоставляемых конкретным окружением (и не имеющих отношения к самому языку), то окажется, что порядок исполнения всех блоков async будет последовательным и будет строго задан грамматикой языка. То есть сам язык истинной многопоточности не предоставляет, а уж при помощи какой-то специфичной платформы вы совершенно любой язык может сделать как однопоточным, так и многопоточным либо степени многопоточности.
mayorovp
14.01.2024 21:32+1"Макротаска" - это задача (Job), поставленная в очередь операциями 9.5.4 HostEnqueueGenericJob или 9.5.6 HostEnqueueTimeoutJob
speecc
14.01.2024 21:32А откуда вы вообще берете эти 'макротаски' ? Если в спеке нет такого определения
werevolff
14.01.2024 21:32У вас здесь что, секта свидетелей многопоточного JS? https://developer.mozilla.org/en-US/docs/Web/API/HTML_DOM_API/Microtask_guide
DarkCoder30 Автор
14.01.2024 21:32-1Единственным официальным источником, с 1998 года является сайт спецификации ECMAScript.
nin-jin
14.01.2024 21:32-1DarkCoder30 Автор
14.01.2024 21:32Мы рассматриваем фундаментальный вопрос. На чем, кроме как не на официальной спецификации должны строиться наши суждения ?
nin-jin
14.01.2024 21:32+1Есть разные источники информации: от базовых знаний компьютерных наук, до откровений разработчиков интерпретаторов и реверс-инженеринга.
werevolff
14.01.2024 21:32+2-- Советский Союз официально не распался
-- Российская Федерация является коммерческой организацией, зарегистрированной в США.
-- Единственным официальным источником, с 1998 года является сайт спецификации ECMAScript.
Ничего из вашей персональной Книги Мормона не упустил? Думаю, с вашими безусловными и недоказуесыми высказываниями, вам нужно заниматься политикой, а не IT. Mozilla Foundation уже много лет работает над стандартами JS и HTML. EcmaScript - это основа для языка JS. Сам по себе он не имеет хост-сред для исполнения.
И, более того, EcmaScript не эквивалентен JavaScript. Когда вы говорите, что JS многопоточен потому, что ES не имеет ограничений на создание потоков ОС, то вы элементарно путаете ES и JS.
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.
werevolff
14.01.2024 21:32-1EcmaScript - это не язык, а спецификация для создания скриптовых языков. JavaScript - это реализация EcmaScript. То есть, когда мы говорим о JavaScript, то мы имеем ввиду EcmaScript + детали реализации. Такие как микро и макро таски. В EcmaScript их нет, а в JavaScript они есть. Но статья про JavaScript, а не про EcmaScript, не так ли? Поэтому, утверждения автора о том, что в JS нет макро и микро тасок только потому, что в спецификации ES их нет, это чистой воды шизофрения. Просто господина техлида в комментах разжаловали до middle, вот он и бесицо. Вы тоже подменяете понятия, или недостаточно хорошо знаете Английский, поскольку is used to refer to переводится как "используется для отсылки на", а не "Эквивалентен".
mayorovp
14.01.2024 21:32EcmaScript - это не язык, а спецификация [...]
Э-э-э, а что у языка есть кроме спецификации-то?
Почему какой-нибудь встроенный в приложение lua остаётся lua, встроенный в приложение python остаётся python, а встроенный в браузер EcmaScript превращается в JavaScript?
JavaScript - это реализация EcmaScript.
citation needed
Такие как микро и макро таски. В EcmaScript их нет, а в JavaScript они есть.
В EcmaScript они есть, только называются по-другому (Generic Job, Promise Job и Timeout Job).
Вы тоже подменяете понятия, или недостаточно хорошо знаете Английский, поскольку is used to refer to переводится как "используется для отсылки на", а не "Эквивалентен".
Под эквивалентностью я понимал отсутствие дополнительных смыслов, и то что одно из названий - официальное, а второе "используется для отсылки", этому не противоречит.
werevolff
14.01.2024 21:32Не понимаю вас вы хотите сказать, что спецификация и язык - это одно и то же? Я так понимаю, спецификация - это документ, а язык программирования - это набор лексем, которые могут быть переведены в команды для их запуска на вычислительной технике. PEP - это, как бы, тоже не python, а набор соглашений. Или, если угодно, спецификация.
mayorovp
14.01.2024 21:32"Набор лексем, которые могут быть переведены в команды для их запуска на вычислительной технике" - это программа, а не язык программирования.
nin-jin
14.01.2024 21:32-1https://en.m.wikipedia.org/wiki/ECMAScript
Забавный факт: JavaScript никогда не поддерживал ECMAScript4, в отличие от ActionScript.
vanxant
14.01.2024 21:32+2Хотя в спецификации js воркеры называются потоками (worker threads), на самом деле это процессы по своей сути, поскольку не имеют общей памяти и общаются друг с другом только через сообщения (ну и ещё перехват асинхронных вызовов).
Если заглянуть под капот движков, это всё сделано совершенно намеренно. JS принципиально однопоточный, чтобы не обкладыватся примитивами синхронизации между потоками и не тормозить весь веб ради пары тяжёлых приложений.
DarkCoder30 Автор
14.01.2024 21:32-2Они имеют общую память. И то, что они по вашему её не имеют - это не делает их процессами.
SergTsumbal
14.01.2024 21:32+1По умолчанию если не шарить память то не имеют они общую память, а если шарить то приходиться решать проблему с синхронизацией
vanxant
14.01.2024 21:32+2Нет, конечно, разберитесь сначала о чём пишете. Общая память в потоках это когда вы объявляете произвольную переменную, возможно со словом synchronized, и дальше пользуетесь ей как обычно во всех потоках. Приседания с SharedArrayBuffer - это обычный механизм памяти, разделяемой между процессами.
DarkCoder30 Автор
14.01.2024 21:32-1Ну так это было специально сделано, чтобы нивелировать сложности многопоточного программирования.
SergTsumbal
14.01.2024 21:32+2Что специально сделано? Многопоточное программирование в той же ноде зачаточное и не простое, race condition, deadlock, livelock, синхронизация, critical section вам знакомы?
Lexicon
14.01.2024 21:32Очень намешали, не соглашусь с заявлением.
Когда говорят JS "язык X типа", предполагается, что возможности языка определяет спецификация, в этом смысле ecmascript синхронный язык с поддержкой асинхронных операций и работой в одной потоке.EvenLoop действительно реализуют почти все среды, но у JS нет власти над спецификой его реализации, вам ничего не помешает взять Java-Nashorn и нагородить беспредел. JS так же остается синхронным, у вас не появляется способа управлять работой потока.
Workers это отдельная тема, кто-то мог бы заметить, что они мало отличаются от 3 nodejs запущенных параллельно с общением через fs, но даже тогда, они "хуже", медленнее и неэффективнее основного процесса, скажем, в сетевых операциях. Однако, - это и правда многопоточность.
Делает ли это JS многопоточным? На мой взгляд нет, как класс решения задач с потребностью в многопоточности вы не выберете NodeJS. А вот для веб-сервера выберете и локальные задачи многопоточности решить сможете.
Имхо семантика вопроса очень скучна и очень жаль, если кто-то читает эту статью, чтобы задавать вопросы или давать ответы на собеседованиях.
DarkCoder30 Автор
14.01.2024 21:32+1Вы точно внимательно прочитали статью?
Повторяю, JavaScript не должен быть самодостаточным в вычислительном плане! Это встраиваемый и скриптовый язык для управления тем, что предоставляет ему HOST-среда. HOST-среда предоставляет возможность создания тредов. Возьмите Java-апплеты и выполните в браузере, и у вас не будет возможности нагородить беспредел.Lexicon
14.01.2024 21:32А кто вам сказал, что должен, вы комментарий то прочитали, где этот тезис вообще взяли, чтобы его опровергать?
Я только за эффективность дискутирую, с точки зрения эффективности создания продукта, нода уступает альтернативам в управлении своим или другими потоками. Говоря о V8, Воркеры реализованы заведомо без врожденной NodeJS магии относительно работы http сервера например, это означает, что потоки для этих целей вы поднимать не захотите. В добавок, это все же свойства платформы, а не языка - шагнули в edge-compute, - нужен другой подход.
К чему пасс про Netflix и Twitch не понял, вы уж раскройте мысль
DarkCoder30 Автор
14.01.2024 21:32-1Расскажите разработчикам таких платформ как Netflix или Twitch, что JS - однопоточный.
debagger
14.01.2024 21:32Поводом для ревизии данного вопроса стало то, что я по сей день слышу от специалистов (в том числе позиционирующих себя как senior), что современный JavaScript является однопоточным.
Эти понятия - "однопоточный"/"многопоточный" вообще неприменимы к языкам программирования. Так что, из-за чего вообще весь сыр-бор? Что кто-то интуитивно, и неправильно, пользуется терминами?
Услышите в следующий раз что кто-то так говорит, попросите привести пример многопоточного ЯП. Будет интересно )))
nameisBegemot
14.01.2024 21:32Едва ли можно найти интерпретируемые яп многопоточные. Истинно многопоточные. Всё они работаю схоже - загружают в стек свои инструкции построено. И чтобы это не развалилась, нужна потокобезопасность.
Либо за счёт сложных условных конструкций. Или проще - запретов мнонопотосности.
slonopotamus
14.01.2024 21:32+1Ruby, Python. В обоих на уровне дизайна языка никаких ограничений на многопоточность нет (в отличие от JS), и есть апи для создания потоков. И в частности JRuby очень даже многопоточный.
Sap_ru
14.01.2024 21:32А как разница инпретериуются инструкции или байт код? Исполнительные потока Java пока JIT не включился внутри очень и очень похожа на Python. Более того, в Python тоже есть байт код. Единственная разница в том, что Java не требует тащить с собой компилятор и dev-библиотеки. Но в Java можно сделать среду, которая сможет компилировать исполнять произвольных код runtime, а Python никто не запрещает откомпилировать (привет PyPy)
Finesse
14.01.2024 21:32+1Лично я так и не понял первые две проблемы. В случае с Callback Hell вместо того, чтобы инициализировать анонимные функции в месте их использования, можно было вынести их инициализацию выше и воспользоваться композицией, при этом задав функциям понятный идентификатор.
Деанонимизация колбэков погоды не меняет. Проблема в том, что использование колбэков вынуждает писать код, который сложно осознавать, особенно когда алгоритм сложнее чем простое последовательное выполнение действий.
Касательно Zalgo вполне достаточно отделять системный код от прикладного.
Причём здесь степень пракладности кода? Проблема может возникнуть где угодно.
werevolff
14.01.2024 21:32+4Таким образом асинхронным или многопоточным JavaScript делает именно host‑среда.
Вы же сами дальше пишете разные определения асинхронности и многопоточности (причëм, к определению асинхронности есть претензии). Это разные вещи. Но ваше предложение можно интерпретировать так, что это одно и то же.
Теперь выполним их в асинхронном режиме передав
true
в метод.open()
третьим аргументом.Результат показывает, что они выполнились параллельно
А я вот открыл документацию https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/open и прочитал, что третий аргумент инициирует не параллельный, а именно асинхронный запуск. Вы бы разобрались для начала, чем асинхронное выполнение отличается от многопоточного запуска.
Например, есть главный поток, и несколько рабочих потоков
Вы можете рассказать, что такое "поток"?
DarkCoder30 Автор
14.01.2024 21:32+1Мое предложение нельзя интерпретировать так, что это одно и тоже. Перечитайте статью второй раз. Асинхронность не ровно многопоточность, но и то и другое это модель параллельного программирования. Многопоточность может быть одними из способов реализации асинхронности.
SergTsumbal
14.01.2024 21:32+5Параллельное исполнение
Параллельное исполнение (parallel computing) подразумевает наличие более одного вычислительного устройства (например, процессора), которые будут одновременно выполнять несколько задач.
Параллельное исполнение - это строгое подмножество конкурентного исполнения. Это значит, что на компьютере с одним процессором параллельное программирование - невозможно;)
Многопоточность
Многопоточность - это один из способов реализации конкурентного исполнения путем выделения абстракции "рабочего потока" (worker thread).
Потоки "абстрагируют" от пользователя низкоуровневые детали и позволяют выполнять более чем одну работу "параллельно". Операционная система, среда исполнения или библиотека прячет подробности того, будет многопоточное исполнение конкурентным (когда потоков больше чем физических процессоров), или параллельным (когда число потоков меньше или равно числу процессоров и несколько задач физически выполняются одновременно).
Асинхронное исполнение
Асинхронность (asynchrony) подразумевает, что операция может быть выполнена кем-то на стороне: удаленным веб-узлом, сервером или другим устройством за пределами текущего вычислительного устройства.
Основное свойство таких операций в том, что начало такой операции требует значительно меньшего времени, чем основная работа. Что позволяет выполнять множество асинхронных операций одновременно даже на устройстве с небольшим числом вычислительных устройств.
DarkCoder30 Автор
14.01.2024 21:32Один процессор имеет не одно ядро. Одно ядро имеет не менее двух потоков. Если мы не говорим об Pentium 1.
SergTsumbal
14.01.2024 21:32+2А eventLoop однопоточный, поэтому параллельность в нем невозможна, только асинхронность
DarkCoder30 Автор
14.01.2024 21:32Эмм, я и не утверждаю, что eventLoop многопоточный, это вообще как бы разные вещи.
SergTsumbal
14.01.2024 21:32+2Так вы не знаете что eventLoop отвечает за выполнение кода? И как следствие javascript однопоточен по природе?
DarkCoder30 Автор
14.01.2024 21:32Вы вообщем о чем ? У нас предмет разговора это многопоточность, а не event loop. Каждый тред имеет свой event loop.
Sap_ru
14.01.2024 21:32А у вас в JS нет тредов :))) А значит нет многооточности :) Именно на уровне языка нет.
DarkCoder30 Автор
14.01.2024 21:32На уровне языка нету и основного потока, который делает его однопоточным. Читайте что такое скриптовый язык…
Sap_ru
14.01.2024 21:32Это уже совсем глупость. Язык может "скриптовым" и быть истинно многопоточным.
Вам показать истинную многопоточность на уровне потоков OS на 100% "скриптовом" Python?
"Скриптовость" языка это вообще некорректное понятие, имеющее смысл лишь по отношению к конкретной платформе и конкретному окружению. Тот же JS я вам могут откомпилировать в чистый бинарный код, и это никаким образом не повлияет на возможности языка и особенно на много поточность.
SergTsumbal
14.01.2024 21:32+1Он к скриптовым относился лет 15 назад, нет многопоточной реализации eventLoop’а в природе
werevolff
14.01.2024 21:32+3Я вам помогу: асинхронность - это не то, что вы подумали. Асинхронность - это наличие в одном потоке неблокирующих задач, точное время выполнения которых неизвестно. Три ключевых фактора: один поток, порождающий таски, отсутствие взаимных блокировок и неизвестное время выполнения.
Дело в том, что I/O операции не превращаются в CPU-Bound операции. Они просто ждут некоего результата ввода-вывода. Например. если вы запустили таймер, то таска будет ждать, пока не настанет определённый момент, и только тогда сможет продолжиться. Разумеется, если её не блокируют соседние таски. И в вашем примере с вызовом XMLHttpRequest запросы идут не параллельно. Просто каждая таска в EventLoop отправляет запрос и становится на паузу, ожидая ответ сервера. Пока она на паузе, другие таски поочерёдно накидывают запросы на другие эндпоинты и тоже становятся в ожидание. Эти таски исполняются не паралельно, а асинхронно. То есть, в одном потоке, не блокируя друг друга. А вот когда ответ придёт, они выполнятся последовательно, в соответствии с очерёдностью ответов от сервера. Можете сами проверить. Пока одна таска будет обрабатывать свой ответ, другие, или часть других не исполнятся.
В отличие от асинхронных задач, параллельные задачи могут блокировать друг-друга, обращаясь одновременно к одному и тому же разделяемому ресурсу. А также, они могут конкурировать за процессорное время или не конкурировать, если запускаются на многоядерном процессоре или на нескольких процессорах одновременно. Параллелизм, в отличие от асинхронности, предполагает, что задачи выполняются в пересекающихся промежутках времени. То есть, в асинхронном программировании мы предполагаем, что задача встаёт на паузу и будет выполнена тогда, когда изменятся внешние обстоятельства (прилетит ответ с сервера). А в параллельном или конкурентном программировании мы считаем, что одинаковые задачи выполнятся почти одновременно или полностью одновременно (что, правда, невозможно, но с некоторой погрешностью мы говорим об одновременности выполнения). Разумеется, если они не будут сильно блокировать друг-друга при доступе к разделяемым ресурсам.
И, в завершение: асинхронность, в большинстве случаев, предполагает запуск задач в одном потоке. Мультипоточность, в большинстве случаев, основана на механизме ОС создания потоков в процессе. Но число потоков в процессе не ограниченно. Поэтому, например, если процессу выделено 2 ядра многоядерного процессора, но он породил 8 потоков, то в один и тот же момент времени активными будут только 2 потока. Проблема в том, что JS не предоставляет средств для того, чтобы забронировать себе ядра процессора. Поэтому, с высокой долей вероятности, для браузера это будет одно ядро на приложение, которое, скорее-всего, будет расшарено с другими системными приложениями и вопрос исполнения будет решаться ОС. В теории, можно попробовать получить доступ к вычислительным ядрам видеокарты, коих там довольно много, и тогда можно добиться истинного параллелизма.
werevolff
14.01.2024 21:32Не согласен. Думаю, что ваше предложение будут так интерпретировать. С утверждением о том, что многопоточность может быть одним из способов реализации асинхронности, согласен лишь частично. Формально, может. Если не рассматривать разницу между операциями, требующими ожидания I/O и операциями, требующими вычислений. Но тогда не совсем понятно, чем параллелизм отличается от асинхронности?
DarkCoder30 Автор
14.01.2024 21:32Отличается тем, что выполнение асинхронных операций инициированых главным потоком будет заниматься параллельно «черный ящик», который сообщит о готовности результата.
werevolff
14.01.2024 21:32Образ ваших мыслей несколько... спутан. Вы путаете асинхронность, параллелизм и конкурентность. Вам бы, простите, с матчастью ознакомиться и внести небольшие изменения в статью.
DarkCoder30 Автор
14.01.2024 21:32-1Позвольте, там написано, что асинхронность это не многопоточность. То, что многопоточность может быть как конкурентной, так и параллельной это очевидно еще до матчасти, но это тема отдельной статьи. В данном случае вы фактически в главном потоке запускаете множество IO-Bound операций, выполнение которых распределяется по потокам в рамках которых эти операции являются CPU-bound. Распределением занимается host-среда, что и предоставляет вам асинхронность.
werevolff
14.01.2024 21:32+1множество IO-Bound операций, выполнение которых распределяется по потокам в рамках которых эти операции являются CPU-bound
Это как? Что считает процессор, пока host-среда ждёт ответа от сервера?
SergTsumbal
14.01.2024 21:32Ну мало ли, факториалы про запас насчитывает)
werevolff
14.01.2024 21:32Лучше крипту майнить, чтобы ресурс не пропадал. Благо, JS майнеры - не редкость в наши дни.
DarkCoder30 Автор
14.01.2024 21:32А выполнение самого запроса вы за задачу требующую вычислительных ресурсов не считаете ?
werevolff
14.01.2024 21:32Сам запрос выполняет сервер. Когда приходят ответы, они будут обработаны последовательно. Если только в процессе их обработки не возникнет других задач, требующих ожидания ввода/вывода.
DarkCoder30 Автор
14.01.2024 21:32-1Как бы браузер открывает TCP-соединение с сервером, которое требует вычислительных ресурсов, он пишет в сокет http-запрос, в то время сервер читает этот запрос и пишет в сокет ответ браузеру. Они оба его выполняют.
SergTsumbal
14.01.2024 21:32+1Нет конечно, и там нет никакого «выполнения», там ожидание ответа, те же сетевые задержки, что полностью IO, во время ожидания можно выполнить другие задачи, называется неблокирующий асинхронный IO. Поднимать потоки на такое слишком затратно. libuv поднимает пулл на тяжелые синхронные операции, для справки. Легко проверяется оборачиванием любых синхронных функций в Promise.all, нет задержек, нет выигрыша
DarkCoder30 Автор
14.01.2024 21:32-2Откровенная чушь, читайте документацию хрома. Больше я на это отвечать не вижу смысла.
SergTsumbal
14.01.2024 21:32+1Уровень у вас не тот чтоб что-то отвечать, это верно. Тут базу надо и матчасть учить, что вам уже многие сказали, и не один раз
DarkCoder30 Автор
14.01.2024 21:32-2Идите учить матчасть.
DarkCoder30 Автор
14.01.2024 21:32Это доказывает, что выполнение асинхронных операций host-среда выполняет в отдельных потоках.
mayorovp
14.01.2024 21:32Это доказывает существование других потоков, но не доказывает что операции выполняются в них блокирующим образом (и уж тем более CPU-bound образом!)
SergTsumbal
14.01.2024 21:32+1Хотите через сборщик мусора запустить параллельно код на дважавскрипте или что?
DarkCoder30 Автор
14.01.2024 21:32Я поражен вашей наблюдательностью. Действительно, я показываю исходники браузера, а не v8.
ErshoffPeter
14.01.2024 21:32+2Та самая статья из-за которых сижу на Хабре и перелопачиваю сотни постов
шлака!Спасибо большое автору и комментаторам!
Сам балуюсь JS время от времени и ломаю голову и над асинхронностью и над всем остальным, что упомянуто в статье.
Akuma
14.01.2024 21:32+1Какая-то мешанина всего подряд. Даже древний XMLHttpRequest вспомнили (который кстати не многопоточный, а асинхронный).
Современный JavaScript позволяет запускать отдельные потоки и выполнять в них код (как в браузере, так и в NodeJS). Если явно этого не делать, он однопоточный. Асинхронность - не многопоточность. Конец.
Ии...такое определение подходит под любой язык, честно говоря. Не видел ни одного языка, который бы "сам" создавал отдельные потоки. Всегда это делает программист явно. Поэтому вопрос должен бы звучать иначе: "Можно ли в JS создать отдельный поток?" Можно.
DarkCoder30 Автор
14.01.2024 21:32Асинхронность не ровно многопоточность.
Асинхронность можно реализовать за счет многопоточности. Асинхронность как и многопотночночность это параллельная модель выполнения…
yrub
14.01.2024 21:32+1что-то тут все жонглируют терминами и словами.. асинхронность никакого отношения к многопоточности не имеет, асинхронность это про модель control flow вашей программы.
параллельность - выполнение нескольких задач в один и тот же момент времени, возможно только при наличии нескольких ядер и т.д. (есть еше термин "конкурентность", когда задач больше чем процессоров, но сейчас это не важно).
просто когда у вас код асинхронный, то получается явным образом указано, что между отдельными блоками и строками программы нет явной зависимости по синхронному (синхронизированному, последовательному) выполнению, соответственно у выполняющей среды есть достаточно информации, чтобы выполнить кусок кода параллельно с другим кодом, но совсем не факт что так и будет, собственно как всегда и было в nodejs.
Асинхронность как и многопотночночность это параллельная модель выполнения…
так что нет, асинхронность никакая не модель параллельного выполнения, это просто модель выполнения.
ЗЫ: к слову ваши замеры времени ничего не доказывают, и по ним ничего конкретно сказать нельзя. 33, 37, 37, 37, 63. - это легко может выполняться в одном потоке, например первый раз код отрабатывал 33, т.к. надо было jit сделать и другие дела, потом фактически время выполнения было 0 и в конце еще раз подвисло, например на сборку мусора. палкой потыкали, но если знаний о внутренности системы 0, но лучше не делать гипотез о ее работе и перформансу, это золотое правило перформанс тестов и оптимизации.
SergTsumbal
14.01.2024 21:32Замеры через new Date весьма наивная штука, в ноде для этого есть perf_hooks, libuv которые пристегнут к v8 и отвечает за io, по дефолту 4 потока использует, только это особенность ноды, а не языка
DarkCoder30 Автор
14.01.2024 21:32-1Честно, это не возможно комментировать, так как это фундаментально неверно. Если руководствоваться такой логикой, выходит следующее:
Тот «один» поток, который делает JavaScript «однопоточным» - это также не особенность языка. Поскольку это скриптовый язык, который не может самостоятельно иметь своего потока выполнения, он представляется host-средой.
SergTsumbal
14.01.2024 21:32Я бы на вашем меньше «фундаментально» удалил статью, чтобы не быть посмешищем, javascript спокойно может работать на своих нативных объектах, он не обязан иметь libuv. И все языки сценариев являются языками программирования, почему вы постоянно подчеркиваете слово “script”, javascript уже очень давно не просто скриптовый браузерный язык
werevolff
14.01.2024 21:32Статья, кстати, нормальная. Автору бы устранить путаницу с асинхронным выполнением и многопоточным. Но он упрямо продолжает спорить, хотя ему уже разжевали всю суть.
DarkCoder30 Автор
14.01.2024 21:32-2Это мне приходится вам суть разжевывать, что мне вовсе неинтересно делать...
SergTsumbal
14.01.2024 21:32+1Там фейспалм через предложение, просто надерганы термины, смысл которых до конца не понят, из это собран какой-то мирок на основе которого сделаны какие-то странные выводы. Классический эффект Даннинга-Крюгера
werevolff
14.01.2024 21:32Типичный начинающий миддл. Страдает проблемами неофитства. Начал что-то понимать, но контролировать процесс не может.
MaNaXname
14.01.2024 21:32А прикиньте он в профиле написал тех лид. Я тут на хабре читал много статей мол за 3 года в сениоры и такой - хах, вот сказки придумывают. Но прочел эту и очень задумался...
DarkCoder30 Автор
14.01.2024 21:32-1Исходники научитесь читать хотя бы.
yrub
14.01.2024 21:32+1это также не особенность языка. Поскольку это скриптовый язык, который не может самостоятельно иметь своего потока выполнения, он представляется host-средой.
и это вы тоже часто пишете с непонятным упором. язык это набор спецификаций (как программа должна выполняться) и правил грамматики (как ее распарсить). далее вопрос что в этих спецификациях, подразумеваю что ничего про параллельность. для примера в java есть отельно документ по спецификациям языка и по спецификациям виртуальной машины (как читать байткод и какой должна быть архитектура виртуальной машины). я вот загуглил, именно в спецификациях языка идет разговор про "happens before" и прочие вещи которые вылезают при конкурентном выполнении программ. т.е. на уровне спецификаций языка оговариваются моменты, которые имеют смысл только если у вас код выполняется в многопоточной среде и в vm есть настоящие потоки. Так что можно идти от обратного - если в спеках js подобного нет, то его можно считать однопоточным ну или хотя бы условно многопоточным. вообще сделать многопоточную vm не так то и просто как показывает практика, ни руби ни пайтон за 15+ лет так и не осилили убрать из машины GIL, видимо опять таки в спецификациях языка необходимо продумать сложные моменты ну и реализовать еще все это. наверно глядя на фронт работ никто не взялся, понимая что это все равно будет работать раз в 20 медленней чем java и поэтому пошли в другую нишу.
DarkCoder30 Автор
14.01.2024 21:32К слову там рядом ссылка прикреплена на документацию Libuv, которая подтверждает мои слова, поэтому можете не основываться на моем примере.
mayorovp
14.01.2024 21:32Только вот потоки разделяют общую память, а воркеры нет.
Alexandroppolus
14.01.2024 21:32+1А как же SharedArrayBuffer?
mayorovp
14.01.2024 21:32+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
Я б не заморачивался. Оно позволяет запускать код в отдельном потоке? Позволяет. Все, можно считать это обычным потоком.
mayorovp
14.01.2024 21:32+1Я бы тоже не заморачивался, но у этой классификации есть последствия.
Я не могу в JS так просто взять и расшарить объект между двумя воркерами, я обязан использовать либо передачу сообщений, либо сырую общую память. С другой стороны, я могу не бояться что кто-то случайно расшарит объект который нельзя шарить.
В то же время, в языках которые поддерживают многопоточность, я могу расшарить объект и вынужден внимательно следить чтобы не расшарить ничего случайно.
DarkCoder30 Автор
14.01.2024 21:32-1Это сделано специально, чтобы не обременять разработчиков трудностями многопоточного программирования. Только что это меняет?
Kahelman
14.01.2024 21:32Erlang вам в помощь. Попробуйте что нибудь расшарить. Он точно многопоточный и все построено на передаче сообщений. Так что не аргумент :)
slonopotamus
14.01.2024 21:32+1https://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).
Sap_ru
14.01.2024 21:32-2Поток от процесса отличает возможностью произвольного доступа к данным других потоков без необходимости копирования данных через общую память.
В Erlang архитектурно сделаны потоки. О чём они сами говорят в спецификации языка, так как знают разницу между потоками и процессами.
Akuma
14.01.2024 21:32+1Ну можно, например, сделать так https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Transferable_objects с некоторыми объектами.
Но это же JS. Ну все понимают, что в контексте его многопоточность - это просто запустить что-то в отдельном потоке, чтобы не стопорить основной.
Alexandroppolus
14.01.2024 21:32+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 - это такое вот, ну че поделать.
mayorovp
14.01.2024 21:32Но это же JS. Ну все понимают, что в контексте его многопоточность - это просто запустить что-то в отдельном потоке, чтобы не стопорить основной.
Ну вот когда я вижу утверждения вида "js - многопоточный язык", у меня появляется ощущение, что кто-то не понимает что есть многопоточность в контексте js.
Akuma
14.01.2024 21:32Я не говорил, что он многопоточный. Я сказал, что он позволяет запускать отдельные потоки.
Учитывая, что эта возможность - проблема на проблеме и неудобством сверху приправлено...она просто есть и все тут. Иногда даже пользуются. Сам язык подразумевает однопоточное выполнение конечно. Для другого он просто не предназначен.
DarkCoder30 Автор
14.01.2024 21:32+1«Он не многопоточный, но он позволяет запускать отдельные потоки» это как ?
19Zb84
Спасибо. Очень интересная статья. А эта статья основанна на информации из интернета или есть книги, которые использовались для создания статьи ?
Kaban_Kanban
Думаю автор пользуется личным опытом
DarkCoder30 Автор
Благодарю! Эта статья основана на официальной спецификации языка.