Откуда ноль ? Многозадачное в однозадачном

Ikemefula Ikemefula
Есть некоторый кусочек кода(ниже). Это олдскульный node, фактически — тот самый однопоточный JavaScript.
Итого — пусть N читателей-писателей работают с глобальным ресурсом по протоколу read-write. Для простоты, глобальный ресурс есть просто файл. Каждый читатель считывает его целиком. Каждый писатель перезаписывает целиком.
Цикл работает примерно так
— читаем контент из файла ('i')
— конвертируем его число
— инкрементим число на 1
— записываем число в тот же файл ('i')
— пишем в консоль промежуточные результаты

Если был запущен один экземпляр функции, вывод получается примерно таким,
after read 0
after write 1
after read 1
after write 2
after read 2
after write 3
after read 3
after write 4
after read 4


Рабочий код, node.js:
// коду примерно три года, некоторые особенности могут проявляться по разному в разных версиях node.
var fs = require('fs');

function read_i(clb) {
    fs.readFile('i', function(e, i) { clb(toNumber(i)); });
}
function write_i(i, clb) {
    fs.writeFile('i', i, clb);
}

function toNumber(buffer) {
    return +(buffer || 0).toString();
}

function task() {
    read_i(function(i) {
         console.log('after read', i);
         write_i(i + 1, function() { 
             console.log('after write', toNumber(fs.readFileSync('i'))); // debug    
             task();
         });
    });
}

task();
task();
task();
task();
task();
task();


Если файл изначально не существовал, то будет вывод примерно такого вида:
after read 0
after read 0
after write 0
after read 5
after read 0


Предположим, файл существует и хранит, скажем, значение 100. Через короткое время после начала работы вывод снова становится вот таким:
after read 0
after read 0
after write 0
after read 5
after read 0


Вопрос — если файл изначально хранит, скажем, 100, откуда берется 0 ?

Пример демонстрирует тёмную сторону node.js и JavaScript в частности. Фактически, речь про кооперативную многозадачность, только с ручным приводом. А раз это многозадачность, то в ней возможны все проблемы свойственные многозадачности вообще. Одна из таких проблем — гонки или race condition. Парадокс, почти на всех сайтах, особенно Микрософта, говорится о том, что никаких race conditions в JavaScript нет и быть не может.
Собственно, авторы таких строк абсолютно правы. Только они забыли сказать, что имели ввиду потоковую многозадачность, которой в JavaScript действительно нет — JavaScript работает в одном физическом потоке.

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