Là Promise.all bản địa của Node.js xử lý song song hoặc tuần tự?


173

Tôi muốn làm rõ điểm này, vì tài liệu không quá rõ ràng về nó;

Q1: Được Promise.all(iterable)xử lý tất cả những lời hứa tuần tự hoặc song song? Hay cụ thể hơn, nó tương đương với những lời hứa bị xiềng xích như

p1.then(p2).then(p3).then(p4).then(p5)....

hoặc là nó một số loại khác của thuật toán mà tất cả p1, p2, p3, p4, p5, vv được gọi cùng một lúc (song song) và kết quả được trả về càng sớm càng tất cả quyết tâm (hoặc một Rejects)?

Câu 2: Nếu Promise.allchạy song song, có cách nào thuận tiện để chạy tuần tự lặp không?

Lưu ý : Tôi không muốn sử dụng Q hoặc Bluebird, nhưng tất cả các thông số ES6 gốc.


Bạn đang hỏi về việc thực hiện nút (V8), hoặc về thông số kỹ thuật?
Amit

1
Tôi khá chắc chắn Promise.allthực hiện chúng song song.
royhowie

@Amit Tôi đã gắn cờ node.jsio.jsvì đây là nơi tôi đang sử dụng nó. Vì vậy, có, việc thực hiện V8 nếu bạn muốn.
Yanick Rochon

9
Lời hứa không thể "được thực thi". Họ bắt đầu nhiệm vụ khi chúng được tạo - chúng chỉ đại diện cho kết quả - và bạn đang thực hiện mọi thứ song song ngay cả trước khi chuyển chúng đến Promise.all.
Bergi

Lời hứa được thực hiện tại thời điểm sáng tạo. (có thể được xác nhận bằng cách chạy một chút mã). Trong new Promise(a).then(b); c();a được thực hiện trước, sau đó c, sau đó b. Đó không phải là Promise.all chạy những lời hứa này, nó chỉ xử lý khi chúng giải quyết.
Mateon1

Câu trả lời:


256

Promise.all(iterable)thực hiện tất cả các lời hứa?

Không, những lời hứa không thể "được thực thi". Họ bắt đầu nhiệm vụ khi chúng được tạo - chúng chỉ đại diện cho kết quả - và bạn đang thực hiện mọi thứ song song ngay cả trước khi chuyển chúng đến Promise.all.

Promise.allkhông chỉ chờ đợi nhiều lời hứa. Nó không quan tâm theo thứ tự họ giải quyết, hoặc liệu các tính toán đang chạy song song.

Có cách nào thuận tiện để chạy một trình tự lặp lại không?

Nếu bạn đã có những lời hứa của mình, bạn không thể thực hiện nhiều nhưng Promise.all([p1, p2, p3, …])(điều này không có khái niệm về trình tự). Nhưng nếu bạn có một hàm lặp không đồng bộ, bạn thực sự có thể chạy chúng tuần tự. Về cơ bản bạn cần phải nhận được từ

[fn1, fn2, fn3, …]

đến

fn1().then(fn2).then(fn3).then(…)

và giải pháp để làm điều đó là sử dụng Array::reduce:

iterable.reduce((p, fn) => p.then(fn), Promise.resolve())

1
Trong ví dụ này, có thể lặp lại một mảng các hàm trả về một lời hứa mà bạn muốn gọi không?
James Reategui

2
@SSHThis: Chính xác như thentrình tự - giá trị trả về là lời hứa cho fnkết quả cuối cùng và bạn có thể xâu chuỗi các cuộc gọi lại khác với điều đó.
Bergi

