Чи гарантовано JavaScript однопоточним?


610

Як відомо, JavaScript є однопоточним у всіх сучасних реалізаціях браузера, але це вказано в будь-якому стандарті чи це просто традиція? Чи цілком безпечно вважати, що JavaScript завжди є однопоточним?


25
Напевно, у контексті браузерів. Але деякі програми дозволяють ставитися до JS як до верхівки верхнього рівня та надавати прив'язки для інших мов C ++. Наприклад, flusspferd (зв'язки C ++ для JS - AWESOME BTW) робив деякі речі з багатопотоковим JS. Це до контексту.
НГ.

11
Це потрібно прочитати: developer.mozilla.org/en/docs/Web/JavaScript/EventLoop
RickyA

Відповіді:


583

Це гарне запитання. Я б хотів сказати «так». Я не можу.

Як правило, у JavaScript вважається один потік виконання, видимий для сценаріїв (*), так що коли введений ваш вбудований сценарій, слухач подій або тайм-аут, ви залишаєтесь повністю під контролем, поки не повернетеся з кінця блоку або функції.

.

Однак насправді це не зовсім правда , підлі підлі.

Найпоширеніший випадок - це негайні події. Веб-переглядачі запускають їх одразу, коли ваш код робить щось, щоб викликати їх:

var l= document.getElementById('log');
var i= document.getElementById('inp');
i.onblur= function() {
    l.value+= 'blur\n';
};
setTimeout(function() {
    l.value+= 'log in\n';
    l.focus();
    l.value+= 'log out\n';
}, 100);
i.focus();
<textarea id="log" rows="20" cols="40"></textarea>
<input id="inp">

Результати log in, blur, log outна всіх, крім IE. Ці події не спрацьовують лише тому, що ви дзвонили focus()безпосередньо, вони можуть статися через те, що ви зателефонували alert(), або відкрили спливаюче вікно чи будь-що інше, що рухає фокус.

Це може призвести і до інших подій. Наприклад, додайте i.onchangeслухача і введіть щось у вхід до того, як focus()виклик розфокусує його, і порядок журналу є log in, change, blur, log out, за винятком Opera, де він є, log in, blur, log out, changeі IE, де це (навіть менш чітко) log in, change, log out, blur.

Аналогічно закликаючи click()елемент, який забезпечує його, викликає onclickобробник негайно у всіх браузерах (принаймні, це відповідає!).

(Я використовую on...тут властивості обробника прямих подій, але те саме відбувається addEventListenerі з attachEvent.)

Також існує маса обставин, за яких події можуть спрацьовувати, поки ваш код є потоком, незважаючи на те, що ви нічого не зробили, щоб його спровокувати. Приклад:

var l= document.getElementById('log');
document.getElementById('act').onclick= function() {
    l.value+= 'alert in\n';
    alert('alert!');
    l.value+= 'alert out\n';
};
window.onresize= function() {
    l.value+= 'resize\n';
};
<textarea id="log" rows="20" cols="40"></textarea>
<button id="act">alert</button>

Натисніть, alertі ви отримаєте модальне діалогове вікно. Більше не виконується сценарій, поки ви не відмовитесь від діалогу, так? Ні. Змініть розмір головного вікна, і ви потрапите alert in, resize, alert outдо текстової області.

Ви можете подумати, що неможливо змінити розмір вікна, поки модальне діалогове вікно не працює, але це не так: в Linux ви можете змінити розмір вікна на скільки завгодно; в Windows це не так просто, але ви можете це зробити, змінивши роздільну здатність екрана з більшого на менший, де вікно не вміщується, і змусити його змінити розмір.

Ви можете подумати, ну, лише resize(і, можливо, ще кілька подібних scroll) може запуститись, коли користувач не має активної взаємодії з браузером, оскільки сценарій є потоковим. І для одиночних вікон ви можете мати рацію. Але це все переходить до того, як ви робите сценарії між вікнами. Для всіх браузерів, окрім Safari, який блокує всі вікна / вкладки / кадри, коли будь-який з них зайнятий, ви можете взаємодіяти з документом з коду іншого документа, запускаючи в окрему нитку виконання та викликаючи будь-які пов'язані обробники подій для вогонь.

Місця, де події, які ви можете викликати, можуть бути збільшені, поки сценарій все ще є потоковим:

  • коли модальні спливаючі вікна ( alert, confirm, prompt) відкриті у всіх браузерах , але Opera;

  • під час роботи showModalDialogу веб-переглядачах, які його підтримують;

  • діалогове вікно "Сценарій на цій сторінці може бути зайнятий ...", навіть якщо ви вирішите дозволити сценарій продовжувати працювати, дозволяє такі події, як розмір і розмиття, щоб запускатись та оброблятись, навіть коли сценарій знаходиться посередині зайнятий цикл, крім опери.

  • деякий час тому для мене, в IE за допомогою модуля Sun Java Plugin, виклик будь-якого методу в аплеті може дозволяти повторювати події та сценарій. Це завжди була чутливою до часу помилка, і можливо, Sun не виправив її з цього часу (я, звичайно, сподіваюся).

  • певно, більше. Минув час, коли я перевірив це, і браузери набули складності з того часу.

