12/08/2018, 13:08

Làm việc với asynchronous APIs

Chắc hẳn khi làm việc trong môi trường web development bạn sẽ không ít lần phải làm việc với các Asynchronous APIs (ví dụ AJAX call), làm sao để có thể thực hiện các xử lý khác sau khi hoàn thành lời gọi? bạn hẳn sẽ cười khẩy vì có sẵn câu trả lời, promise. Nhưng hôm nay tôi muốn nói đến vấn đề ...

Chắc hẳn khi làm việc trong môi trường web development bạn sẽ không ít lần phải làm việc với các Asynchronous APIs (ví dụ AJAX call), làm sao để có thể thực hiện các xử lý khác sau khi hoàn thành lời gọi? bạn hẳn sẽ cười khẩy vì có sẵn câu trả lời, promise. Nhưng hôm nay tôi muốn nói đến vấn đề khác, về ý nghĩa của "multi-threading" trong javascript.

Promise

Tóm tắt lại thì promise là một đối tượng đại diện cho kết quả của các tiến trình asynchronous, và bạn có thể thao tác với promise như đăng ký các hàm xử lý sau khi tiến trình đó hoàn thành (hoặc bị lỗi). Bạn có thể tạo ra các chuỗi xử lý như: sau khi thực hiện xong task 1 thì chạy task 2, nếu task 2 lỗi thì chạy task 3...

Nhưng ko phải tất cả các APIs đều có hỗ trợ promise.

ví dụ folder exploration

Trong ví dụ này, nhiệm vụ đưa ra là liệt kê tất cả các file và folders với path được cho trước bằng cách sử dụng NodeJS.

NodeJS cung cấp sẵn cho ta 2 File API sau:

  • Synchronous: fs.statSync, fs.readFileSync.
  • Asynchronous: fs.stat, fs.readFile.

Synchronous version

Rõ ràng, cách sử dụng synchronous khá đơn giản khi chỉ việc nhận lấy thông tin file trả về từ API và xử lý tiếp sau đó theo 1 cách tuần tự.

var fs = require("fs"),
    path = require("path"),
    start = new Date(),
    files,
    key,
    count;

function exploreSync(currentPath, result) {
    "use strict";
    var stat = fs.statSync(currentPath);
    if (undefined === result) {
        result = {};
    }
    if (stat.isDirectory()) {
        var list = fs.readdirSync(currentPath),
            len,
            idx;
        len = list.length;
        for (idx = 0; idx < len; ++idx) {
            exploreSync(path.join(currentPath, list[idx]), result);
        }
    } else {
        result[currentPath] = fs.readFileSync(currentPath);
    }
    return result;
}

files = exploreSync(process.argv[2]);

console.log("Here we can use files dictionary:");
count = 0;
for (key in files) {
    if (files.hasOwnProperty(key)) {
        ++count;
        console.log(key + " " + files[key].length);
    }
}
console.log("Count: " + count);
console.log("Spent " + (new Date() - start) + "ms");

result

Synchronous vs. Asynchronous

Không phải vô cớ mà NodeJS lại cung cấp cho chúng ta tính năng asynchronous, lý do ở đây là javascript engine là mono-thread. Có nghĩa là, nếu bạn có một NodeJS server để cung cấp chức năng exploreSync thì server sẽ luôn quá tải vì phải xử lý các request một cách tuần tự. Rõ ràng nó không phù hợp để là 1 "server"!

Asynchronous dựa trên một ý tưởng đơn giản là việc truy cập và đọc file sẽ được thưcj hiện bởi hệ điều hành. Trong khi đó thì NodeJS server của chúng ta có thể thực hiện các tác vụ khác. Từ đó nó có thể phục vụ nhiều request 1 lúc, chính là multi-threading!

Nhưng lại có một vấn đề nữa nảy sinh, đó là việc bắt lỗi. Với synchronous, chúng ta chỉ việc handle exception. Trong khi đó với asynchronous, lỗi có xảy ra hay không lại phụ thuộc vào thời điểm hoàn thành tác vụ đó bởi hệ điều hành. Tuy nhiên, NodeJS có cơ chế trả về lỗi thông qua hàm callback, nên đây chính là điểm khác biệt khi handle error so với synchronous.

Ví dụ asynchronous