1
@wojjas Điều đó chính xác tương đương với fn1().then(p2).then(fn3).catch(…? Không cần sử dụng biểu thức hàm.
Bergi

1
@wojjas Tất nhiên retValFromF1là được truyền vào p2, đó chính xác là những gì p2. Chắc chắn, nếu bạn muốn làm nhiều hơn (chuyển các biến bổ sung, gọi nhiều hàm, v.v.), bạn cần sử dụng biểu thức hàm, mặc dù việc thay đổi p2trong mảng sẽ dễ dàng hơn
Bergi

1
@ robe007 Vâng, ý tôi là đó iterable[fn1, fn2, fn3, …]mảng
Bergi

62

Song song

await Promise.all(items.map(async item => { await fetchItem(item) }))

Ưu điểm: Nhanh hơn. Tất cả các lần lặp sẽ được thực hiện ngay cả khi một lần thất bại.

Theo thứ tự

for (let i = 0; i < items.length; i++) {
    await fetchItem(items[i])
}

Ưu điểm: Các biến trong vòng lặp có thể được chia sẻ bởi mỗi lần lặp. Hành vi như mã đồng bộ bắt buộc bình thường.


7
Hoặc:for (const item of items) await fetchItem(item);
Robert Penner

1
@david_adler Trong các ưu điểm ví dụ song song, bạn đã nói Tất cả các lần lặp sẽ được thực hiện ngay cả khi một lỗi . Nếu tôi không sai thì điều này vẫn thất bại nhanh chóng. Để thay đổi hành vi này, người ta có thể làm một cái gì đó như: await Promise.all(items.map(async item => { return await fetchItem(item).catch(e => e) }))
Taimoor

@Taimoor vâng, nó "không nhanh" và tiếp tục thực thi mã sau Promise.all nhưng tất cả các lần lặp vẫn được thực thi codepen.io/mfbx9da4/pen/BbaaXr
david_adler

Cách tiếp cận này tốt hơn, khi asyncchức năng là một API gọi bạn không muốn DDOS máy chủ. Bạn có quyền kiểm soát tốt hơn đối với các kết quả riêng lẻ và các lỗi được đưa ra trong quá trình thực thi. Thậm chí tốt hơn bạn có thể quyết định những lỗi nào sẽ tiếp tục và những gì để phá vỡ vòng lặp.
quan

Lưu ý rằng javascript không thực sự thực hiện các yêu cầu không đồng bộ theo cách "song song" bằng cách sử dụng các luồng vì javascript là một luồng. developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop
david_adler

11

Câu trả lời của Bergis đã đưa tôi đi đúng hướng bằng Array.reduce.

Tuy nhiên, để thực sự có được các hàm trả về lời hứa của tôi để thực hiện lần lượt, tôi phải thêm một số lồng nhau.

Trường hợp sử dụng thực tế của tôi là một loạt các tệp mà tôi cần chuyển theo thứ tự lần lượt do giới hạn hạ lưu ...

Đây là những gì tôi đã kết thúc với.

getAllFiles().then( (files) => {
    return files.reduce((p, theFile) => {
        return p.then(() => {
            return transferFile(theFile); //function returns a promise
        });
    }, Promise.resolve()).then(()=>{
        console.log("All files transferred");
    });
}).catch((error)=>{
    console.log(error);
});

Như câu trả lời trước cho thấy, sử dụng:

getAllFiles().then( (files) => {
    return files.reduce((p, theFile) => {
        return p.then(transferFile(theFile));
    }, Promise.resolve()).then(()=>{
        console.log("All files transferred");
    });
}).catch((error)=>{
    console.log(error);
});

không đợi quá trình chuyển hoàn tất trước khi bắt đầu chuyển khác và văn bản "Tất cả các tệp được chuyển" xuất hiện trước cả khi quá trình truyền tệp đầu tiên được bắt đầu.

Không chắc chắn những gì tôi đã làm sai, nhưng muốn chia sẻ những gì làm việc cho tôi.

Chỉnh sửa: Kể từ khi tôi viết bài đăng này, bây giờ tôi đã hiểu tại sao phiên bản đầu tiên không hoạt động. sau đó () mong đợi một hàm trả về một lời hứa. Vì vậy, bạn nên chuyển vào tên hàm mà không có dấu ngoặc đơn! Bây giờ, hàm của tôi muốn một đối số vì vậy sau đó tôi cần phải bọc trong một hàm ẩn danh không có đối số!


4

chỉ để giải thích về câu trả lời của @ Bergi (rất ngắn gọn, nhưng khó hiểu;)

Mã này sẽ chạy từng mục trong mảng và thêm 'chuỗi tiếp theo' vào cuối;

function eachorder(prev,order) {
        return prev.then(function() {
          return get_order(order)
            .then(check_order)
            .then(update_order);
        });
    }
orderArray.reduce(eachorder,Promise.resolve());

Hy vọng rằng có ý nghĩa.


3

Bạn cũng có thể xử lý tuần tự lặp lại với chức năng async bằng chức năng đệ quy. Ví dụ, được cung cấp một mảng ađể xử lý với chức năng không đồng bộ someAsyncFunction():

var a = [1, 2, 3, 4, 5, 6]

function someAsyncFunction(n) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log("someAsyncFunction: ", n)
      resolve(n)
    }, Math.random() * 1500)
  })
}