Підводячи підсумок, JavaScript, як видається, більшості користувачів, як правило, має чітку єдину нитку виконання, керовану подіями. Насправді у нього такого немає. Не ясно, наскільки це просто помилка і скільки обдуманий дизайн, але якщо ви пишете складні програми, особливо це стосується перехресних вікон / сценаріїв кадрів, є всі шанси, що вони можуть вас вкусити - і в перерві, важкі для налагодження способи.

Якщо найгірше приходить до гіршого, ви можете вирішити проблеми з одночасністю, опосередкувавши всі відповіді на події. Коли подія приходить, опустіть її в чергу і розробіть її з чергою, щоб пізніше, у setIntervalфункції. Якщо ви пишете рамку, яку ви плануєте використовувати в складних додатках, це може бути хорошим кроком. postMessageсподіваємось, також заспокоїть біль перехресних сценаріїв у майбутньому.


14
@JP: Особисто я не хочу знати це відразу, бо це означає, що я повинен бути обережним, щоб мій код був повторним, щоб виклик мого коду розмиття не впливав на стан, на який покладається зовнішній код. Занадто багато випадків, коли розмивання є несподіваним побічним ефектом, щоб обов'язково наздогнати кожного. І на жаль, навіть якщо ви цього хочете, це не є надійним! IE спрацьовує blur після того, як ваш код поверне контроль до браузера.
bobince

107
Javascript є однопоточним. Припинення виконання на спостереженні () не означає, що потік подій перестає перекачувати події. Просто означає, що ваш сценарій спить, поки сповіщення на екрані, але він повинен тримати перекачування подій, щоб намалювати екран. Поки триває сповіщення, що працює насос подій, що означає, що цілком правильно продовжувати надсилати події. У кращому випадку це демонструє спільну нитку, яка може траплятися в javascript, але все це може бути пояснено функцією, просто додаючи подію до помпа для події, який буде оброблено в подальшому, а потім робити це зараз.
кругляки

34
Але, пам’ятайте, кооперативна нитка все ще є однопоточною. Дві речі не можуть відбутися одночасно, а це те, що дозволяє багатопрофільна різьба і вводить недетермінізм. Все, що було описано, є детермінованим, і це гарне нагадування про ці типи питань. Гарна робота над аналізом @bobince
chubbsondubs

94
Chubbard має рацію: JavaScript є однопоточним. Це не приклад багатопотокової, а скоріше синхронної розсилки повідомлень в одному потоці. Так, можна призупинити стек і продовжувати відправлення подій (наприклад, сповіщення ()), але види проблем з доступом, що виникають у справжніх багатопотокових середовищах, просто не можуть трапитися; наприклад, ви ніколи не матимете змінних значень зміни між тестом і негайно наступним призначенням, оскільки ваш потік не може бути довільно перерваний. Я побоююсь, що ця відповідь лише спричинить плутанину.
Kris Giesing

19
Так, але враховуючи, що функція блокування, яка очікує на введення користувача, може відбуватися між будь-якими двома операторами, у вас є потенційно всі проблеми узгодженості, які приносять вам потоки на рівні ОС. Незалежно від того, чи дійсно двигун JavaScript працює в декількох потоках ОС, мало значення.
bobince

115

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

Додайте до цього той факт, що HTML5 вже вказує Web Workers (явний, стандартизований API для багатопотокового коду JavaScript), введення багатопотокової передачі в основний Javascript буде здебільшого безглуздим.

( Зауважте іншим коментаторам. Хоча setTimeout/setIntervalподії завантаження HTTP-запиту (XHR) та події UI (клацання, фокусування тощо) створюють сильне враження про багатопотоковість - вони все ще виконуються за однією часовою шкалою - одна на час - тому навіть якщо ми не знаємо наперед їх порядок виконання, немає необхідності турбуватися про зміни зовнішніх умов під час виконання обробника подій, функцію з тимчасової передачі або зворотного виклику XHR.)


21
Я згоден. Якщо в браузері коли-небудь додається багатопотокова передача, це буде через явний API (наприклад, Web Workers) так само, як і з усіма необхідними мовами. Це єдиний спосіб, який має сенс.
Дін Хардінг

