淺談非同步程式設計

鄭大路發表於2018-06-02

執行緒與程式

眾所周知,Javascript的執行環境是'單執行緒'。執行緒的定義又是什麼呢?在說到執行緒之前我們先了解下程式。

程式:程式指正在執行的程式。確切的來說,當一個程式進入記憶體執行,即變成一個程式,程式是處於執行過程中的程式,並且具有一定獨立功能。

執行緒:執行緒是程式中的一個執行單元,負責當前程式中程式的執行,一個程式中至少有一個執行緒。一個程式中是可以有多個執行緒的,這個應用程式也可以稱之為多執行緒程式。

一個程式執行後至少有一個程式,一個程式可以包含多個執行緒。

多執行緒對於高併發量就有益,但多執行緒程式會出現多個執行緒對一個資源進行訪問的問題。具體的解決方案是利用鎖。單執行緒就不會出現此類的問題。但單執行緒如果出現堵塞,就會而獨佔cpu導致其他程式碼不能執行。其解決方案就是非同步程式設計。node是單執行緒,Java是多執行緒。

nodejs裡面的非同步。

nodejs裡面大部分的api都是非同步,少量的api是同步的。I/O操作會比較耗時但不會獨佔CPU,典型的I/O比如檔案讀寫,遠端資料庫讀寫,網路請求等.耗時的解決方案就是非同步。在node.js程式裡面,有一個使用者執行緒(javascript所宣稱的單執行緒)和一個非同步執行緒池(使用者無法直接訪問), 如果跑在非同步執行緒上的程式碼是阻塞的,那麼這種非同步根本就起不到消除阻塞的作用.原因就是阻塞程式碼會霸佔cpu,導致本程式所有程式碼都等待不管是哪個執行緒。但是node.js裡面的I/O API都是不會霸佔CPU的,所以是非阻塞的,就不會出現這個問題。這就是node.js的最引以為傲的特性之一:非同步非阻塞I/O.

javascript非同步程式設計的方式

  • 回撥函式。

回撥函式用來定義一次性響應的邏輯。比如說對於資料庫查詢,可以指定一個回撥函式來處理如何處理查詢結果。

function f2(){
  console.log("f1任務程式碼")
}
function f1(callback){

        setTimeout(function () {

        // f1的任務程式碼

            callback();

        }, 1000);
}

f1(f2);
複製程式碼

這裡的非同步api是瀏覽器提供的setTimeout,回撥函式callback會被放入事件佇列裡面,不會阻塞cpu,也就是說程式碼會繼續往下執行.等setTimeOut完成後,再執行callback。

var http = require("http");
var fs = require("fs");

http.createServer(function (req, res) {
    if(req.url == '/'){
        fs.readFile('./title.json',function (err, data) {
            if(err){
                console.log(err);
                res.end('Server error');
            }else {
                var titles = JSON.parse(data.toString());
                
                fs.readFile('./template.html',function (err, data) {
                    if(err){
                        console.log(err);
                        res.end('Server error');
                    }else {
                        var templ = data.toString();
                        var html = templ.replace('%', titles.join('<li></li>'));
                        res.writeHead(200, {'Content-Type': 'text/html'});
                        res.end(html);
                    }
                })
            }
        })
    }
}).listen(8000, "127.0.0.1");
複製程式碼

上面的例子就是node的一些I/O操作。回撥函式層層巢狀,這也就是傳說中的'callback hell'.寫起來很爽,簡單、容易理解.但維護的人估計得瘋。解決方法是建立中間函式以減少巢狀,或者是node中慣用手法減少if/else引起的巢狀:儘早在函式中返回。詳情請見node in action一書中3-2;

  • 事件監聽。

本質也是一個回撥,但它和一個概念實體(事件)有關聯。點選滑鼠是一個事件。當然在伺服器端,當有HTTP請求過來的時候,HTTP伺服器會發出一個請求事件。你所要做的就是監聽那個請求事件,並新增一些響應邏輯。

談到事件,我們就必須要說到觀察者模式(observer pattern)。這種模式在程式設計屆無處不在。阮一峰給出的定義是:

我們假定,存在一個"訊號中心",某個任務執行完成,就向訊號中心"釋出"(publish)一個訊號,其他任務可以向訊號中心"訂閱"(subscribe)這個訊號,從而知道什麼時候自己可以開始執行。這就叫做"釋出/訂閱模式"(publish-subscribe pattern),又稱"觀察者模式"(observer pattern)。

我一直堅信程式是生活的抽象。不要以為你每天面對的是一個機器,其實它也是一個世界。坐臥鋪車,列車員收走車票,我們訂閱這個事件,就相當於訂閱者,列車員相當於觀察者。等車到了站,釋出這個事件,列車員就會來叫我們。我們執行下車這個回撥。

