Сомневаетесь в цикле событий, многопоточности, порядке выполнения readFile () в Node.js?

fs.readFile("./large.txt", "utf8", (err, data) => {
console.log('It is a large file') 

//this file has many words (11X MB). 
//It takes 1-2 seconds to finish reading (usually 1)

});

fs.readFile("./small.txt","utf8", (err, data) => {

for(let i=0; i<99999 ;i++)
console.log('It is a small file');

//This file has just one word. 
//It always takes 0 second
}); 

Результат:

Консоль всегда сначала будет печатать «Это небольшой файл» 99999 раз (для завершения печати требуется около 3 секунд). Тогда, после того как все они будут напечатаны, консоль не сразу выведет «Это большой файл». (Он всегда печатается через 1-2 секунды).

Моя мысль:

Таким образом, похоже, что первая функция readFile () и вторая функция readFile () не работают параллельно. Если бы две функции readFile () выполнялись параллельно, то я ожидал бы, что после того, как «Это небольшой файл» было напечатано 99999 раз, первый readFile () завершит чтение намного раньше (всего на 1 секунду), и консоль будет немедленно распечатайте обратный вызов первого readFile () (т.е. «Это большой файл».)

Мои вопросы:

(1a) Означает ли это, что первый readFile () начнет читать файл только после того, как обратный вызов второго readFile () выполнит свою работу?

(1b) Насколько я понимаю, в nodeJs цикл событий передает readFile () многопоточному Libuv. Однако мне интересно, в каком порядке они передаются. Если эти две функции readFile () не работают параллельно, почему вторая функция readFile () всегда выполняется первой?

(2) По умолчанию Libuv имеет четыре потока для Node.js. Итак, выполняются ли эти два readFile () в одном потоке? Я не уверен, есть ли среди этих четырех потоков только один для readFile ().

Большое спасибо за то, что уделили время! Ценить!


person Jacky Chau    schedule 04.01.2020    source источник


Ответы (4)


(1a) Означает ли это, что первый readFile () начнет читать файл только после того, как обратный вызов второго readFile () выполнит свою работу?

Нет. Каждый readFile() фактически состоит из нескольких шагов (открытие файла, чтение блока, чтение блока ... закрытие файла). Логический поток между шагами контролируется кодом Javascript в библиотеке node.js fs. Но часть каждого шага реализуется с помощью собственного многопоточного кода в libuv с использованием пула потоков.

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

(1b) Насколько я понимаю, в nodeJs цикл событий передает readFile () многопоточному Libuv. Однако мне интересно, в каком порядке они передаются. Если эти две функции readFile () не работают параллельно, почему вторая функция readFile () всегда выполняется первой?

fs.readFile() сам по себе не реализован в libuv. Он реализован в виде серии отдельных шагов в JavaScript в node.js. Каждый отдельный шаг (открытие файла, чтение блока, закрытие файла) реализован в libuv, но Javascript в библиотеке fs управляет последовательностью между шагами. Итак, думайте о fs.readfile() как о серии вызовов libuv. Когда у вас одновременно выполняются две fs.readFile() операции, каждая из них будет иметь некоторую операцию libuv, выполняющуюся в любой момент времени, и один шаг для каждой fs.readFile() может выполняться параллельно из-за реализации пула потоков в libuv. Но между каждым шагом в процессе управление возвращается интерпретатору JS. Таким образом, если интерпретатор будет занят в течение некоторого длительного периода времени, дальнейший прогресс в планировании следующего шага другой fs.readFile() операции останавливается.

(2) По умолчанию Libuv имеет четыре потока для Node.js. Итак, выполняются ли эти два readFile () в одном потоке? Я не уверен, есть ли среди этих четырех потоков только один для readFile ().

Я думаю, что это описано в двух предыдущих объяснениях. readFile() сам по себе не реализован в собственном коде libuv. Вместо этого он написан на Javascript с вызовами операций открытия, чтения и закрытия, которые написаны в собственном коде и используют libuv и пул потоков.

Вот полный отчет о том, что происходит. Чтобы полностью понять, нужно знать об этом:

Основные понятия

  1. Однопоточный, без упреждающего характера node.js, на котором запущен ваш Javascript (при условии, что здесь вручную не запрограммированы WorkerThreads, а это не так).
  2. Многопоточный собственный код файлового ввода-вывода модуля fs и принцип его работы.
  3. Как асинхронные операции собственного кода сообщают о завершении через очередь событий и как работает планирование цикла событий, когда интерпретатор JS чем-то занят.

Асинхронный, неблокирующий

Я полагаю, вы знаете, что fs.readFile() асинхронный и неблокирующий. Это означает, что когда вы его вызываете, все, что он делает, - это инициирует операцию чтения файла, а затем он переходит прямо к следующей строке кода на верхнем уровне после fs.readFile() (а не кода внутри обратного вызова, который вы ему передаете).

Итак, сокращенная версия вашего кода в основном такова:

fs.readFile(x, funcA);
fs.readFile(y, funcB);

Если мы добавим к этому логирование:

function funcA() {
    console.log("funcA");
}
function funcB() {
    console.log("funcB");
}

function spin(howLong) {
    let finishTime = Date.now() + howLong;
    // spin until howLong ms passes
    while (Date.now() < finishTime) {}
}

console.log("1");
fs.readFile(x, funcA);
console.log("2");
fs.readFile(y, funcB);
console.log("3");
spin(30000);         // spin for 30 seconds    
console.log("4");

Вы бы увидели либо такой порядок:

1
2
3
4
A
B

или в таком порядке:

1
2
3
4
B
A

Какой из двух это будет, будет зависеть от неопределенной гонки между двумя fs.readFile() операциями. Либо могло случиться. Также обратите внимание, что 1, 2, 3 и 4 регистрируются до того, как могут произойти какие-либо события асинхронного завершения. Это связано с тем, что основной поток однопоточного интерпретатора JS без упреждения занят выполнением Javascript. Он не будет извлекать следующее событие из очереди событий, пока не выполнит этот фрагмент Javascript.

Пул потоков Libuv

Как вы, кажется, уже знаете, модуль fs использует пул потоков libuv для выполнения файлового ввода-вывода. Это не зависит от основного потока JS, поэтому эти операции чтения могут выполняться независимо от дальнейшего выполнения JS. Используя собственный код, файловый ввод-вывод будет связываться с очередью событий, когда они будут выполнены, чтобы запланировать обратный вызов завершения.

Неопределенная гонка между двумя асинхронными операциями

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

Какой бы fs.readFile() ни завершился раньше, первым вставит свой обратный вызов в очередь событий. Когда интерпретатор JS свободен, он выберет следующее событие из очереди событий. То, что завершится первым, первым запустит свой обратный вызов. Поскольку небольшой файл, скорее всего, завершится первым (о чем вы сообщаете), он должен выполнить свой обратный вызов. Теперь, когда он выполняет свой обратный вызов, это просто Javascript, и даже если большой файл может завершиться и вставить свой обратный вызов в очередь событий, этот обратный вызов не может выполняться, пока не завершится обратный вызов из небольшого файла. Итак, он завершается, и ТОГДА запускается обратный вызов из большого файла.

В общем, вы никогда не должны писать подобный код, если вам совсем не важно, в каком порядке завершаются две асинхронные операции, потому что это неопределенная гонка, и вы не можете рассчитывать, какая из них завершится первой. Из-за асинхронного неблокирующего характера fs.readFile() нет гарантии, что первая инициированная файловая операция завершится первой. Это не что иное, как запуск двух отдельных HTTP-запросов один за другим. Вы не знаете, какой из них завершится первым.

Пошаговая хронология