var fs = require("fs"),
    path = require("path"),
    start = new Date(),
    files,
    key,
    count;

function exploreAsync(currentPath, result) {
    "use strict";
    if (undefined === result) {
        result = {};
    }
    fs.stat(currentPath, function (err, stat) {
        if (err) {
            console.error(err);
        } else if (stat && stat.isDirectory()) {
            fs.readdir(currentPath, function (err, list) {
                var len,
                    idx;
                if (err) {
                    console.error(err);
                } else {
                    len = list.length;
                    for (idx = 0; idx < len; ++idx) {
                        exploreAsync(path.join(currentPath, list[idx]), result);
                    }
                }
            });
        } else {
            fs.readFile(currentPath, function (err, data) {
                if (err) {
                    console.error(err);
                } else {
                    result[currentPath] = data;
                }
            });
        }
    });
}

files = exploreAsync(process.argv[2]);

console.log("Here we can use files dictionary?");
count = 0;
for (key in files) {
    if (files.hasOwnProperty(key)) {
        ++count;
        console.log(key + " " + files[key].length);
    }
}
console.log("Count: " + count);
console.log("Spent " + (new Date() - start) + "ms");

async

Asynchronous chạy lỗi sao?

vậy hãy thử trace xem sao:

var fs = require("fs"),
    path = require("path"),
    start = new Date(),
    files,
    key,
    count;

function exploreAsync(currentPath, result) {
    "use strict";
    if (undefined === result) {
        result = {};
    }
    console.log(">> fs.stat(" + currentPath + ")");
    fs.stat(currentPath, function (err, stat) {
        console.log("<< fs.stat(" + currentPath + ")");
        if (err) {
            console.error(err);
        } else if (stat && stat.isDirectory()) {
            console.log(">> fs.readdir(" + currentPath + ")");
            fs.readdir(currentPath, function (err, list) {
                console.log("<< fs.readdir(" + currentPath + ")");
                var
                    len,
                    idx;
                if (err) {
                    console.error(err);
                } else {
                    len = list.length;
                    for (idx = 0; idx < len; ++idx) {
                        exploreAsync(path.join(currentPath, list[idx]), result);
                    }
                }
            });
        } else {
            console.log(">> fs.readFile(" + currentPath + ")");
            fs.readFile(currentPath, function (err, data) {
                console.log("<< fs.readFile(" + currentPath + ")");
                if (err) {
                    console.error(err);
                } else {
                    result[currentPath] = data;
                }
            });
        }
    });
}

files = exploreAsync(process.argv[2]);

console.log("Here we can use files dictionary?");
count = 0;
for (key in files) {
    if (files.hasOwnProperty(key)) {
        ++count;
        console.log(key + " " + files[key].length);
    }
}
console.log("Count: " + count);
console.log("Spent " + (new Date() - start) + "ms");

async_trace

Vì thao tác đọc file được thực hiện asynchronous nên việc tính toán của chúng ta đã diễn ra trước khi thao tác đọc file được hoàn thành!

Chúng ta có cách để giải quyết vấn đề này.

Cách truyền thống

Để biết khi nào thao tác đọc file được hoàn thành chúng ta phải truyền callback tính toán vào hàm exploreAsync

