NOTE: racket/future 的限制與想像
1. 最開始的問題
在解決 sauron 要如何從定義跳轉到使用位置的時候,我讓 maintainer 之間互相通知依賴關係,由依賴方通知被依賴方。 這樣的程式雖然符合直覺,卻因為實作而受到限制。正如以下推文說到的問題
no,這個想法的問題是沒有意識到記憶體開銷出在 thread internal channel 上
— 悃悃 (@dannypsnl) August 13, 2022
thread 是 single processor 上的切換,同時是公平排班
身上有 message 不是空的的 thread 都有機會被換出來,這從 UI 甚至可以直接觀察到,鼠標會一直顯示 loading https://t.co/34T9ue3doi
這些通知會被放到各個 thread 隱含的 channel 上,而這些會觸發排程器啟動 thread,然而在 racket thread 上並不會利用 multi-processor。 而使得這些 thread 被快速的換到同一個 core 上執行,大量的 context switching,拖垮了編輯器的反應。
2. 解方?
我只好去思考還有什麼能讓我繼續用熟悉的 Erlang process 抽象方式寫程式。
2.1. Thread group(不可行)
直覺的反應是用 thread group 限定 CPU 的分享。但在 racket 中,一個 thread 只能在啟動時指定其 group,因而限制了這個方案的可行性。 而且重點應該還是訊息的處理應該是平行的。
2.2. 封裝 Future(失敗)
於是我寫了以下的程式
(define (make-process ch) (future (lambda () (let loop () (match (async-channel-get ch) ...) (loop)))))
這段程式的問題是, async-channel-get
會 suspend 一個
future,於是它其實並沒有像我想像的那樣運作! 或許每次
async-channel-put
都配合一個 touch
是可行的,但是因為最後這個 future
沒有終止,所以程式要是最後想 touch
來結束是不可能的。
於是我似乎需要更好的方案。
2.3. Future thunk 回傳 Future?
我現在想的方式則是延續 async-channel-put
配合一個 touch
之後,讓
future 內部的函數再次回傳一個同樣的運算來表達無限迴圈
(struct process (fu ch)) (define (process-send pro msg) (async-channel-put (process-ch pro) msg) (touch (process-fu pro))) (define (^friend my-name [ch (make-async-channel)]) (process (future (thunk (match (async-channel-get ch) [(list 'ping from) (printf "ping from ~a~n" my-name) (process-send from 'pong) (^friend my-name ch)] ['pong (printf "pong from ~a~n" my-name) (^friend my-name ch)]))) ch)) (define bob (^friend "Bob")) (define jack (^friend "Jack")) (process-send bob (list 'ping jack))
這當然是可以簡單地令第一次的 process-send
運作,但要是呼叫第二次就會發現因為我們 touch
過了,而沒有儲存下這個新的運算位置因此會出錯。
進一步就會想到要是我們建立一個 hash
來儲存這個結果,應該就可以重複運算。
然而, future
是平行運算,於是不同的 future
完全可能在同一個時間上 process-send
到某一個 future
。 這時, hash
應該儲存哪一個運算結果呢?
所以我應該會需要一個 place
專門管理這件事,要求所有的訊息應該都要先通過這個 place
的 channel
之後才往各個 process 上分配。
3. 結論
我應該會在之後建立實驗性的程式庫去驗證這個想法,希望能用