Вот пошаговая хронология того, что происходит:

  1. Звоните fs.readFile("./large.txt", ...);
  2. В коде Javascript это инициирует открытие файла large.txt путем вызова собственного кода, а затем возвращается. Открытие фактического файла обрабатывается libuv в собственном коде, и когда это будет сделано, событие будет вставлено в очередь событий JS.
  3. Сразу после того, как эта операция инициируется, возвращается первый fs.readFile() (еще не сделано, все еще обрабатывается внутри).
  4. Теперь интерпретатор JS выбирает следующую строку кода и запускает fs.readFile("./small.txt", ...);
  5. В коде Javascript это инициирует открытие файла small.txt, вызывая собственный код, а затем возвращается. Открытие фактического файла обрабатывается libuv в собственном коде, и когда это будет сделано, событие будет вставлено в очередь событий JS.
  6. Сразу после того, как эта операция инициирована, возвращается вторая fs.readFile() (еще не завершена, все еще выполняется внутренняя обработка).
  7. Интерпретатор JS фактически может запускать любой следующий код или обрабатывать любые входящие события.
  8. Затем, спустя некоторое время, одна из двух fs.readFile() операций завершает свой первый шаг (открытие файла), событие вставляется в очередь событий JS, и когда у интерпретатора JS есть время, вызывается обратный вызов. Поскольку открытие каждого файла занимает примерно одинаковое время операции, вполне вероятно, что операция открытия файла large.txt завершится первой, но это не гарантируется.
  9. После успешного открытия файла он инициирует асинхронную операцию для чтения первого фрагмента из файла. Это снова асинхронно и обрабатывается libuv, поэтому, как только это инициируется, оно возвращает управление интерпретатору JS.
  10. Открытие второго файла, вероятно, завершается следующим и делает то же, что и первое, инициирует чтение первого фрагмента данных с диска и возвращает управление интерпретатору JS.
  11. Затем один из этих двух блоков чтения завершается и вставляет событие в очередь событий, и когда интерпретатор JS свободен, для его обработки вызывается обратный вызов. На этом этапе это может быть либо большой, либо маленький файл, но для простоты объяснения давайте предположим, что первая часть большого файла завершается первой. Он буферизует этот кусок, увидит, что есть больше данных для чтения, и инициирует другую операцию асинхронного чтения, а затем вернет управление обратно интерпретатору JS.
  12. Затем заканчивается чтение другого первого фрагмента. Он буферизует этот фрагмент и увидит, что данных для чтения больше нет. На этом этапе он выполнит операцию закрытия файла, которая снова будет обработана libuv, и управление будет возвращено интерпретатору JS.
  13. Одна из двух предыдущих операций завершается (чтение второго блока из large.txt или закрытие файла small.txt) и вызывается ее обратный вызов. Поскольку операция закрытия не обязательно должна касаться диска (она просто переходит в ОС), для объяснения предположим, что операция закрытия завершается первой. Это закрытие запускает конец fs.ReadFile() для small.txt и вызывает обратный вызов завершения для этого.
  14. Итак, на этом этапе small.txt завершено, и large.txt прочитал один фрагмент из своего файла и ожидает завершения второго фрагмента для чтения.
  15. Теперь ваш код выполняет цикл for, который занимает все необходимое время.
  16. К тому моменту, когда интерпретатор JS снова становится свободным, второй файл, считанный из large.txt, вероятно, завершен, поэтому интерпретатор JS находит это событие в очереди событий и выполняет обратный вызов, чтобы выполнить дополнительную обработку при чтении дополнительных фрагментов из этого файла.
  17. Процесс чтения фрагмента, возврата управления обратно интерпретатору, ожидания следующего события завершения фрагмента и последующей буферизации этого фрагмента продолжается до тех пор, пока все данные не будут прочитаны.
  18. Затем для large.txt инициируется операция закрытия.
  19. Когда эта операция закрытия завершена, вызывается обратный вызов для fs.readFile() для large.txt, и ваш код, который синхронизирует large.txt, будет измерять завершение.

Итак, поскольку логика fs.readFile() реализована в Javascript с рядом дискретных асинхронных шагов, каждый из которых в конечном итоге обрабатывается libuv (открыть файл, прочитать фрагмент - N раз, закрыть файл), будет чередование работы между двумя файлы. Чтение меньшего файла завершится первым только потому, что в нем меньше и меньше операций чтения. Когда он завершится, у большого файла останется еще несколько фрагментов для чтения, и останется операция закрытия. Поскольку несколько шагов fs.readFile() управляются с помощью Javascript, когда вы выполняете длинный for цикл в small.txt завершении, вы также останавливаете операцию fs.readFile() для файла large.txt. Независимо от того, какой фрагмент читался, когда произошел этот цикл, он завершится в фоновом режиме, но следующее чтение фрагмента не будет выполнено до тех пор, пока не завершится обратный вызов небольшого файла.