//You can run each array sequentially with: 

function sequential(arr, index = 0) {
  if (index >= arr.length) return Promise.resolve()
  return someAsyncFunction(arr[index])
    .then(r => {
      console.log("got value: ", r)
      return sequential(arr, index + 1)
    })
}

sequential(a).then(() => console.log("done"))


sử dụng array.prototype.reducetốt hơn nhiều về mặt hiệu suất so với chức năng đệ quy
Mateusz Sowiński 18/07/18

@ MateuszSowiński, có khoảng thời gian chờ 1500ms giữa mỗi cuộc gọi. Xem xét rằng điều này đang thực hiện các cuộc gọi async theo tuần tự, thật khó để thấy mức độ phù hợp ngay cả đối với việc quay vòng async rất nhanh.
Mark Meyer

Giả sử bạn phải thực hiện 40 chức năng async thực sự nhanh chóng với nhau - sử dụng các hàm đệ quy sẽ làm tắc nghẽn bộ nhớ của bạn khá nhanh
Mateusz Sowiński 18/07/18

@ MateuszSowiński, rằng ngăn xếp không kết thúc ở đây ... chúng tôi sẽ trở lại sau mỗi cuộc gọi. So sánh với reducenơi bạn phải xây dựng toàn bộ then()chuỗi trong một bước và sau đó thực hiện.
Mark Meyer

Trong cuộc gọi thứ 40 của chức năng tuần tự, cuộc gọi đầu tiên của chức năng vẫn còn trong bộ nhớ chờ chuỗi chức năng tuần tự trở lại
Mateusz Sowiński

2

Sử dụng async đang chờ một loạt các lời hứa có thể dễ dàng được thực hiện tuần tự:

let a = [promise1, promise2, promise3];

async function func() {
  for(let i=0; i<a.length; i++){
    await a[i]();
  }  
}

func();

Lưu ý: Trong triển khai ở trên, nếu một lời hứa bị từ chối, phần còn lại sẽ không được thực hiện. Nếu bạn muốn tất cả các lời hứa của mình được thực hiện, thì hãy bọc await a[i]();bên trong của bạntry catch


2

song song, tương đông

xem ví dụ này

const resolveAfterTimeout = async i => {
  return new Promise(resolve => {
    console.log("CALLED");
    setTimeout(() => {
      resolve("RESOLVED", i);
    }, 5000);
  });
};

const call = async () => {
  const res = await Promise.all([
    resolveAfterTimeout(1),
    resolveAfterTimeout(2),
    resolveAfterTimeout(3),
    resolveAfterTimeout(4),
    resolveAfterTimeout(5),
    resolveAfterTimeout(6)
  ]);
  console.log({ res });
};

call();

bằng cách chạy mã, nó sẽ điều khiển "GỌI" cho tất cả sáu lời hứa và khi chúng được giải quyết, nó sẽ điều khiển cứ 6 phản hồi sau khi hết thời gian cùng một lúc


2

NodeJS không chạy song song các lời hứa, nó chạy đồng thời vì chúng là kiến ​​trúc vòng lặp sự kiện đơn luồng. Có khả năng chạy mọi thứ song song bằng cách tạo một tiến trình con mới để tận dụng CPU đa lõi.

Song song Vs đồng tình

Trong thực tế, những gì Promise.all đó là , xếp chồng hàm hứa hẹn trong hàng đợi thích hợp (xem kiến ​​trúc vòng lặp sự kiện) chạy chúng đồng thời (gọi P1, P2, ...) sau đó chờ từng kết quả, sau đó giải quyết Promise.all với tất cả các lời hứa các kết quả. Promise.all sẽ thất bại ở lời hứa đầu tiên thất bại, trừ khi bạn tự mình quản lý việc từ chối.

