Chapter2:Asynchronous Control Flow Patterns
從一個同步式程式設計風格遷移到Node平臺(在Node架構中,持續傳遞風格和非同步介面是常用方式),將是一件令人受挫的事情。編寫非同步程式碼可能是一種與眾不同的體驗,特別是設計到控制流的時候。一些簡單的問題,比如說:對一系列檔案進行迭代、執行連續的一系列任務、或者等待一系列執行被完成,需要開發者採取新的方法或者技術來避免寫出低效或者可讀性差的程式碼。一種常見的問題是陷入回撥地域問題的陷阱,並且看到程式碼規模橫向發展而不是縱向,伴隨著巢狀,使得即使是簡單的日常工作也變得難以維護和閱讀。
在這篇文章中,我們可以看到它實際上可能馴服回撥,並且能夠通過設計模式和一些原則寫出乾淨、可控制的非同步程式碼。我們將看到如何控制流庫,如非同步,可以明顯簡化我們的問題,同時我們也能發現持續傳遞方式並不是唯一的方式來實現非同步介面。事實上,我們將學會Promises和ECMAScript6中的generators如何成為有力而靈活的替代方案。對於每一個範例而言,我們將學習有助於幫助我們實現最常見控制流的模式,並且在最後一章,我們將準備有足夠的自信去編寫乾淨並且高效的非同步程式碼的。
The difficulties of asynchronous programming
在JavaScript中,非同步程式碼失去控制,毫無疑問是十分容易的。閉包和就地定義匿名函式的方式允許一種平滑過渡的程式設計經驗,而不需要開發者跳轉到程式碼庫中的其他點。這完全吻合原理:它使程式碼簡潔,保持程式碼的流暢性,並且能在更短的時間內工作。不幸的是,犧牲程式碼質量,比如說:模組化、可重用性、可維護性,遲早會導致回撥巢狀的失控擴散,函式規模的臃腫,並且導致糟糕的程式碼組織。大多數時間下,建立閉包並不是函式功能的需要。所以它更像原則問題,而不是一個與非同步程式設計相關的問題。意識到我們的程式碼正在變得更醜陋或者更好,更進一步直到如何讓程式碼不醜陋並採用合適的方式是區分新手和專家的標準。
Creating a simple web spider
為了解釋這個問題,我們將創造一個小小的網路爬蟲,一個命令列應用程式,以URL作為輸入,並將其內容本地下載到一個檔案當中。在本章提供的程式碼中,我們將使用一些npm依賴:
•request:一個庫來精簡HTTP呼叫。
•mkdirp:一個小的實用應用程式來建立目錄遞迴。
此外,我們將經常引用一個名為./utilities的本地模組,其中包括一些我們將在應用程式中使用的輔助程式。我們為了簡潔忽略檔案中的內容,但是你可以找到完整的實現,伴隨一個package.json包括完整的依賴列表,本書的包下載提供在:http://www.packtpub.com.。
我們的應用中的核心函式被包括在一個名為spider.js的模組內部。我們來一起看看這段程式碼是什麼樣的。首先,我們先要下載我們將用到的所有依賴:
var request = require('request');
var fs = require('fs');
var mkdirp = require('mkdirp');
var path = require('path');
var utilities = require('./utilities');
接下來我將創造一個新的函式被稱為spider(),當下載過程完成時,將通過URL來下載並且回撥callback函式將被呼叫。
function spider(url, callback) {
var filename = utilities.urlToFilename(url);
fs.exists(filename, function(exists) { //[1]
if(!exists) {
console.log("Downloading " + url);
request(url, function(err, response, body) { //[2]
if(err) {
callback(err);
} else {
mkdirp(path.dirname(filename), function(err) { //[3]
if(err) {
callback(err);
} else {
fs.writeFile(filename, body, function(err) { //[4]
if(err) {
callback(err);
} else {
callback(null, filename, true);
}
});
}
});
}
});
} else {
callback(null, filename, false);
}
});
}
前述程式碼將會執行如下任務:
1.通過驗證相應的檔案是否已經完成建立,檢查URL是否已經完成下載:
fs.exists(filecodename, function(exists) …
2.如果找不到檔案,使用以下行程式碼來下載URL:
request(url, function(err, response, body) …
3.然後,我們將確定包含該檔案的目錄是否存在:
mkdirp(path.dirname(filename), function(err) …
4.最後我們將把HTTP響應的主體寫入檔案系統:
fs.writeFile(filename, body, function(err) …
為了完善我們的爬蟲應用程式,我們只需要通過提供一個URL作為輸入,來呼叫spider()(在我們的例子中,我們從命令列引數中讀取)。
spider(process.argv[2], function(err, filename, downloaded) {
if(err) {
console.log(err);
} else if(downloaded){
console.log('Completed the download of "'+ filename +'"');
} else {
console.log('"'+ filename +'" was already downloaded');
}
});
現在,首先我們準備去試試我們的網路爬蟲程式,首先,請確保你有utilities.js模組,並且package.json包含你整個工程目錄下的全部依賴。接下來通過執行一下程式碼來安裝所有的依賴,程式碼如下:
npm install
接下,來我們將執行spider模組來下載網頁的內容,伴隨著如下命令列:
node spider http://www.example.com
我們的web爬蟲應用程式需要我們總是包括協議頭(比如:http://),在我們所提供的URL當中。另外不要期望HTML連結會被重寫或者圖片資源會被下載,因為這只是一個簡單的例子來演示非同步程式設計如何工作。
Generators
ES6中的Generator的引入,極大程度上改變了JavaScript程式設計師對迭代器的看法,併為解決callback hell提供了新方法。
迭代器模式是很常用的設計模式,但是實現起來,很多東西是程式化的;當迭代規則比較複雜時,維護迭代器內的狀態,是比較麻煩的。 於是有了generator,何為generator?
Generators: a better way to build Iterators.
藉助 yield 關鍵字,可以更優雅的實現fibonacci數列:
function* fibonacci() {
let a = 0, b = 1;
while(true) {
yield a; [a, b] = [b, a + b];
} }
yield與非同步
yield可以暫停執行流程,那麼便為改變執行流程提供了可能。這和Python的coroutine類似。
Geneartor之所以可用來控制程式碼流程,就是通過yield來將兩個或者多個Geneartor的執行路徑互相切換。這種切換是語句級別的,而不是函式呼叫級別的。其本質是CPS變換。
yield之後,實際上本次呼叫就結束了,控制權實際上已經轉到了外部呼叫了generator的next方法的函式,呼叫的過程中伴隨著狀態的改變。那麼如果外部函式不繼續呼叫next方法,那麼yield所在函式就相當於停在yield那裡了。所以把非同步的東西做完,要函式繼續執行,只要在合適的地方再次呼叫generator 的next就行,就好像函式在暫停後,繼續執行。
Buffer
在Node.js中,Buffer類是隨Node核心一起釋出的核心庫。Buffer庫為Node.js帶來了一種儲存原始資料的方法,可以讓Nodejs處理二進位制資料,每當需要在Nodejs中處理I/O操作中移動的資料時,就有可能使用Buffer庫。原始資料儲存在 Buffer 類的例項中。一個 Buffer 類似於一個整數陣列,但它對應於 V8 堆記憶體之外的一塊原始記憶體。
Buffer 和 Javascript 字串物件之間的轉換需要顯式地呼叫編碼方法來完成。以下是幾種不同的字串編碼:
‘ascii’ – 僅用於 7 位 ASCII 字元。這種編碼方法非常快,並且會丟棄高位資料。
‘utf8’ – 多位元組編碼的 Unicode 字元。許多網頁和其他檔案格式使用 UTF-8。
‘ucs2’ – 兩個位元組,以小尾位元組序(little-endian)編碼的 Unicode 字元。它只能對 BMP(基本多文種平面,U+0000 – U+FFFF) 範圍內的字元編碼。
‘base64’ – Base64 字串編碼。
‘binary’ – 一種將原始二進位制資料轉換成字串的編碼方式,僅使用每個字元的前 8 位。這種編碼方法已經過時,應當儘可能地使用 Buffer 物件。
'hex' - 每個位元組都採用 2 進位制編碼。
在Buffer中建立一個陣列,需要注意以下規則:
Buffer 是記憶體拷貝,而不是記憶體共享。
Buffer 佔用記憶體被解釋為一個陣列,而不是位元組陣列。比如,new Uint32Array(new Buffer([1,2,3,4])) 建立了4個 Uint32Array,它的成員為 [1,2,3,4] ,而不是[0x1020304] 或 [0x4030201]。
slab 分配
在 lib/buffer.js 模組中,有個模組私有變數 pool, 它指向當前的一個8K 的slab :
Buffer.poolSize = 8 * 1024;
var pool;
function allocPool() {
pool = new SlowBuffer(Buffer.poolSize);
pool.used = 0;
}
SlowBuffer 為 src/node_buffer.cc 匯出,當使用者呼叫new Buffer時 ,如果你要申請的空間大於8K,node 會直接呼叫SlowBuffer ,如果小於8K ,新的Buffer 會建立在當前slab 之上:
新建立的Buffer的 parent成員變數會指向這個slab ,
offset 變數指向在這個slab 中的偏移:
if (!pool || pool.length - pool.used < this.length) allocPool();
this.parent = pool;
this.offset = pool.used;
pool.used += this.length;
在 lib/_tls_legacy.js 中,SlabBuffer建立了一個 10MB 的 slab:
function alignPool() {
// Ensure aligned slices
if (poolOffset & 0x7) {
poolOffset |= 0x7; poolOffset++;
}
}
這裡做了8位元組的記憶體對齊處理。
如果不按照平臺要求對資料存放進行對齊,會帶來存取效率上的損失。比如32位的Intel處理器通過匯流排訪問(包括讀和寫)記憶體資料。每個匯流排週期從偶地址開始訪問32位記憶體資料,記憶體資料以位元組為單位存放。如果一個32位的資料沒有存放在4位元組整除的記憶體地址處,那麼處理器就需要2個匯流排週期對其進行訪問,顯然訪問效率下降很多。
Node.js 是一個跨平臺的語言,第三方的C++ addon 也是非常多,避免破壞了第三方模組的使用,比如 directIO 就必須要記憶體對齊。
淺拷貝
Buffer更像是可以做指標操作的C語言陣列。例如,可以用[index]方式直接修改某個位置的位元組。 需要注意的是:Buffer#slice 方法, 不是返回一個新的Buffer, 而是返回對原 Buffer 某個區間數值的引用。
const buf1 = Buffer.allocUnsafe(26);
for (var i = 0 ; i < 26 ; i++) {
buf1[i] = i + 97; // 97 is ASCII a
}
const buf2 = buf1.slice(0, 3);
buf2.toString('ascii', 0, buf2.length);
// Returns: 'abc'
buf1[0] = 33;
buf2.toString('ascii', 0, buf2.length);
// Returns : '!bc'
官方 API 提供的例子,buf2是對buf1前3個位元組的引用,對buf2的修改就相當於作用在buf1上。
深拷貝
如果想要拷貝一份Buffer,得首先建立一個新的Buffer,並通過.copy方法把原Buffer中的資料複製過去。
const buf1 = Buffer.allocUnsafe(26);
const buf2 = Buffer.allocUnsafe(26).fill('!');
for (let i = 0 ; i < 26 ; i++) {
buf1[i] = i + 97; // 97 is ASCII a
}
buf1.copy(buf2, 8, 16, 20);
console.log(buf2.toString('ascii', 0, 25));
// Prints: !!!!!!!!qrst!!!!!!!!!!!!!
通過深拷貝的方式,buf2擷取了buf1的部分內容,之後對buf2的修改並不會作用於buf1, 兩者內容獨立不共享。需要注意的事:深拷貝是一種消耗 CPU 和記憶體的操作,需要非常謹慎。
記憶體碎片
動態分配將不可避免會產生記憶體碎片的問題,那麼什麼是記憶體碎片? 記憶體碎片即“碎片的記憶體”描述一個系統中所有不可用的空閒記憶體,這些碎片之所以不能被使用,是因為負責動態分配記憶體的分配演算法使得這些空閒的記憶體無法使用。
上述的 slab 分配,存在明顯的記憶體碎片,即 8KB 的記憶體並沒有完全被使用,存在一定的浪費。通用的slab實現,會浪費約1/2的空間。
當然存在更高效,更省記憶體的記憶體管理分配,比如 tcmalloc, 但也必須承受一定的管理代價。node.js 在這方面並沒有一味的執著於此,而是達到一種效能與空間使用的平衡。
PM2原始碼淺析
近年來,大前端和全棧的思潮下,很多公司的專案轉成了node驅動,pm2做為一個帶有負載均衡功能的程式管理器,是眾多公司的主流方案。
PM2工作原理:
要理解pm2就要理解god和santan的關係:god的職責是守護程式,重啟程式。santan的職責是異常程式的退出,殺死程式,毀滅程式等工作。
god和santan通訊的方式,是RPC:
RPC(Remote Procedure Call Protocol)——遠端過程呼叫協議,它是一種通過網路從遠端計算機程式上請求服務,而不需要了解底層網路技術的協議。RPC協議假定某些傳輸協議的存在,如TCP或UDP,為通訊程式之間攜帶資訊資料。在OSI網路通訊模型中,RPC跨越了傳輸層和應用層。RPC使得開發包括網路分散式多程式在內的應用程式更加容易。
總結
pm2的叢集,從原理是採用cluster.fork來實現的,深入理解cluser模組,精度pm2的原始碼,能更好的理解pm2,更好的理解node設計思想
ES7+ES8新趨勢與非同步處理:
ES7新特性:
Array.prototype.includes()方法:
includes()的作用,是查詢一個值在不在陣列裡,若在,則返回true,反之返回false。 基本用法:
['a', 'b', 'c'].includes('a') // true
['a', 'b', 'c'].includes('d') // false
Array.prototype.includes()方法接收兩個引數:要搜尋的值和搜尋的開始索引。當第二個引數被傳入時,該方法會從索引處開始往後搜尋(預設索引值為0)。若搜尋值在陣列中存在則返回true,否則返回false。 且看下面示例:
['a', 'b', 'c', 'd'].includes('b') // true
['a', 'b', 'c', 'd'].includes('b', 1) // true
['a', 'b', 'c', 'd'].includes('b', 2) // false
那麼,我們會聯想到ES6裡陣列的另一個方法indexOf,下面的示例程式碼是等效的:
['a', 'b', 'c'].includes('a') //true
['a', 'b', 'c'].indexOf('a') > -1 //true
*非同步函式(Async functions)
為什麼要引入async?
眾所周知,JavaScript語言的執行環境是“單執行緒”的,那麼非同步程式設計對JavaScript語言來說就顯得尤為重要。以前我們大多數的做法是使用回撥函式來實現JavaScript語言的非同步程式設計。回撥函式本身沒有問題,但如果出現多個回撥函式巢狀,例如:進入某個頁面,需要先登入,拿到使用者資訊之後,調取使用者商品資訊,程式碼如下:
this.$http.jsonp('/login', (res) => {
this.$http.jsonp('/getInfo', (info) => {
// do something
})
})
假如上面還有更多的請求操作,就會出現多重巢狀。程式碼很快就會亂成一團,這種情況就被稱為“回撥函式地獄”(callback hell)。
於是,我們提出了Promise,它將回撥函式的巢狀,改成了鏈式呼叫。寫法如下:
var promise = new Promise((resolve, reject) => {
this.login(resolve)
})
.then(() => this.getInfo())
.catch(() => { console.log("Error") })
從上面可以看出,Promise的寫法只是回撥函式的改進,使用then方法,只是讓非同步任務的兩段執行更清楚而已。Promise的最大問題是程式碼冗餘,請求任務多時,一堆的then,也使得原來的語義變得很不清楚。此時我們引入了另外一種非同步程式設計的機制:Generator。
Generator 函式是一個普通函式,但是有兩個特徵。一是,function關鍵字與函式名之間有一個星號;二是,函式體內部使用yield表示式,定義不同的內部狀態(yield在英語裡的意思就是“產出”)。一個簡單的例子用來說明它的用法:
function* helloWorldGenerator() {
yield 'hello';
yield 'world';
return 'ending';
}
var hw = helloWorldGenerator();
上面程式碼定義了一個 Generator 函式helloWorldGenerator,它內部有兩個yield表示式(hello和world),即該函式有三個狀態:hello,world 和 return 語句(結束執行)。Generator 函式的呼叫方法與普通函式一樣,也是在函式名後面加上一對圓括號。不同的是,呼叫 Generator 函式後,該函式並不執行,返回的也不是函式執行結果,而是一個指向內部狀態的指標物件,必須呼叫遍歷器物件的next方法,使得指標移向下一個狀態。也就是說,每次呼叫next方法,內部指標就從函式頭部或上一次停下來的地方開始執行,直到遇到下一個yield表示式(或return語句)為止。換言之,Generator 函式是分段執行的,yield表示式是暫停執行的標記,而next方法可以恢復執行。上述程式碼分步執行如下:
hw.next()
// { value: 'hello', done: false }
hw.next()
// { value: 'world', done: false }
hw.next()
// { value: 'ending', done: true }
hw.next()
// { value: undefined, done: true }
Generator函式的機制更符合我們理解的非同步程式設計思想。
使用者登入的例子,我們用Generator來寫,如下:
var gen = function* () {
const f1 = yield this.login()
const f2 = yield this.getInfo()
};
雖然Generator將非同步操作表示得很簡潔,但是流程管理卻不方便(即何時執行第一階段、何時執行第二階段)。此時,我們便希望能出現一種能自動執行Generator函式的方法。我們的主角來了:async/await。
ES8引入了async函式,使得非同步操作變得更加方便。簡單說來,它就是Generator函式的語法糖。
async function asyncFunc(params) {
const result1 = await this.login()
const result2 = await this.getInfo()
}
更加簡潔易懂
變體,非同步函式存在以下四種使用形式:
函式宣告:asyncfunctionfoo(){}
函式表示式:constfoo=asyncfunction(){}
物件的方式:letobj={asyncfoo(){}}
箭頭函式:constfoo=async()=>{}
常見用法彙總:
處理單個非同步結果:
async function asyncFunc() {
const result = await otherAsyncFunc();
console.log(result);
}
順序處理多個非同步結果:
async function asyncFunc() {
const result1 = await otherAsyncFunc1();
console.log(result1);
const result2 = await otherAsyncFunc2();
console.log(result2);
}
並行處理多個非同步結果:
async function asyncFunc() {
const [result1, result2] = await Promise.all([
otherAsyncFunc1(), otherAsyncFunc2()
]);
console.log(result1, result2);
}
處理錯誤:
async function asyncFunc() {
try {
await otherAsyncFunc();
} catch (err) {
console.error(err);
}
}
網路 (Net)
網路模型
ISO制定的OSI參考模型的過於龐大、複雜招致了許多批評。與此對照,由技術人員自己開發的TCP/ IP協議棧獲得了更為廣泛的應用。如圖所示,是TCP/IP參考模型和OSI參考模型的對比示意圖。
UDP vs TCP
TCP(Transmission Control Protocol):傳輸控制協議
UDP(User Datagram Protocol):使用者資料包協議
主要在於連線性(Connectivity)、可靠性(Reliability)、有序性(Ordering)、有界性(Boundary)、擁塞控制(Congestion or Flow control)、傳輸速度(Speed)、量級(Heavy/Light weight)、頭部大小(Header size)等差異。
主要差異:
1.TCP是面向連線(Connection oriented)的協議,UDP是無連線(Connection less)協議;
TCP用三次握手建立連線:1) Client向server傳送SYN;2) Server接收到SYN,回覆Client一個SYN-ACK;3)Client接收到SYN_ACK,回覆Server一個ACK。到此,連線建成。
UDP傳送資料前不需要建立連線。
2.TCP可靠,UDP不可靠;
TCP丟包會自動重傳,UDP不會。
3.TCP有序,UDP無序;
訊息在傳輸過程中可能會亂序,後傳送的訊息可能會先到達,TCP會對其進行重排序,UDP不會。從程式實現的角度來看,可以用下圖來進行描述。
從上圖也能清晰的看出,TCP通訊需要伺服器端偵聽listen、接收客戶端連線請求accept,等待客戶端connect建立連線後才能進行資料包的收發(recv/send)工作。而UDP則伺服器和客戶端的概念不明顯,伺服器端即接收端需要繫結埠,等待客戶端的資料的到來。後續便可以進行資料的收發(recvfrom/sendto)工作。
Socket 抽象
Socket 是對 TCP/IP 協議族的一種封裝,是應用層與TCP/IP協議族通訊的中間軟體抽象層。它把複雜的TCP/IP協議族隱藏在Socket介面後面,對使用者來說,一組簡單的介面就是全部,讓Socket去組織資料,以符合指定的協議。
Socket 還可以認為是一種網路間不同計算機上的程式通訊的一種方法,利用三元組(ip地址,協議,埠)就可以唯一標識網路中的程式,網路中的程式通訊可以利用這個標誌與其它程式進行互動。
Socket 起源於 Unix ,Unix/Linux 基本哲學之一就是“一切皆檔案”,都可以用“開啟(open) –> 讀寫(write/read) –> 關閉(close)”模式來進行操作。因此 Socket 也被處理為一種特殊的檔案。
TCP Socket
Node.js 的 Net模組也對 TCP socket 進行了抽象封裝:
function Socket(options) {
if (!(this instanceof Socket)) return new Socket(options);
this._connecting = false;
this._hadError = false;
this._handle = null;
this._parent = null;
this._host = null;
if (typeof options === 'number')
options = { fd: options }; // Legacy interface.
else if (options === undefined)
options = {};
stream.Duplex.call(this, options);
if (options.handle) {
this._handle = options.handle; // private
} else if (options.fd !== undefined) {
this._handle = createHandle(options.fd);
this._handle.open(options.fd);
if ((options.fd == 1 || options.fd == 2) &&
(this._handle instanceof Pipe) &&
process.platform === 'win32') {
// Make stdout and stderr blocking on Windows
var err = this._handle.setBlocking(true);
if (err)
throw errnoException(err, 'setBlocking');
}
this.readable = options.readable !== false;
this.writable = options.writable !== false;
} else {
// these will be set once there is a connection
this.readable = this.writable = false;
}
// shut down the socket when we're finished with it.
this.on('finish', onSocketFinish);
this.on('_socketEnd', onSocketEnd);
initSocketHandle(this);
// ...
}
util.inherits(Socket, stream.Duplex);
首先Socket是一個全雙工的 Stream,所以繼承了 Duplex。通過createHandle建立套接字並賦值到this._handle上。同時監聽finish,_socketEnd事件:
粘包
一般所謂的TCP粘包是在一次接收資料不能完全地體現一個完整的訊息資料。TCP通訊為何存在粘包呢?主要原因是TCP是以流的方式來處理資料,再加上網路上MTU的往往小於在應用處理的訊息資料,所以就會引發一次接收的資料無法滿足訊息的需要,導致粘包的存在。處理粘包的唯一方法就是制定應用層的資料通訊協議,通過協議來規範現有接收的資料是否滿足訊息資料的需要。
情況分析
TCP粘包通常在流傳輸中出現,UDP則不會出現粘包,因為UDP有訊息邊界,傳送資料段需要等待緩衝區滿了才將資料傳送出去,當滿的時候有可能不是一條訊息而是幾條訊息合併在換中去內,在成粘包;另外接收資料端沒能及時接收緩衝區的包,造成了緩衝區多包合併接收,也是粘包。
解決辦法
自定義應用層協議;
不使用Nagle演算法, 使用提供的 API:socket.setNoDelay。
UDP
UDP Socket:
function Socket(type, listener) {
EventEmitter.call(this);
if (typeof type === 'object') {
var options = type;
type = options.type;
}
var handle = newHandle(type);
handle.owner = this;
this._handle = handle;
this._receiving = false;
this._bindState = BIND_STATE_UNBOUND;
this.type = type; this.fd = null; // compatibility hack
// If true - UV_UDP_REUSEADDR flag will be set
this._reuseAddr = options && options.reuseAddr;
if (typeof listener === 'function')
this.on('message', listener);
}
util.inherits(Socket, EventEmitter);
UDP 繼承了EventEmitter, 同樣也支援 IPV4和 IPV6協議, 由type區分,this._reuseAddr標識是否要使用選項:SO_REUSEADDR。
SO_REUSEADDR允許完全重複的捆綁:當一個IP地址和埠繫結到某個套介面上時,還允許此IP地址和埠捆綁到另一個套介面上。一般來說,這個特性僅在支援多播的系統上才有,而且只對UDP套介面而言(TCP不支援多播)。
總結
儘量不要嘗試去使用UDP,除非知道丟包了對於應用是沒有影響的,否則排查網路丟包會很困難。
相關文章
- Control Flow 控制流程
- Swift-控制流(Control Flow)Swift
- Swift--控制流 (Control Flow)Swift
- .Net Core自實現CLR非同步程式設計模式(Asynchronous programming patterns)非同步程式設計設計模式
- STREAM的CAPTURE出現PAUSED FOR FLOW CONTROLAPT
- JavaScript statement flow control || JavaScript 語句流程控制JavaScript
- 《Windows 10 Control Flow Guard Internals》 Reading NotesWindows
- CSAPP英語學習系列:Chapter 8: Exceptional Control FlowAPPAPTException
- streams capture狀態PAUSED FOR FLOW CONTROL原因分析和診斷APT
- Reinforcement Learning Chapter2APT
- Asynchronous CommitMIT
- ORA-25307 ENQUEUE RATE TOO HIGH. ENABLE FLOW CONTROLENQ
- React PatternsReact
- (二)Flutter學習之Dart展開操作符 和 Control Flow CollectionsFlutterDart
- [翻譯] TensorFlow 分散式之論文篇 "Implementation of Control Flow in TensorFlow"分散式
- chapter2:協同過濾APT
- 非同步模式(Asynchronous)非同步模式
- No model,no patterns ,then no frameworkFramework
- Digital PatternsGit
- Simple FSM 3(asynchronous reset)
- Concurrency Patterns in GoGo
- ID3d11asynchronous3D
- 非同步提交(Asynchronous COMMIT)非同步MIT
- Simple FSM1(asynchronous reset)
- Simple FSM2(asynchronous reset)
- Glob Patterns匹配模式使用模式
- Patterns and Frameworks (my first topic)Framework
- Exploiting CVE-2015-0311, Part II: Bypassing Control Flow Guard on Windows 8.1Windows
- Git Dev FlowGitdev
- game development -- flowGAMdev
- Embedding flow
- asynchronous i/o (aio) on HP-UXAIUX
- Dynamics CRM Asynchronous Service Performance: Code ManiaORM
- Taming the asynchronous beast with ES7AST
- 《Python Cookbook v3.0.0》Chapter2 字串、文字PythonAPT字串
- Advanced-react-patterns(2)React
- Java 21 新特性:Record PatternsJava
- 閒侃Domain Logic PatternsAI