Похоже, что у node.js была бы возможность улучшить отзывчивость fs.readFile() в таких конкурентных условиях, если бы эта операция была полностью переписана в машинном коде, чтобы одна операция в машинном коде могла читать содержимое всего файла, а не все эти переключается между однопоточным основным потоком JS и libuv. Если бы это было так, большой цикл for не останавливал бы выполнение large.txt, потому что он выполнялся бы полностью в потоке libuv, а не ждал бы нескольких циклов от интерпретатора JS, чтобы перейти к следующему этапу.

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

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

Тестирование

Итак, давайте проверим всю эту теорию. Я создал два файла. small.txt составляет 558 байт. large.txt составляет 255 194 500 байт.

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

const fs = require('fs');

let doSpin = false;              // -s will set this to true
let fname = "./large.txt";      

for (let i = 2; i < process.argv.length; i++) {
    let arg = process.argv[i];
    console.log(`"${arg}"`);
    if (arg.startsWith("-")) {
        switch(arg) {
            case "-s":
                doSpin = true;
                break;
            default:
                console.log(`Unknown arg ${arg}`);
                process.exit(1);
                break;
        }
    } else {
        fname = arg;
    }
}

function padDecimal(num, n = 3) {
    let str = num.toFixed(n);
    let index = str.indexOf(".");
    if (index === -1) {
        str += ".";
        index = str.length - 1;
    }
    let zeroesToAdd = n - (str.length - index);
    while (zeroesToAdd-- >= 0) {
        str += "0";
    }

    return str;
}

let startTime;
function log(msg) {
    if (!startTime) {
        startTime = Date.now();
    }
    let diff = (Date.now() - startTime) / 1000;    // in seconds
    console.log(padDecimal(diff), ":", msg)
}


function funcA(err, data) {
    if (err) {
       log("error on large");
       log(err);
       return;
    }
    log("large completed");
}

function funcB(err, data) {
    if (err) {
       log("error on small");
       log(err);
       return;
    }
    log("small completed");
    if (doSpin) {
        spin(3000);
        log("spin completed");
    }
}

function spin(howLong) {
    let finishTime = Date.now() + howLong;
    // spin until howLong ms passes
    while (Date.now() < finishTime) {}
}

log("start");
fs.readFile(fname, funcA);
log("large initiated");
fs.readFile("./small.txt", funcB);
log("small initiated");

Затем (используя узел v12.13.0) я запустил его как с 3-секундным вращением, так и без него. Без вращения я получаю такой вывод:

0.000 : start
0.015 : large initiated
0.016 : small initiated
0.021 : small completed
0.240 : large completed

Это показывает разницу в 0,219 секунды между временем выполнения малого и большого (при одновременном выполнении обоих).

Затем, вставив 3-секундную задержку, мы получаем следующий результат:

0.000 : start
0.003 : large initiated
0.004 : small initiated
0.009 : small completed
3.010 : spin completed
3.229 : large completed

У нас есть точно такая же разница в 0,219 секунды между временем выполнения малого и большого (при одновременном выполнении обоих). Это показывает, что большой fs.readFile() практически не продвинулся во время 3-х секундного вращения. Его прогресс был полностью заблокирован. Как мы предположили в предыдущем объяснении, это, по-видимому, связано с тем, что переход от одного фрагментированного чтения к следующему записывается в Javascript, и пока выполняется цикл вращения, этот переход к следующему фрагменту блокируется, поэтому он не может ничего сделать. дальнейший прогресс.

Насколько большой файл занимает секунду после обработки большого файла?

Если вы посмотрите код для fs.readFile() в источнике для узла v12.13.0, вы увидите, что размер блока, который он читает, равен 512 * 1024, то есть 512 КБ. Итак, теоретически возможно, что файл большего размера может закончиться первым, если его можно будет прочитать одним фрагментом. Произойдет ли это на самом деле или нет, зависит от некоторых деталей реализации ОС и диска, но я подумал, что попробую это на своем ноутбуке с текущей версией Windows 10 с SSD-накопителем.

