Rust為何無法成為超級語言?

banq發表於2024-05-25


與其他命令式語言相比,Rust 型別系統和避免共享可變狀態兩個特性可以實現更好的本地推理和形式驗證。

區域性推理

  • 區域性推理重要性:它能在不考慮整個程式狀態的情況下驗證程式屬性。
  • Rust 的所有權模型和沒有可變別名比其他語言更有利於區域性推理。

對形式驗證進行擴充套件

  • 雖然完整的形式驗證很難,但Rust 已經證明了基於編譯器的驗證是值得的。
  • 如果可以以較少的成本新增更多內容,就應該這樣做。

Rust 將開發工作量轉移到左側

  • 編譯 Rust 程式碼是一種形式化驗證,有助於更早地發現錯誤(“左移”)。
  • 雖然並非所有程式都能得到完全驗證,但驗證某些程式的某些屬性是有價值的。

原文摘要:
在 Rust 1.0 釋出九週年之際,其建立者 Graydon Hoare 現身並撰寫了一篇部落格文章,擴充套件了without.boats 的另一篇文章

你看,這些聰明的人正在努力解決一個問題:如何最大限度地發揮語言的靜態分析能力和可用性?

Rust 邁出了一大步。但人們還想要更多:
劇透:答案是結構化併發

  • 結構化併發是將執行緒限定在實際上下文範圍內,而不僅僅是父物件或某物。
  • 甚至會限制結構化併發,這樣執行緒集/子集/其他任何東西都不是語言的首要概念(非一流)。

Leakpocalyspe
Leakpocalyspe是一個只有高階 Rustaceans(和像我這樣的語言迷)才認識的術語,我懷疑它在 Rust 1.0 左右團隊成員的心裡佔有特殊的位置。

但是,如果 Rust 不能保證解構函式會被呼叫,那麼某些 API 實際上是不安全的。

為何Rust不保證解構函式被呼叫?

  • 這個月是 Rust 1.0 釋出九週年,九年前也就是 2015 年 5 月。發生了 Leakpocalypse 的 bug。
  • 如果您在 2015 年 4 月加入 Rust 團隊,那麼您只有不到兩個月的時間來解決可能導致安全程式碼變得不安全的設計問題!
  • 由於時間不足,需要走捷徑,所以最終的決定是Rust 不保證呼叫解構函式。

我認為這在當時是正確的決定;

  • 為了保證 Rust 的安全,故意洩漏記憶體似乎是一件小事。
  • 但是,這意味著 Rust 不是最優的。

Rust成功原因
Leakpocalypse 並沒有奪走 Rust 最大的成功。儘管我討厭 Rust,但我當然承認它比我最喜歡的可用語言 C 有了很大的進步。

Graydon Hoare 和我認為 without.boats 正確地指出了 Rust 成功背後的原因:“shared-xor-mutable(共享可變狀態)”規則。

  1. Rust 最大的成功並不是借用檢查器,而是 shared^mut 。
  2. 但是,shared^mut增加了複雜性

這主要是增加設計的複雜性,一旦你瞭解了模式,就很容易開始適當的設計。有點像 Rustaceans 改變他們的設計習慣來滿足借用檢查器。

rust 避免共享可變狀態具有深遠的影響;
當我們在 Rust 中形式驗證程式時,我們可以使用 FOL 並避免分離邏輯,因為型別系統保護我們免受可變別名的影響,而 caml 中的情況並非如此,儘管它是“函式性的”

如果我只編寫不會洩漏執行緒的函式會怎麼樣?

  • 假設你有一個函式。該函式啟動一個執行緒,然後返回。
  • 函式返回後,新執行緒繼續執行。
  • 新執行緒洩露了:執行緒從函式中洩漏出來。

非同步也會洩露:

  • 非同步函式返回 Future 或 Promise,即程式碼最終將在稍後執行的承諾。
  • 函式返回後,子程式中的程式碼也還將執行。
  • 非同步也會從函式中洩漏出來。

Rust async 一書中提到了另外三種方法:

  • 事件驅動程式設計,但如果沒有其他方法之一則不允許並行執行。
  • 協同程式,但它們也會洩漏出函式。
  • Actor模型,但後期的Actor可以比較早期的Actor持續更長的時間,這意味著他們也可以洩漏出功能。

所以它們都會洩漏.

如果我只編寫不會洩漏執行緒的函式會怎麼樣?
這就是解決方案:不要將執行緒從建立它們的函式中洩漏出去。
真的就這麼簡單。

這是 Nathaniel J. Smith想出來的,所以他稱之為結構化併發,並撰寫了 關於它是什麼和為什麼的開創性資料。