這種模式有多種的實現,而他的應用就不勝列舉了,比如rxjs裡面的響應式資料,redux裡面的subscribe等等.

node裡面的http伺服器例項就是一個事件發射器,也是這種模式的應用。

該例項來自於node in action.

var events = require("events");
var net = require("net");

var channel = new events.EventEmitter();

channel.clients = {};

channel.subscriptions = {};

channel.on('join',function (id, client) {
    this.clients[id] = client;
    this.subscriptions[id] = function (senderId, message) {
        if(id != senderId){
            this.clients[id].write(message);
        }
    };
    var welcome = "Welcome!\n" + "Guests online " + this.listeners('broadcast').length;
    client.write(welcome + '\n');
    this.on('broadcast', this.subscriptions[id]);
});

//當有使用者離開時,通知其他使用者;
channel.on('leave',function (id) {
   channel.removeListener('broadcast',this.subscriptions[id]);
    channel.emit('broadcast',id, id + "has left the chat.\n");
});

var server = net.createServer(function (client) {
    var id = client.remoteAddress + ': ' + client.remotePort;
    // client.on('connect', function () {
        channel.emit('join', id, client);
    // });
    client.on('data', function (data) {
        var data = data.toString();
        channel.emit('broadcast', id, data);
    });
    client.on('close',function () {
        channel.emit('leave',id);
    })
});

server.listen(8888);
複製程式碼

這裡channel物件繼承自EventEmitter,利用on訂閱事件,emit釋出事件。執行相應的回撥邏輯。EventEmitter核心是事件觸發與事件監聽器功能的封裝,大多數模組多是繼承它。fs,net,http.

  • Promise物件。

Promises物件是CommonJS工作組提出的一種規範,目的是為非同步程式設計提供統一介面。簡單說,它的思想是,每一個非同步任務返回一個Promise物件,該物件有一個then方法,允許指定回撥函式。 這裡簡單介紹下Promise.想要就具體介紹可以瞭解下你不知道的JavaScript 中和阮一峰的es6入門

Promise只是改善了回撥函式的寫法,使回撥函式變成了鏈式寫法。並且可以執行多次的回撥。廢話不多說,直接上程式碼。


const promise = new Promise(function(resolve, reject) {
  // ... 這裡可以做一些非同步的操作;

  if (/* 非同步操作成功 */){
    resolve(value);
  } else {
    reject(error);
  }
});

promise.then(function(value) {
  // success
}, function(error) {
  // failure
});

複製程式碼
  • Generator函式.

執行Generator函式後會返回一個遍歷器物件(Iterator)。本質上Generator是一個狀態機,每次的遍歷Generator函式返回的都是函式內部的每個狀態。

function* gen(){
    yield 1;
    yield 2;
    return 1;
}

var g = gen();

gen.next() // {value:1,done:false}

gen.next() // {value:2,done:false}

gen.next() // {value:3,done:true}

複製程式碼

呼叫Generator函式,返回遍歷器物件,代表Generator函式內部指標。每次呼叫next方法,就會返回一個value和done屬性的物件,value是yield後面表示式的值。done表示便利是否結束。

Generator函式的非同步程式設計;

var fetch = require('node-fetch');

function* gen(){
  var url = 'https://api.github.com/users/github';
  var result = yield fetch(url);
  console.log(result.bio);
}

var g = gen();
var result = g.next();

result.value.then(function(data){
  return data.json();
}).then(function(data){
  g.next(data);
});

複製程式碼

這裡的fetch請求資料。相當於ajax. result.value是fetch(url)返回的值是一個Promise. 所以可以用then語法。這裡注意下yield語句本身沒有返回值。在next中引數將作為上一個yield的返回值。

  • async和await async函式是Generator函式的語法糖。
const fs = require('fs');

const readFile = function (fileName) {
  return new Promise(function (resolve, reject) {
    fs.readFile(fileName, function(error, data) {
      if (error) return reject(error);
      resolve(data);
    });
  });
};

// Generator函式版本

const gen = function* () {
  const f1 = yield readFile('/etc/fstab');
  const f2 = yield readFile('/etc/shells');
  console.log(f1.toString());
  console.log(f2.toString());
};

// async函式版本。
const asyncReadFile = async function () {
  const f1 = await readFile('/etc/fstab');
  const f2 = await readFile('/etc/shells');
  console.log(f1.toString());
  console.log(f2.toString());
};

複製程式碼

我們可以發現sync函式就是將 Generator 函式的星號(*)替換成async,將yield替換成await,僅此而已。 但有一些區別async函式的返回值是 Promise 物件,這比 Generator 函式的返回值是 Iterator 物件方便多了。你可以用then方法指定下一步的操作。

進一步說,async函式完全可以看作多個非同步操作,包裝成的一個 Promise 物件,而await命令就是內部then命令的語法糖。

相關文章