Я обнаружил, что для «большого» файла размером 255 КБ он заканчивается раньше, чем маленький файл (по существу, в порядке выполнения). Таким образом, поскольку чтение большого файла начинается до чтения небольшого файла, даже если у него есть больше данных для чтения, оно все равно завершится раньше, чем маленький файл.

0.000 : start
0.003 : large initiated
0.003 : small initiated
0.007 : large completed
0.008 : small completed

Имейте в виду, что это зависит от ОС и диска, поэтому это не гарантируется.

person jfriend00    schedule 04.01.2020
comment
Я слышал, что это цикл событий, который передает операцию ввода-вывода в многопоточность. Я также слышал, что код верхнего уровня выполняется перед этапом цикла событий. Кроме того, вы сказали, что readFile - это код верхнего уровня. Затем, основываясь на этих трех утверждениях, я не понимаю, когда действительно начинается операция ввода-вывода. - person Jacky Chau; 04.01.2020
comment
@JackyChau - операция ввода-вывода начинается сразу после вызова fs.readFile(). Ввод-вывод передается потоку машинного кода в пуле потоков. Как только это произойдет, fs.readFile() вернется, и будет запущена следующая инструкция на том же уровне кода, что и она. Дело не в цикле событий. Очередь событий предназначена для сообщения о завершении. Инициирование вещей - это просто вызов функции в собственный код (Javascript может вызывать собственный код напрямую через интерфейс надстройки node.js). Затем собственный код задействует пул потоков при файловом вводе-выводе и немедленно возвращается обратно в JS. - person jfriend00; 04.01.2020
comment
@JackyChau - Пожалуйста, не запутайтесь в словосочетании «код верхнего уровня». Я хотел сказать следующую строку кода, но поскольку вы используете встроенные обратные вызовы, я не хотел, чтобы вы думали, что следующая выполняемая строка находится в обратном вызове, поэтому я попытался найти другую фразу. Это следующая строка кода на том же уровне, что и fs.readFile(), которая выполняется следующей. - person jfriend00; 04.01.2020
comment
`` `Строка 1: fs.readFile (./ large.txt, utf8, (err, data) =› {Line2: console.log ('Для завершения чтения большого файла потребовалось 2 секунды')}) Строка 3: для ( let i = 0; i ‹99999; i ++) {console.log ('блокировка 4 секунды') Ссылка 4: fs.readFile (./ small.txt, utf8, (err, data) =› {console.log (' small file занял 0 секунд завершения чтения) `` Предположим, что строка 1 выполняется в 00:00:05, тогда теоретически операция ввода-вывода завершается в 00:00:07 и помещается в очередь событий. ~ 00: 00: 09; в 00:00:09 выполняется другая операция ввода / вывода в строке 4. - person Jacky Chau; 04.01.2020
comment
(Продолжить) Обратный вызов readFile в строке 4 кажется поставлен в очередь после обратного вызова readFile в строке 1; Итак, почему, в конце концов, сначала выполняется обратный вызов readFile в строке 4? - person Jacky Chau; 04.01.2020
comment
@JackyChau - обратный вызов ставится в очередь только после завершения асинхронной операции, выполняемой в потоках собственного кода. Когда это происходит, это не имеет ничего общего с тем, когда была запущена асинхронная операция. Пожалуйста, прочтите мою пошаговую хронологию. Думаю, там все объяснено. - person jfriend00; 05.01.2020
comment
@JackyChau - Я расширил пошаговую хронологию, чтобы показать, как на самом деле написано fs.readFile(), что представляет собой серию переходов между Javascript и собственным кодом в libuv. Это показывает, как чтение large.txt на некоторое время останавливается во время длительного for цикла в обратном вызове завершения small.txt. - person jfriend00; 05.01.2020
comment
@JackyChau - Я добавил конкретные ответы по пунктам на вопросы, которые вы задали в своем исходном вопросе. - person jfriend00; 05.01.2020
comment
@JackyChau - Я добавил в ответ тестовую программу и тестовые данные. - person jfriend00; 05.01.2020
comment
Вау, это очень подробно. Большое спасибо за ваши усилия! Извините за мой поздний ответ. - person Jacky Chau; 15.01.2020
comment
@JackyChau - Знаете ли вы, что вы можете выбрать здесь лучший ответ, щелкнув галочку рядом с ответом. Это укажет сообществу, какой ответ, по вашему мнению, помог вам больше всего. Это также принесет вам несколько очков репутации за соблюдение надлежащей процедуры здесь. - person jfriend00; 15.01.2020
comment
Спасибо за напоминания. Извините, я новичок в Stackoverflow. Только что я выбрал лучший ответ - person Jacky Chau; 15.01.2020
comment
@JackyChau - Нет проблем. Рад, что смог помочь. - person jfriend00; 15.01.2020

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

const fs = require('fs');

const readLarge = process.argv.includes('large');
const readSmall = process.argv.includes('small');

if (readLarge) {
    console.time('large');
    fs.readFile('./large.txt', 'utf8', (err, data) => {
        console.timeEnd('large');
        if (readSmall) {
            console.timeEnd('large (since busy wait)');
        }
    });
}

if (readSmall) {
    console.time('small');
    fs.readFile('./small.txt', 'utf8', (err, data) => {
        console.timeEnd('small');
        var stop = new Date().getTime();
        while(new Date().getTime() < stop + 3000) { ; } // busy wait
        console.time('large (since busy wait)');
    });
}

(Обратите внимание, что я заменил ваш цикл console.logs на ожидание занятости 3 секунды).

Запустив это против узла v8.15.0, я получаю следующие результаты:

$ node t small # read small file only
small: 0.839ms
$ node t large # read large file only
large: 447.348ms
$ node t small large # read both files
small: 3.916ms
large: 3252.752ms
large (since busy wait): 247.632ms

Эти результаты кажутся разумными; для чтения большого файла потребовалось ~ 0,5 с, но, когда обратный вызов занятого ожидания вмешался в течение 2 с, после этого он завершился относительно быстро (~ 1/4 с). Настройка продолжительности ожидания при занятости сохраняет это относительно согласованным, поэтому я хотел бы просто сказать, что это были какие-то накладные расходы на планирование, а не обязательно признак того, что ввод-вывод больших файлов не выполнялся во время ожидания при занятости.

Но затем я запустил ту же программу против узла 10.16.3, и вот что у меня получилось:

$ node t small
small: 1.614ms
$ node t large
large: 1019.047ms
$ node t small large
small: 3.595ms
large: 4014.904ms
large (since busy wait): 1009.469ms

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

Я подозреваю, что это изменение с 8.x на 10.x является результатом этой «оптимизации» в узле 10: https://github.com/nodejs/node/pull/17054. Это изменение, которое разделяет чтение больших файлов на несколько операций, кажется подходящим для сглаживания производительности системы в случаях общего назначения, но, вероятно, усугубляется неестественно долгой обработкой основного потока / ожиданием занятости в этом сценарии. Предположительно, без выполнения основного потока, ввод-вывод не получает возможности перейти к следующему диапазону байтов в большом файле для чтения.

Казалось бы, с Node 10.x важно иметь отзывчивый основной поток (то есть тот, который часто дает результат и не занят ожиданием, как в этом примере), чтобы поддерживать производительность ввода-вывода большого файла. читает.

person Myk Willis    schedule 04.01.2020
comment
Это фактически объяснило бы некоторые из вопросов, которые я видел в форме, почему node.js медленнее, чем (вставьте язык X), и я не мог воспроизвести результаты на своей машине - person slebetman; 04.01.2020
comment
Только что наткнулся на оживленное обсуждение, связанное с изменением поведения больших файлов в Node 10.x: github.com / nodejs / node / issues / 25741 - person Myk Willis; 04.01.2020
comment
Вот это да. Очень хорошая работа! Итак, мне кажется, что я правильно понимаю, и здесь есть неловкая и исключительная проблема. На самом деле я новичок в Node.js. Я был рад, что мои вопросы и сомнения побудили вас, ребята, открыть для себя что-то новое. - person Jacky Chau; 04.01.2020
comment
Если вы посмотрите на конец моего ответа здесь, я посмотрел, как работает реальный код для fs.readFile(), и проработал фактические шаги для этих двух конкурирующих операций. Поскольку fs.readFile() - это многоэтапная операция, и шаги управляются с помощью Javascript, когда происходит цикл занятости, он останавливает дальнейший прогресс при чтении дополнительных фрагментов из large.txt. Если fs.readFile() был написан полностью в многопоточном машинном коде, тогда он мог бы продолжаться, даже если интерпретатор JS был занят, но это не так. - person jfriend00; 05.01.2020
comment
Таким образом, во время цикла занятости следующий блок чтения для large.txt может завершиться в фоновом режиме, но затем дальнейшее продвижение останавливается из-за занятости интерпретатора JS. - person jfriend00; 05.01.2020

Файловый ввод-вывод в Node.js выполняется в отдельном потоке. Но это не важно. Node.js всегда выполняет все обратные вызовы в основном потоке. Обратные вызовы ввода-вывода никогда не выполняются в отдельном потоке (операция чтения файла выполняется в отдельном потоке, а затем, когда она будет завершена, будет сигнализировать основному потоку для запуска вашего обратного вызова). Это по сути делает node.js однопоточным, потому что весь код, который вы пишете, выполняется в основном потоке (мы, конечно, игнорируем модуль / API worker_threads, который позволяет вам вручную выполнять код в отдельных потоках) .

Но байты в файлах читаются параллельно (или настолько параллельно, насколько позволяет ваше оборудование - в зависимости от количества свободных каналов DMA, с какого диска находится каждый файл и т. Д.). Параллельно с этим ожидание. Асинхронный ввод-вывод на любом языке (node.js, Java, C ++, Python и т. Д.) - это, по сути, API, который позволяет вам ждать параллельно, но обрабатывать события в одном потоке. Есть такое слово для обозначения параллельности: одновременный. По сути, это параллельное ожидание (пока данные параллельно обрабатываются вашим оборудованием), но не параллельное выполнение кода.

person slebetman    schedule 04.01.2020
comment
Искренне благодарим Вас за подробный ответ. Извините, я не уверен, действительно ли я это понимаю. Вы имеете в виду, что две операции ввода-вывода в моих двух функциях readFile () в моем коде не выполняются одновременно? - person Jacky Chau; 04.01.2020
comment
Это не правильно. Файловый ввод-вывод в node.js использует пул потоков, который имеет несколько потоков. - person jfriend00; 04.01.2020
comment
Я попытался выяснить следующие шаги. Я не уверен. Шаг 1: Второй readFile () запускает операцию ввода-вывода. Шаг 2: Когда это будет сделано, отправьте обратный вызов основному потоку для обработки. Шаг 3: первый readFile () запускает операцию ввода-вывода. Шаг 4: Когда это будет сделано, отправьте обратный вызов в основной поток. - person Jacky Chau; 04.01.2020
comment
@JackyChau Это больше похоже на шаг 1: запланирован первый readFile (), step2: запланирован второй readFile (), step3: конец скрипта, больше ничего не нужно выполнять, поэтому node.js входит в обработку цикла событий, step3.1: и первый, и запускаются вторые операции readFile (), и цикл событий ожидает завершения любого из них, шаг 4: первый файл, который завершит чтение, запускает свой обратный вызов - обратите внимание, что это может быть либо readFile (), но, поскольку второй файл меньше, он, очевидно, завершается первым, step5: следующий readFile () для завершения запускает свой обратный вызов - person slebetman; 04.01.2020
comment
@ jfriend00 - Редактирование этой части - person slebetman; 04.01.2020
comment
@JackyChau Что нужно иметь в виду, так это то, что node.js не может обрабатывать обратный вызов (обработку цикла событий), пока не будет больше javascript для выполнения - вот почему бесконечные циклы блокируют ввод-вывод и таймеры - person slebetman; 04.01.2020
comment
@slebetman Думаю, ваши шаги более разумны. Я тоже думал об этих шагах. Но я не умею пользоваться удобством. Обратный вызов функции readFile (файл меньшего размера) занимает около 4 секунд (из-за цикла в 99999 раз). Таким образом, в течение этих четырех секунд после печати операция ввода-вывода для другой функции readFile (для файла большего размера) должна была быть завершена раньше и помещена в очередь событий. Затем, после того, как сообщения (99999 раз) были напечатаны, должен быть немедленно вызван обратный вызов другой функции. Но почему ему нужно ждать одну секунду? - person Jacky Chau; 04.01.2020
comment
@JackyChau - см. Ответ Мика Уиллиса - это кажется непреднамеренным следствием оптимизации, которое нарушает мое теоретическое объяснение обработки ввода-вывода javascript. И обратите внимание, что это ведет себя по-разному с разными версиями node, поэтому это не общий механизм node.js, а зависит от конкретной реализации. - person slebetman; 04.01.2020
comment
Вот это да. Очень хорошая работа! Итак, мне кажется, что я правильно понимаю, и здесь есть неловкая и исключительная проблема. На самом деле я новичок в Node.js. Я был рад, что мои вопросы и сомнения побудили вас, ребята, открыть для себя что-то новое. - person Jacky Chau; 04.01.2020
comment
@JackyChau stackoverflow потрясающий. Я видел, как Крис Эспиноза (один из первых сотрудников Apple, когда Джобс и Воз работали вне гаража) отвечал на вопросы по Xcode, а Гвидо ван Россум (создатель Python) отвечал на вопросы по Python. - person slebetman; 05.01.2020

Я думаю, что вы понимаете поведение цикла событий и libuv, не заблудитесь.

Мои ответы:

1a) Конечно, два прочитанных файла выполняются в двух разных потоках, я попытался запустить ваш код, заменив большой файл маленьким, и результат был
It is a large file It is a small file

1b) Второй вызов просто завершается раньше в вашем случае, а затем обратный вызов вызывается раньше.

2) Как вы сказали, libuv по умолчанию имеет четыре потока, но убедитесь, что значения по умолчанию не изменены, установив переменную env UV_THREADPOOL_SIZE (http://docs.libuv.org/en/v1.x/threadpool.html)

Я пытался работать с большим и большим файлом, на чтение большого файла на моем компьютере уходит 23/25 мс, на чтение маленького файла - 8/10 мс. Когда я пытаюсь прочитать, оба процесса завершаются через 26/27 мс, и это демонстрирует, что два файла чтения выполняются параллельно.

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

console.log(process.env.UV_THREADPOOL_SIZE)
const fs = require('fs')
const start = Date.now()
let smallFileEnd


fs.readFile("./alphabet.txt", "utf8", (err, data) => {
    console.log('It is a large file') 
    console.log(`From the start to now are passed ${Date.now() - start} ms`)
    console.log(`From the small file end to now are passed ${Date.now() - smallFileEnd} ms`)
    //this file has many words (11X MB). 
    //It takes 1-2 seconds to finish reading (usually 1)
    // 18ms to execute
}); 
 

fs.readFile("./stackoverflow.js","utf8", (err, data) => {
    
    for(let i=0; i<99999 ;i++)
        if(i === 99998){
            smallFileEnd = Date.now()
            console.log('is a small file ')
            console.log(`From the start to now are passed ${Date.now() - start} ms`)
             // 4/7 ms to execute
        }
            
});
 

person pioardi    schedule 04.01.2020
comment
Я пытаюсь переварить твой ответ. Между тем, не могли бы вы также потратить некоторое время на чтение ответа Мика Уиллиса и посмотреть, появятся ли у вас какие-то другие мысли после этого? - person Jacky Chau; 04.01.2020
comment
Привет, @JackyChau, я взглянул на ответ Мика, это хороший момент, и первая часть ответа похожа на мой комментарий, вторая часть говорит о запросе на перенос об оптимизации для метода readFile, и это от моя точка зрения не связана с этим. Вы прекрасно понимаете эту тему, просто попробуйте сделать то же, что и я, и Milk, запишите, сколько времени прошло между маленьким обратным вызовом и большим обратным вызовом, и дайте нам знать :) - person pioardi; 04.01.2020