結構化併發可以傳遞一個一流的值(NJS 稱之為託兒所,我稱之為執行緒集)。
我後來意識到這也是一個問題;如果一個函式以某種方式返回一個執行緒集,就像一個非同步函式返回一個future一樣,那麼裡面的執行緒就會洩漏。

這就是為什麼受限結構化併發(restricted structured concurrency:簡稱RSC)是缺失的部分:它讓所有函式成為它們自己的子程式(具有併發性!),因此區域性推理成立並且屬性可以透過歸納證明。

受限結構化併發RSC 取消了將執行緒集作為一等值傳遞的能力。換句話說,RSC 是完全靜態的,純粹的編譯時構造,並且它建立了一個執行緒樹。

為什麼解構函式必須執行
為了使受限結構化併發 RSC 正常工作,必須保證解構函式執行。

如果解構函式無法執行,則 RSC 可能會導致 use-after-free,,這是最嚴重的錯誤之一。

這也是執行緒集不能成為一流值,並且必須與特定上下文範圍/生命週期繫結的另一個原因。

  • 如果一種語言支援比 RSC 更多的併發原語(多執行緒 等併發概念),那麼這很難做到,
  • 但如果 R​​SC 是唯一的方法(沒有執行緒等概念),那麼實際上很容易保證解構函式執行:只需新增基於範圍的資源管理(SBRM)

那麼,即使出現異常,解構函式也會始終執行。

我用 C 語言編寫了一種SBRM 形式,並將我的所有執行緒集繫結到它。我甚至新增了setjmp異常longjmp和執行緒取消訊號。
有了這些,只要我不在 SBRM 之外分配任何東西(,所有解構函式都會執行。而且是確定性的。

基於上下文範圍的資源管理
一旦你有了 RSC 和是SBRM保證銷燬,那麼你就會解決兩個重要問題: function colors 和非同步清理問題

  • 如果每個函式呼叫都是其自己的子程式,那麼您就沒有函式顏色。
  • 如果您沒有非同步,則無需擔心非同步清理。

這些問題在 Rust 中已經被認識到,有人說非同步 Rust 根本不起作用,我同意這種觀點。

Rust成功兩個原因:
Rust 之所以受到人們的喜愛:

  1. 是因為它讓軟體開發左移。
  2. Rust的編譯就是 形式化驗證

現在我們知道了為什麼編寫正確的程式很難:因為必須編寫正確,但是,我們又無法始終驗證所有程式。不過,沒有什麼可以阻止我們在某些時候驗證某些程式的某些屬性。
– 
Ron Pressler,“為什麼編寫正確的軟體很難”

 Rust 已經證明了編譯器中的形式驗證是 值得的。如果我們能以較少的成本增加更多功能,我們何樂而不為?

 結構化併發RSC糟糕的地方
現在人們使用了幾種併發程式碼模式,而 RSC 確實使它們更難實現:

  • 生產者/消費者模式
  • 執行緒池模式

需要同步是 RSC 的另一個缺點:

任何聲音 API 最多隻能提供以下三個理想屬性中的兩個:

  1. 併發:子任務與父任務同時進行。
  2. 可並行性:子任務可以與父任務並行進行。
  3. 借用:子任務可以從父任務借用資料,無需同步。

– without.boats,“範圍任務三難困境”

再次重申一遍,runtime 比 comptime 更強大

  • 如果 RSC 只是一個 comptime(編譯時期) 的東西,那麼它就會一直受到痛苦的阻礙。
  • 所以解決方案是使其動態化,擁有一些執行時runtime元件。

這就是催生了:動態受限結構化併發 (DRSC)

  • 受限結構化併發RSC 取消了將執行緒集作為一等值傳遞的能力。換句話說,RSC 是完全靜態的,純粹的編譯時構造,並且它建立了一個執行緒樹。
  • Matt Kline 可能正在考慮靜態DAG;如果我們有 動態DAG 會怎麼樣?如果我們有動態 DAG 會怎樣?如果我們不一次性(在編譯時)生成執行緒和執行緒之間的邊,而是允許自己動態(執行時)生成邊,會怎麼樣?

DRSC 能夠實現任何併發模式!
此時,阻礙我們接近 C10M 的唯一因素就純粹是硬體了!

結論
如果我讓 Rust 1.0 團隊中的任何人相信 DRSC 的價值,那麼他們現在一定很沮喪,這是因為 Rusty 的一些諷刺。

你看,Leakpocalypse 是由使用 Rust 所稱的 thread::scoped() 時發現的一個設計錯誤造成的。本質上,這是結構化併發的早期形式!於是他們放棄了這個簡單的解決方案!

如果他們那時推遲修補Bug,從設計上慢思考,做些困難的事情,推遲 Rust 1.0釋出,完全真正解決這個問題,那麼 Rust 可能會成為最優秀的超級語言!
 

相關文章