1
Зауважте, що є одиничний. головний потік JS, Але деякі речі запускаються у браузері паралельно. Це не просто враження багатопоточності. Запити фактично виконуються паралельно. Слухачі, яких ви визначаєте в JS, працюють один за одним, але запити справді паралельні.
Nux

16

Так, хоча ви все ще можете постраждати від деяких проблем одночасного програмування (переважно умови перегонів) при використанні будь-якого з асинхронних API, таких як setInterval та xmlhttp зворотні виклики.


10

Так, хоча Internet Explorer 9 буде компілювати ваш Javascript на окремому потоці, готуючись до виконання на головному потоці. Це, однак, нічого не змінює для вас як програміста.


8

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

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

Nodejs дотримувався підходу браузерів .

Однак двигун Rhino підтримує запущений js-код у різних потоках . Виконання не може мати спільний контекст, але вони можуть мати спільний обсяг. Для цього конкретного випадку в документації зазначено:

... "Rhino гарантує, що доступ до властивостей об'єктів JavaScript є атомарним через потоки, але не дає більше гарантій для сценаріїв, що виконуються в одному і тому ж просторі одночасно. Якщо два сценарії одночасно використовують однакову область, скрипти відповідальний за координацію будь-якого доступу до загальних змінних . "

Читаючи документацію Rhino, я роблю висновок, що хтось може написати api-javascript, який також породжує нові потоки javascript, але api буде специфічним для носорога (наприклад, вузол може породити лише новий процес).

Я думаю, що навіть для двигуна, який підтримує декілька потоків у javascript, має бути сумісність із сценаріями, які не враховують багатопотоковість чи блокування.

Скористаючись браузерами та nodejs, як я бачу це:

    1. Чи весь js- код виконується в одному потоці ? : Так.
    1. Чи може js- код викликати запуск інших потоків ? : Так.
    1. Чи можуть ці потоки мутувати контекст виконання js ?: Ні. Але вони можуть (безпосередньо / опосередковано (?)) Додавати до черги подій, з якої слухачі можуть мутувати контекст виконання . Але не обманюйте, слухачі знову запускають атомно по головній нитці .

Так, у випадку браузерів і nodejs (а можливо, і багатьох інших двигунів) JavaScript не є багатопотоковим, а самі двигуни .


Оновлення про веб-працівників:

Наявність веб-працівників додатково виправдовує те, що javascript може бути багатопотоковою, в тому сенсі, що хтось може створити код у javascript, який працюватиме на окремій потоці.

Однак: веб-працівники не переймаються проблемами традиційних потоків, які можуть поділитися контекстом виконання. Правила 2 і 3 вище застосовуються , але на цей раз потіковий код створюється користувачем (js-кодом) у javascript.

Єдине, що слід врахувати - це кількість породжених ниток, з точки зору ефективностіне одночасності ). Дивись нижче:

Про безпеку нитки :

Інтерфейс Worker породжує реальні потоки на рівні ОС, і уважні програмісти можуть бути стурбовані тим, що паралельність може спричинити «цікаві» ефекти у вашому коді, якщо ви не будете обережні.

Однак, оскільки працівники Інтернету ретельно контролюють точки зв’язку з іншими потоками, насправді дуже важко викликати проблеми з одночасністю . Немає доступу до незахищених компонентів або DOM. І вам потрібно передавати конкретні дані в і з потоку через серіалізовані об'єкти. Тож вам доведеться потрудитися, щоб викликати проблеми у вашому коді.


PS

Окрім теорії, завжди будьте готові до можливих випадків та помилок, описаних у прийнятій відповіді


7

JavaScript / ECMAScript розроблений для того, щоб жити в хостовому середовищі. Тобто JavaScript насправді нічого не робить, якщо середовище хоста не вирішить проаналізувати та виконати заданий скрипт та не надасть об’єкти середовища, які дозволяють JavaScript насправді бути корисним (наприклад, DOM у браузерах).

Я думаю, що дана функція або блок скриптів буде виконуватися по черзі, і це гарантовано для JavaScript. Однак, можливо, середовище хоста могло б виконати кілька сценаріїв одночасно. Або хост-середовище завжди може забезпечити об'єкт, який забезпечує багатопотоковість. setTimeoutі setIntervalє прикладами або, принаймні, псевдо-прикладами хост-середовища, що надає спосіб зробити певну сумісність (навіть якщо це не зовсім паралельність).


7

Насправді, батьківське вікно може спілкуватися з дочірніми або рідними вікнами або кадрами, у яких запущені власні потоки виконання.


6

@Bobince дає дійсно непрозору відповідь.

Окремлюючи відповідь Марра Ерлігссона, Javascript завжди є однопоточним через цей простий факт: Все в Javascript виконується за однією шкалою часу.

Це суворе визначення однопотокової мови програмування.