Có một sự khác biệt lớn giữa song song và đồng thời, cái đầu tiên sẽ chạy các tính toán khác nhau trong quá trình riêng biệt cùng một lúc và chúng sẽ tiến triển ở đó rythme, trong khi cái còn lại sẽ thực hiện tính toán khác nhau mà không phải chờ trước. tính toán để hoàn thành và tiến bộ cùng một lúc mà không phụ thuộc vào nhau.

Cuối cùng, để trả lời câu hỏi của bạn, Promise.allsẽ không thực hiện song song hoặc tuần tự mà đồng thời.


Điều này không chính xác. NodeJS có thể chạy mọi thứ song song. NodeJS có một khái niệm về luồng công nhân. Theo mặc định, số lượng luồng công nhân là 4. Ví dụ: nếu bạn sử dụng thư viện tiền điện tử để băm hai giá trị thì bạn có thể thực hiện chúng song song. Hai chủ đề công nhân sẽ xử lý các nhiệm vụ. Tất nhiên, CPU của bạn phải đa lõi để hỗ trợ song song.
Shihab

Vâng bạn đúng, đó là những gì tôi đã nói ở cuối đoạn đầu tiên, nhưng tôi đã nói về quá trình trẻ em, tất nhiên họ có thể điều hành công nhân.
Adrien De Peretti

1

Câu trả lời của Bergi đã giúp tôi thực hiện cuộc gọi đồng bộ. Tôi đã thêm một ví dụ bên dưới nơi chúng tôi gọi từng chức năng sau khi chức năng trước đó được gọi.

function func1 (param1) {
    console.log("function1 : " + param1);
}
function func2 () {
    console.log("function2");
}
function func3 (param2, param3) {
    console.log("function3 : " + param2 + ", " + param3);
}

function func4 (param4) {
    console.log("function4 : " + param4);
}
param4 = "Kate";

//adding 3 functions to array

a=[
    ()=>func1("Hi"),
    ()=>func2(),
    ()=>func3("Lindsay",param4)
  ];

//adding 4th function

a.push(()=>func4("dad"));

//below does func1().then(func2).then(func3).then(func4)

a.reduce((p, fn) => p.then(fn), Promise.resolve());

Đây có phải là một câu trả lời cho câu hỏi ban đầu?
Giulio Caccin

0

Bạn có thể làm điều đó bằng vòng lặp.

hứa hẹn trả lại chức năng

async function createClient(client) {
    return await Client.create(client);
}

let clients = [client1, client2, client3];

nếu bạn viết đoạn mã sau thì client được tạo song song

const createdClientsArray = yield Promise.all(clients.map((client) =>
    createClient(client);
));

sau đó tất cả các khách hàng được tạo ra song song. nhưng nếu bạn muốn tạo máy khách tuần tự thì bạn nên sử dụng cho vòng lặp

const createdClientsArray = [];
for(let i = 0; i < clients.length; i++) {
    const createdClient = yield createClient(clients[i]);
    createdClientsArray.push(createdClient);
}

sau đó tất cả các khách hàng được tạo ra một cách tuần tự.

hạnh phúc mã hóa :)


8
Tại thời điểm này, async/ awaitchỉ khả dụng với một bộ chuyển mã hoặc sử dụng các công cụ khác ngoài Node. Ngoài ra, bạn thực sự không nên kết hợp asyncvới yield. Khi họ hành động giống nhau với một bộ chuyển mã và co, họ thực sự khá khác biệt và không nên thay thế nhau. Ngoài ra, bạn nên đề cập đến những hạn chế này vì câu trả lời của bạn gây nhầm lẫn cho các lập trình viên mới làm quen.
Yanick Rochon

0

Tôi đã sử dụng để giải quyết các lời hứa tuần tự. Tôi không chắc nó có giúp gì ở đây không nhưng đây là những gì tôi đã làm.

async function run() {
    for (let val of arr) {
        const res = await someQuery(val)
        console.log(val)
    }
}

run().then().catch()

0

điều này có thể trả lời một phần câu hỏi của bạn.