function exploreAsync(currentPath, callback, result) {//...

Sau đó là sử dụng biến đếm để theo dấu việc hoàn thành thao tác đọc, ví dụ mỗi lần bắt đầu đọc chúng ta tăng lên 1, khi hoàn thành thì giảm đi một, khi biến đếm trở về 0 có nghĩa là thao tác đọc đã hoàn thành.

var fs = require("fs"),
    path = require("path"),
    start = new Date(),
    key,
    count,
    pendingCount = 0;

function exploreAsync(currentPath, callback, result) {
    "use strict";
    if (undefined === result) {
        result = {};
    }
    ++pendingCount;
    fs.stat(currentPath, function (err, stat) {
        if (err) {
            console.error(err);
        } else if (stat && stat.isDirectory()) {
            ++pendingCount;
            fs.readdir(currentPath, function (err, list) {
                var
                    len,
                    idx;
                if (err) {
                    console.error(err);
                } else {
                    len = list.length;
                    for (idx = 0; idx < len; ++idx) {
                        exploreAsync(path.join(currentPath, list[idx]),
                            callback, result);
                    }
                }
                if (0 === --pendingCount) {
                    callback(result);
                }
            });
        } else {
            ++pendingCount;
            fs.readFile(currentPath, function (err, data) {
                if (err) {
                    console.error(err);
                } else {
                    result[currentPath] = data;
                }
                if (0 === --pendingCount) {
                    callback(result);
                }
            });
        }
        if (0 === --pendingCount) {
            callback(result);
        }
    });
}

exploreAsync(process.argv[2], function (files) {
    "use strict";
    console.log("Here we can use files dictionary");
    count = 0;
    for (key in files) {
        if (files.hasOwnProperty(key)) {
            ++count;
            console.log(key + " " + files[key].length);
        }
    }
    console.log("Count: " + count);
    console.log("Spent " + (new Date() - start) + "ms");
});

kết quả đúng như chúng ta mong đợi

async_count

Tuy nhiên, dường như cách này vẫn chưa thực sự cho chúng ta kết quả tốt nhất, hãy thử gọi hàm exploreAsync nhiều hơn 1 lần xem sao

/*...*/
function result (files) {
    "use strict";
    console.log("Here we can use files dictionary");
    count = 0;
    for (key in files) {
        if (files.hasOwnProperty(key)) {
            ++count;
        }
    }
    console.log("Count: " + count);
    console.log("Spent " + (new Date() - start) + "ms");
}

exploreAsync(process.argv[2], result);
exploreAsync(process.argv[2], result);
exploreAsync(process.argv[2], result);

và kêt quả:

async_count_more_than_once

Chúng ta chỉ nhận được kết quả 1 lần duy nhất, bởi vì biến đếm pendingCount là biến toàn cục. Để giải quyết vấn đề chúng ta có 2 cách:

  1. Tạo ra một đối tượng để lưu trữ pendingCount và truyền nó vào như một tham số.
  2. Tạo ra closure để lưu trữ pendingCount như là một private scope.

Chúng ta có thể implement cách 2 như sau:

var fs = require("fs"),
    path = require("path"),
    start = new Date(),
    key,
    count;

function exploreAsync(currentPath, callback) {
    "use strict";
    var
        result = {},
        pendingCount = 0;
    function _explore(currentPath) {
        ++pendingCount;
        fs.stat(currentPath, function (err, stat) {
            if (err) {
                console.error(err);
            } else if (stat && stat.isDirectory()) {
                ++pendingCount;
                fs.readdir(currentPath, function (err, list) {
                    var
                        len,
                        idx;
                    if (err) {
                        console.error(err);
                    } else {
                        len = list.length;
                        for (idx = 0; idx < len; ++idx) {
                            _explore(path.join(currentPath, list[idx]));
                        }
                    }
                    if (0 === --pendingCount) {
                        callback(result);
                    }
                });
            } else {
                ++pendingCount;
                fs.readFile(currentPath, function (err, data) {
                    if (err) {
                        console.error(err);
                    } else {
                        result[currentPath] = data;
                    }
                    if (0 === --pendingCount) {
                        callback(result);
                    }
                });
            }
            if (0 === --pendingCount) {
                callback(result);
            }
        });
    }
    _explore(currentPath);
}

function result (files) {
    "use strict";
    console.log("Here we can use files dictionary");
    count = 0;
    for (key in files) {
        if (files.hasOwnProperty(key)) {
            ++count;
        }
    }
    console.log("Count: " + count);
    console.log("Spent " + (new Date() - start) + "ms");
}

exploreAsync(process.argv[2], result);
exploreAsync(process.argv[2], result);
exploreAsync(process.argv[2], result);

voila!

use scope

Bài học

Việc xử lý các thao tác asynchronous khá phức tạp, giống như việc lập trình multi-threading vậy. Tuy nhiên với Javascript, bản chất là mono-thread, nên bạn sẽ không phải động tới vấn đề truy cập đồng thời vào cùng 1 biến.

Và với cách lập trình này chúng ta thu được:

  1. các lời gọi non-blocking, nghĩa là NodeJS server sẽ nhanh hơn.
  2. Cải thiện performance khi các tác vụ được thực hiện đồng thời!

tham khảo: http://gpf-js.blogspot.com/2015/02/asynchronous-synchronisation.html

0