4

Ні.

Я йду проти натовпу тут, але нехай мати мене. Один сценарій JS призначений для ефективного однопотокового потоку, але це не означає, що його не можна інтерпретувати по-різному.

Скажімо, у вас є такий код ...

var list = [];
for (var i = 0; i < 10000; i++) {
  list[i] = i * i;
}

Це написано з розрахунком, що до кінця циклу в списку повинно бути 10000 записів, які є індексом у квадраті, але ВМ може помітити, що кожна ітерація циклу не впливає на інший, і повторно інтерпретувати за допомогою двох потоків.

Перша нитка

for (var i = 0; i < 5000; i++) {
  list[i] = i * i;
}

Друга нитка

for (var i = 5000; i < 10000; i++) {
  list[i] = i * i;
}

Я тут спрощую, тому що масиви JS складніші, ніж тупі шматки пам'яті, але якщо ці два сценарії здатні додавати записи до масиву безпечним способом, то до того часу, як обидва будуть виконані, це буде мати такий же результат, як і однопотокова версія.

Хоча я не знаю, що жоден VM виявляє паралельний код, подібний цьому, мабуть, він може з’явитися в майбутньому для JIT VM, оскільки він може запропонувати більшу швидкість у деяких ситуаціях.

Якщо взяти далі цю концепцію, можливо, що код може бути помічений, щоб ВМ знав, що перетворити в багатопотоковий код.

// like "use strict" this enables certain features on compatible VMs.
"use parallel";

var list = [];

// This string, which has no effect on incompatible VMs, enables threading on
// this loop.
"parallel for";
for (var i = 0; i < 10000; i++) {
  list[i] = i * i;
}

Оскільки веб-робітники приходять до Javascript, навряд чи ця ... гірша система коли-небудь з'явиться, але я думаю, що можна впевнено сказати, що Javascript є однопоточним традицією.


2
Більшість мовних визначень розроблені таким чином, щоб вони були ефективно однопоточні, але заявляють, що багатопотоковість дозволена до тих пір, поки ефект однаковий. (напр. UML)
jevon

1
Я маю згоду з відповіддю просто тому, що поточний ECMAScript не передбачає (хоча, мабуть, я думаю, що те ж саме можна сказати і для C) для паралельних контекстів виконання ECMAScript . Тоді, як ця відповідь, я стверджую, що будь-яка реалізація, яка має одночасні потоки, здатні змінювати спільний стан, є розширенням ECMAScript .
користувач2864740

3

Ну, Chrome багатопроцесорний, і я думаю, що кожен процес має власний код Javascript, але, наскільки код знає, він "однопоточний".

У Javascript немає жодної підтримки для багатопотокової передачі, принаймні не явно, тому це не має значення.


2

Я спробував приклад @ bobince з незначними модифікаціями:

<html>
<head>
    <title>Test</title>
</head>
<body>
    <textarea id="log" rows="20" cols="40"></textarea>
    <br />
    <button id="act">Run</button>
    <script type="text/javascript">
        let l= document.getElementById('log');
        let b = document.getElementById('act');
        let s = 0;

        b.addEventListener('click', function() {
            l.value += 'click begin\n';

            s = 10;
            let s2 = s;

            alert('alert!');

            s = s + s2;

            l.value += 'click end\n';
            l.value += `result = ${s}, should be ${s2 + s2}\n`;
            l.value += '----------\n';
        });

        window.addEventListener('resize', function() {
            if (s === 10) {
                s = 5;
            }

            l.value+= 'resize\n';
        });
    </script>
</body>
</html>

Отже, коли ви натискаєте Виконати, закрийте спливаюче повідомлення та зробіть "єдину нитку", ви повинні побачити щось подібне:

click begin
click end
result = 20, should be 20

Але якщо ви спробуєте запустити це в Opera або Firefox стабільно в Windows і мінімізувати / максимізувати вікно з спливаючим екраном на екрані, тоді буде щось подібне:

click begin
resize
click end
result = 15, should be 20

Я не хочу сказати, що це "багатопоточність", але якийсь фрагмент коду був виконаний не в той час, коли я цього не очікував, і тепер у мене зіпсований стан. І краще знати про цю поведінку.


-4

Спробуйте вкласти дві функції setTimeout всередину один одного, і вони будуть вести себе багатопотоково (тобто зовнішній таймер не чекатиме завершення внутрішнього перед виконанням його функції).


5
Хром робить це правильно, не знаю, де @James бачить, що це багатопотокове ...: setTimeout(function(){setTimeout(function(){console.log('i herd you liek async')}, 0); alert('yo dawg!')}, 0)(для запису, йо-дауг завжди повинен бути спочатку першим, потім консольний вихід журналу)
Tor Valamo
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.