có, bạn có thể xâu chuỗi một chuỗi các hàm trả về lời hứa như sau ... (điều này chuyển kết quả của từng hàm sang hàm tiếp theo). tất nhiên bạn có thể chỉnh sửa nó để chuyển cùng một đối số (hoặc không có đối số) cho mỗi hàm.

function tester1(a) {
  return new Promise(function(done) {
    setTimeout(function() {
      done(a + 1);
    }, 1000);
  })
}

function tester2(a) {
  return new Promise(function(done) {
    setTimeout(function() {
      done(a * 5);
    }, 1000);
  })
}

function promise_chain(args, list, results) {

  return new Promise(function(done, errs) {
    var fn = list.shift();
    if (results === undefined) results = [];
    if (typeof fn === 'function') {
      fn(args).then(function(result) {
        results.push(result);
        console.log(result);
        promise_chain(result, list, results).then(done);
      }, errs);
    } else {
      done(results);
    }

  });

}

promise_chain(0, [tester1, tester2, tester1, tester2, tester2]).then(console.log.bind(console), console.error.bind(console));


0

Tôi tình cờ thấy trang này trong khi cố gắng giải quyết vấn đề trong NodeJS: tập hợp lại các khối tệp. Về cơ bản: Tôi có một loạt tên tập tin. Tôi cần nối thêm tất cả các tệp đó, theo đúng thứ tự để tạo một tệp lớn. Tôi phải làm điều này không đồng bộ.

Mô-đun 'fs' của Node cung cấp appendFileSync nhưng tôi không muốn chặn máy chủ trong hoạt động này. Tôi muốn sử dụng mô-đun fs.promises và tìm cách xâu chuỗi các công cụ này lại với nhau. Các ví dụ trên trang này không hoạt động với tôi vì tôi thực sự cần hai thao tác: fsPromises.read () để đọc trong đoạn tệp và fsPromises.appendFile () để nối với tệp đích. Có lẽ nếu tôi tốt hơn với javascript, tôi có thể làm cho các câu trả lời trước hoạt động với tôi. ;-)

Tôi tình cờ phát hiện ra điều này ... https://css-tricks.com/why-USE-reduce-to- resultively-resolve-promises-works / ... và tôi đã có thể hack cùng một giải pháp làm việc.

TLDR:

/**
 * sequentially append a list of files into a specified destination file
 */
exports.append_files = function (destinationFile, arrayOfFilenames) {
    return arrayOfFilenames.reduce((previousPromise, currentFile) => {
        return previousPromise.then(() => {
            return fsPromises.readFile(currentFile).then(fileContents => {
                return fsPromises.appendFile(destinationFile, fileContents);
            });
        });
    }, Promise.resolve());
};

Và đây là một bài kiểm tra đơn vị hoa nhài cho nó:

const fsPromises = require('fs').promises;
const fsUtils = require( ... );
const TEMPDIR = 'temp';

describe("test append_files", function() {
    it('append_files should work', async function(done) {
        try {
            // setup: create some files
            await fsPromises.mkdir(TEMPDIR);
            await fsPromises.writeFile(path.join(TEMPDIR, '1'), 'one');
            await fsPromises.writeFile(path.join(TEMPDIR, '2'), 'two');
            await fsPromises.writeFile(path.join(TEMPDIR, '3'), 'three');
            await fsPromises.writeFile(path.join(TEMPDIR, '4'), 'four');
            await fsPromises.writeFile(path.join(TEMPDIR, '5'), 'five');

            const filenameArray = [];
            for (var i=1; i < 6; i++) {
                filenameArray.push(path.join(TEMPDIR, i.toString()));
            }

            const DESTFILE = path.join(TEMPDIR, 'final');
            await fsUtils.append_files(DESTFILE, filenameArray);

            // confirm "final" file exists    
            const fsStat = await fsPromises.stat(DESTFILE);
            expect(fsStat.isFile()).toBeTruthy();

            // confirm content of the "final" file
            const expectedContent = new Buffer('onetwothreefourfive', 'utf8');
            var fileContents = await fsPromises.readFile(DESTFILE);
            expect(fileContents).toEqual(expectedContent);

            done();
        }
        catch (err) {
            fail(err);
        }
        finally {
        }
    });
});

Tôi hi vọng nó giúp ích cho ai đó.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.