1 điểm bởi GN⁺ 2024-07-22 | 1 bình luận | Chia sẻ qua WhatsApp
  • Pin trong Rust là thành phần nền tảng được đưa vào để xử lý an toàn trạng thái tự tham chiếu bên trong Future do async/await tạo ra
  • Future async lưu trạng thái tại mỗi điểm await, nên một field trong cùng một đối tượng có thể tham chiếu tới field khác của chính đối tượng đó, trở thành kiểu tự tham chiếu
  • Các thiết kế move constructor, offset pointer và ?Move lần lượt không được chọn do chi phí theo dõi lúc chạy, khả năng hiện thực ở mức biên dịch và vấn đề tương thích ngược với API hiện có
  • Thiết kế cuối cùng là Pin, một kiểu bọc con trỏ để đưa đối tượng vào pinned typestate; nhờ auto trait Unpin, phần lớn kiểu vẫn có thể di chuyển như trước
  • Điểm khó của Pin không nằm ở riêng khái niệm bất biến, mà đến từ giới hạn của kiểu thư viện: reborrowing, Pin::set, pinned projection và tương tác với Drop đều làm giảm đáng kể tính dễ dùng

Vấn đề cần Pin

  • Trong hệ sinh thái async của Rust, Pinpinning là nền tảng cốt lõi, nhưng vẫn là lĩnh vực khó và dễ bị hiểu sai với người học async Rust
  • Mục đích của Pin không phải để người dùng tự tạo kiểu tự tham chiếu chỉ bằng Rust an toàn
    • Nó nhằm thao tác an toàn với các Future tự tham chiếu do compiler tạo ra từ hàm async, hoặc các kiểu tự tham chiếu do runtime như tokio tạo bằng mã unsafe
  • Ví dụ async fn bar phải lưu cả z lẫn Future Foo tham chiếu tới z vào cùng một trạng thái Future tại điểm foo(&mut z).await
    • Lúc này một field bên trong đối tượng Future tham chiếu tới field khác trong chính cùng đối tượng đó
    • Kiểu Future như vậy trở thành kiểu tự tham chiếu
  • Nếu đối tượng bị di chuyển sau khi vào trạng thái này, tham chiếu bên trong sẽ trỏ tới vị trí bộ nhớ cũ, nơi đó có thể đã chết hoặc bị tái sử dụng cho giá trị khác
  • Trong Rust trước Pin, nếu có quyền sở hữu hoặc mutable reference thì có thể di chuyển đối tượng, vì vậy cần một cách biểu đạt rằng sau một thời điểm nhất định đối tượng không được phép di chuyển nữa

Những hướng tiếp cận không trở thành lời giải

  • move constructor

    • move constructor là cách chạy mã khi giá trị bị di chuyển, giống destructor, để sửa các con trỏ tự tham chiếu sang vị trí mới
    • Trong Rust, con trỏ không chỉ nằm “bên trong” giá trị bị di chuyển; ví dụ chúng còn có thể nằm trong một vector các con trỏ đang trỏ tới trạng thái của chính nó
    • Muốn theo dõi tất cả các con trỏ như vậy thì cuối cùng sẽ cần quản lý bộ nhớ lúc chạy tương tự garbage collection
    • Rust từ sớm đã quyết định không có move constructor, và nhiều mã unsafe dựa vào giả định rằng có thể di chuyển giá trị chỉ bằng sao chép bộ nhớ
    • Nếu bổ sung move constructor sau này thì sẽ thành breaking change
  • offset pointer

    • offset pointer là cách biên dịch tự tham chiếu thành độ lệch tính từ địa chỉ của đối tượng tự tham chiếu, thay vì dùng tham chiếu thông thường
    • Ở thời điểm biên dịch không phải lúc nào cũng xác định được một tham chiếu có phải là tự tham chiếu hay không
    • Tùy theo nhánh, cùng một giá trị có thể trỏ vào bên trong chính đối tượng hoặc ra bên ngoài
    • Muốn xử lý điều đó thì phải biên dịch tham chiếu thành dạng như enum của offset và reference, và vào thời điểm phát triển async/await, cách này bị xem là không thực tế

Yêu cầu của pinned typestate

  • Future tự tham chiếu không cần phải bất động ngay từ đầu; thay vào đó, trong vòng đời của nó sẽ có giai đoạn còn được di chuyển tự do, rồi sau một thời điểm nhất định thì không được di chuyển nữa
    • Trong lúc được kết hợp với các Future khác, nó vẫn phải có thể di chuyển
    • Sau khi được đặt vào nơi sẽ sống trong lúc bị poll, nó không nên di chuyển nữa
  • Mô hình của Ralf Jung bổ sung trạng thái thứ ba cho Future tự tham chiếu là pinned typestate, bên cạnh hai typestate hiện có là “owned” và “shared”
  • Khi đối tượng đã vào pinned typestate thì không được di chuyển nữa
    • Nói chính xác hơn, không được làm mất hiệu lực vùng nhớ của đối tượng nếu chưa chạy destructor trước
    • Trên thực tế có thể hiểu là không được chuyển đối tượng sang vị trí mới trong bộ nhớ
  • Phần lớn kiểu không thể chứa tự tham chiếu, nên pinned typestate không mang nhiều ý nghĩa với chúng
    • Với các kiểu như vậy, tốt hơn là thoát khỏi ràng buộc của pinning và tiếp tục có thể di chuyển
  • Mô hình hình thức chi tiết của pinned typestate được trình bày trong A Formal Look at Pinning của Ralf Jung

Vì sao thiết kế ?Move thất bại

  • Trước Pin, đã có nỗ lực thiết kế dựa trên trait mới tên Move
    • Phần lớn kiểu sẽ implement Move
    • Các kiểu có thể chứa tự tham chiếu sẽ không implement Move
    • Khi tạo tham chiếu tới giá trị của kiểu không implement Move, giá trị đó sẽ đi vào pinned typestate và không thể di chuyển nữa
  • Cách này khá trực quan vì gắn thời điểm tạo tham chiếu với thời điểm chuyển sang pinning để đảm bảo an toàn
    • Thậm chí nó đã từng được hiện thực trong một nhánh compiler
  • Giới hạn căn bản là có trường hợp ta muốn tạm thời tham chiếu tới một giá trị sau này có thể trở thành tự tham chiếu, nhưng hiện tại vẫn chưa muốn pinning nó
    • Ví dụ có thể muốn lưu tạm giá trị vào Option, rồi lấy ra bằng Option::take
  • Vấn đề lớn hơn là tương thích ngược
    • Không thể biến Move thành auto trait
    • Vì đã tồn tại các API ổn định như mem::swap, vốn giả định rằng luôn có thể di chuyển giá trị thông qua mutable reference
  • Cách thêm dưới dạng ?Move cũng không tương thích ngược do liên quan tới associated type
    • Vị trí thêm ràng buộc ?Trait cho associated type nằm ở phần định nghĩa trait
    • Nếu nới lỏng ràng buộc associated type của trait hiện có, mã đang dựa vào ràng buộc đó có thể bị hỏng
    • Nhiều phép toán cơ bản như associated future type của IntoFuture, Target của DerefMut, kiểu trả về của hàm, item của iterator, giá trị trả về của toán tử index, hay kết quả của toán tử số học đều gắn với associated type
  • Ngay cả edition cũng khó giải quyết
    • Vì để các crate ở edition khác nhau cùng hoạt động, giao diện trait phải được giữ nguyên

Thiết kế Pin

  • Thiết kế cuối cùng biểu diễn pinned typestate không phải như thuộc tính của kiểu đối tượng, mà là trạng thái do một con trỏ đặc biệt tạo ra
  • Pin là kiểu wrapper bao quanh con trỏ
    • Nó có thể bọc cả kiểu tham chiếu dựng sẵn của ngôn ngữ
    • Nó cũng có thể bọc smart pointer do thư viện định nghĩa như Box
  • Pin đưa đối tượng mà con trỏ trỏ tới vào pinned typestate, và đối tượng đó không được phép di chuyển nữa
  • Để giảm thay đổi ở mức tối thiểu, thiết kế này được hiện thực dưới dạng API thư viện chứ không phải tính năng compiler
    • Mã cần thực sự thay đổi đối tượng đã pin phải dùng API unsafe
    • Khi đó phải đảm bảo rằng đối tượng sẽ không bị di chuyển thông qua mutable reference thông thường
  • Vì phần lớn kiểu không có khác biệt có ý nghĩa giữa trạng thái pinned và trạng thái thường, nên auto trait Unpin được thêm vào
    • Nếu kiểu không thể là tự tham chiếu, có thể lấy mutable reference từ pinned pointer mà không cần unsafe
    • Đối tượng implement Unpin vẫn an toàn nếu bị di chuyển ra ngoài Pin
  • Vì pinning chỉ áp dụng cho pinned pointer, nên unpinned reference thông thường vẫn hoạt động với cả những kiểu không phải Unpin
  • Giải thích thêm có trong tài liệu chuẩn cho kiểu Pin và module pin
  • Ưu điểm lớn nhất của thiết kế này là có thể bổ sung mà không phá vỡ mã hiện có
    • Các API có thể di chuyển dữ liệu đang được tham chiếu, như swap, đều cần mutable reference
    • Khi một đối tượng đã được pin bằng Pin, không thể tiếp tục gọi các API như vậy lên nó
    • Vì pinned typestate chỉ áp dụng với pinned reference đặc biệt, nó không phá vỡ cam kết tương thích ngược của toàn bộ ngôn ngữ Rust

Vấn đề về tính dễ dùng của Pin

  • Pin đáp ứng yêu cầu theo cách tương thích ngược, nhưng khi người dùng phải trực tiếp xử lý nó thì xuất hiện vách đá độ phức tạp
  • Một cách giải thích là muốn thay đổi đối tượng đã pin thì cần mã unsafe
    • Tuy nhiên không nên phóng đại vấn đề này
    • Có thể gán an toàn cho đối tượng đã pin bằng Pin::set
    • Trên thực tế, mã cần trực tiếp thay đổi đối tượng đã pin đa phần là mã compiler tạo ra khi hạ hàm async xuống Future; người dùng tự viết trường hợp này khá hiếm
  • Cũng có quan điểm cho rằng Pin khó vì nó hoạt động có điều kiện, nhưng đó không phải nguyên nhân cốt lõi
    • Trong Rust vẫn có những tính năng hành xử theo điều kiện nhưng dễ hiểu hơn
    • non-lexical lifetimes là ví dụ, khi lifetime kết thúc ở các điểm khác nhau tùy theo từng nhánh điều kiện
  • Vấn đề cốt lõi là Pin chỉ là một kiểu thư viện, trong khi kiểu tham chiếu thông thường là kiểu dựng sẵn của ngôn ngữ và được hỗ trợ bởi nhiều cú pháp, sugar ngôn ngữ
    • Những gì vốn rất tự nhiên với tham chiếu thường lại biến mất ở pinned reference
    • Mental model mà người dùng xây dựng từ cách compiler xử lý tham chiếu thông thường bị phá vỡ khi chuyển sang pinned reference

reborrowing và Pin::as_mut

  • Mutable reference thông thường &mut T không implement Copy, nhưng vẫn có thể truyền nhiều lần vào cùng một hàm số
    • Đó là vì compiler ngầm reborrow như thể chèn &mut *x thay cho x
  • Pin<&mut T> là kiểu thư viện thông thường và không implement Copy, nên không có sự tiện lợi này
    • Nếu dùng Pin<&mut T> nhiều hơn một lần, có thể gặp lỗi dùng giá trị sau move, hoặc lỗi lifetime còn khó hiểu hơn
    • Cần gọi tường minh Pin::as_mut để reborrow
  • Với mutable reference thông thường, có thể gán trực tiếp thông qua dereference và toán tử gán, nhưng với Pin thì phải học phương thức set
    • Việc phải dùng nhiều API đặc biệt như vậy đến từ chỗ Pin là kiểu thư viện, không có hỗ trợ cú pháp từ ngôn ngữ

pinned projection và Drop

  • pinned projection là bài toán lấy pinned reference tới field của một đối tượng từ pinned reference tới chính đối tượng đó
    • projection nghĩa là truy cập từ đối tượng xuống field
  • Vì việc này khó hơn rất nhiều so với truy cập field trên tham chiếu thông thường, người ta dùng các crate bên thứ ba như pin-project-lite
    • Các crate này buộc người dùng phải học API mới khá phức tạp, thường đi kèm macro
  • Tương tác tệ nhất xảy ra giữa pinned projection và trait Drop
    • Drop::drop nhận mutable reference thông thường
    • Nếu một kiểu có field tự tham chiếu, pin-project xuống field đó rồi poll nó, sau đó lại di chuyển field này trong destructor, thì có thể phá vỡ bảo đảm của pinning
    • Chẳng hạn, nếu trong destructor pinning Future đó lên stack rồi poll nó, bảo đảm pinning ban đầu sẽ bị vi phạm
  • Các crate như pin-project-lite xử lý chuyện này bằng cách hạn chế khả năng tự định nghĩa destructor
    • Về mặt thực tiễn cách này hoạt động được, nhưng nó làm tăng thêm độ phức tạp phải ghi nhận trong tài liệu giải thích bảo đảm pinning
    • Drop đã ổn định trước Pin, nên cần giải pháp vòng tránh

Đánh giá hiện tại và hướng cải thiện tiếp theo

  • Pin cho phép biên dịch các hàm async có chứa tham chiếu tùy ý thành các đối tượng tự tham chiếu an toàn
    • Tham chiếu là phần cực kỳ cơ bản trong cách người dùng Rust viết mã, nên nếu thiếu điều này thì tính dễ dùng của async/await sẽ giảm mạnh
  • Đồng thời, Pin được bổ sung theo cách hoàn toàn tương thích ngược với Rust hiện có
  • Pin đã trở thành thành phần nền tảng của hệ sinh thái hỗ trợ dịch vụ mạng hiệu năng cao và nhiều trường hợp sử dụng lập trình bất đồng bộ khác
  • Tuy vậy, làm việc với pinned reference khó hơn rất nhiều so với tham chiếu thông thường, và Pin thực sự tạo ra một vách đá về độ phức tạp
  • Khái niệm trọng tâm cho hướng cải thiện tiếp theo là pinned places

1 bình luận

 
GN⁺ 2024-07-22
Ý kiến trên Hacker News
  • Tôi luôn nghĩ rằng Pin rất khó hiểu vì không được giải thích rõ ràng trong tài liệu chính thức
    Đặc biệt có nhiều cách giải thích kiểu “Pin đảm bảo đối tượng tuyệt đối không bao giờ bị di chuyển”, nhưng điều đó không đúng
    Nó chỉ đúng khi đối tượng không phải là Unpin; còn phần lớn các đối tượng thông thường đều là Unpin, nên Pin thường chẳng làm gì cả
    Tôi đã mất rất lâu mới hiểu được điều này, và tôi cho rằng tập hợp các kiểu T mà Pin thực sự có ý nghĩa khá đặc thù và kỳ lạ, nhưng tài liệu lại không nhấn mạnh đủ điều đó

    • Phản hồi hay, và sẽ tốt nếu tài liệu làm rõ hơn phần này
      Tất nhiên, các kiểu mà ta thực sự sẽ xử lý ở trạng thái được ghim cố định, tức future và stream, có khả năng cao hơn nhiều là những đối tượng đặc thù như vậy
      Dù thế, tôi nghĩ tài liệu đã tốt lên rất nhiều trong vài năm qua
      Khi kiểm tra trong lúc viết bài này, tôi đã ngạc nhiên vì nó tập trung vào những điểm khá phù hợp; tôi nhớ khoảng năm 2019, nó nghiêng nhiều hơn về đặc tả hợp đồng kiểu đáng lẽ nên nằm trong tài liệu tham chiếu Rust hơn là tài liệu API của std
  • Tôi nghĩ lý do người dùng thấy Pin khó là vì bản thân Pin không có ý nghĩa
    Điều này khác với các wrapper khác của ngôn ngữ; nếu có ngoại lệ thì có lẽ là AssertUnwindSafe, thứ hầu như không ai dùng theo mục đích ban đầu
    Khi có Pin<&mut InnerType>, chỉ riêng Pin trong ngôn ngữ hay thư viện chuẩn không cho biết bạn có thể hay không thể làm gì
    Chỉ khi InnerType được khai báo là Unpin thì điều đó mới có nghĩa là bạn có thể làm mọi việc mà con trỏ thông thường làm được
    Thay vào đó, Pin hoạt động theo kiểu “hãy tự mang ý nghĩa của bạn vào”, trong đó nhà cung cấp InnerType tạo thêm các phương thức và API nội bộ dùng unsafe để thao tác an toàn với đối tượng đã được ghim cố định
    Mục đích của chính Pin là cung cấp một con trỏ có ít năng lực nội tại hơn, chẳng hạn như thay thế qua &mut hay lấy ra khỏi Box rồi di chuyển, để kiểu bên trong có thể cho phép thêm năng lực một cách an toàn trên nền đó
    Tôi nghĩ sự mơ hồ về ý nghĩa này là điều khiến mọi người bối rối nhất, và bản thân tôi cũng mất khá lâu mới hiểu được
    Khái niệm về trường có cấu trúc và trường phi cấu trúc chỉ là cơ chế để cho phép các mẫu truy cập phổ biến như “trường này là dữ liệu bình thường, còn trường kia chứa một đối tượng tự nó muốn được ghim cố định”

    • Pin có ý nghĩa. Chừng nào kiểu đích chưa triển khai Unpin, điều đó có nghĩa là đối tượng mà con trỏ này trỏ tới sẽ không bao giờ có thể bị di chuyển nữa
      Chính xác hơn, nghĩa là không thể làm vô hiệu hóa đối tượng đích nếu không chạy destructor, và lý do việc di chuyển trở thành vấn đề cũng nằm ở đó
      Khi từ bỏ một số quyền nhất định, bạn nhận được các quyền khác, chẳng hạn quyền lưu giá trị tự tham chiếu
      Hợp đồng giữa các component thường hoạt động theo kiểu như vậy
      Tương tự, nếu từ bỏ quyền thay đổi thông qua tham chiếu, bạn đồng thời có thể biến tham chiếu đó thành alias
      Mỗi khi nghĩ đến điểm này, dù là một chủ đề hoàn toàn khác và nặng nề hơn nhiều, tôi lại nhớ tới câu thoại trong phim Lincoln: “Nếu chúng ta tuân theo luật pháp, Alex, nếu ta tuân theo đến mức đánh mất tự do — chẳng hạn như tự do áp bức — thì có lẽ ta sẽ khám phá ra những tự do khác mà trước đây ta chưa từng biết”
      Tuy nhiên, tôi đồng ý rằng về mặt giảng dạy, vấn đề là trong code an toàn bạn không thể trực tiếp sử dụng những quyền đó
      Bởi rất khó đưa ra ví dụ dễ hiểu về việc có thể làm gì với một tham chiếu đã ghim cố định, ngoài “gọi phương thức poll do compiler tạo ra”
  • Tôi đã phát triển chuyên nghiệp bằng Rust trong vài năm, nhưng thành thật mà nói tôi không hiểu Pin giỏi đến vậy
    Tôi biết lý thuyết, nhưng không có nhiều trực giác về khi nào nên dùng nó
    Việc dùng Pin trên thực tế gần giống như “thử làm gì đó, compiler phàn nàn, rồi ghim cố định chỗ này chỗ kia thì compile được”
    Trong công việc code hằng ngày, nó chưa từng thật sự trở thành rào cản đến mức tôi phải ngồi xuống và hiểu sâu về nó

    • Tôi cũng vậy. Đây là một trong những trường hợp phổ biến nhất thuộc kiểu “cứ tránh unsafe và biết ơn vì những người làm compiler thông minh đã giải quyết hết rồi”
      Ngược lại, trong C++ tôi thường xuyên đi dọc bờ sông của những “thứ mình không hiểu nhưng bắt buộc phải dùng” rồi bị cá sấu ăn thịt
  • Khi giảng dạy, để làm rõ rằng các mục Unpin không bị ảnh hưởng bởi Pin, có thể dùng những phép ví von ngoài đời về việc dùng công cụ được tạo ra để giữ vật tại chỗ nhưng lại không tác động được
    Móc Velcro không bám vào bề mặt nhẵn: Pin → Velcro, Unpin → bề mặt nhẵn
    Nam châm không tác động tới vật liệu phi từ tính: Pin → nam châm, Unpin → phi từ tính/kính/đồng thau
    Keo dán không bám vào bề mặt chống dính: Pin → keo dán, Unpin → chống dính
    Như vậy sẽ rõ rằng “Velcro” giữ vật ở đúng vị trí, nhưng nếu vật “nhẵn” thì nó không bị cơ chế Velcro tác động
    Xét theo bầu không khí đặt tên trong hệ sinh thái Rust, nếu tên trait được đổi theo hướng như nam châm và phi từ tính thì hẳn đã rất đẹp

    • Nhưng vật nhẵn thì không thể dán bằng Velcro, và gỗ thì không thể giữ nam châm lại
      Tôi nghĩ Unpin có nghĩa là đối tượng luôn sẵn sàng được ghim cố định bất cứ lúc nào
      Tôi đọc bài tối qua, nhưng đã quên mất liệu việc ghim cố định có cần một bước điều chỉnh hay không
      Vì vậy, tôi cho rằng T: Pin + !Unpin giống như tờ giấy chỉ có thể cố định bằng ghim bấm, còn T: Pin + Unpin giống một bức tranh có móc treo, có thể treo lên đinh rồi tháo xuống mà không làm hỏng móc hơn
  • Thuật ngữ “định danh giá trị” không được định nghĩa ở bất kỳ đâu trong bài này và tôi cũng không tìm thấy trong tài liệu Mojo, nên cơ sở để Modular nói rằng Mojo giải quyết được vấn đề mà Pin muốn giải quyết là không rõ ràng
    Tôi cũng không dám khẳng định mình biết câu trả lời, nhưng tôi nhớ đến một bài thuyết trình xuất sắc của Dave Abrahams, người từng làm về ngữ nghĩa giá trị của Swift cùng Chris Lattner
    Tiêu đề bài thuyết trình là “Value Semantics: Safety, Independence, Projection, & Future of Programming”
    [0] https://www.youtube.com/watch?v=QthAU-t3PQ4

    • Rõ ràng Mojo, theo một nghĩa nào đó, đã kế thừa khái niệm ngữ nghĩa giá trị của Swift, nhưng Rust cũng có ngữ nghĩa giá trị theo cùng nghĩa đó
      Rust có cả tham chiếu như các kiểu hạng nhất, trong khi Swift và, theo tôi thấy, Mojo chỉ cho phép tham chiếu như một cách truyền tham số
      Có vẻ Mojo đã mở rộng tham số inout của Swift để có cả cách truyền tham chiếu bất biến
      Nếu không cho phép lưu tham chiếu bên trong đối tượng thì đúng là giải quyết được vấn đề “struct tự tham chiếu”, vì sẽ không thể triển khai loại mã mà Rust biên dịch được
      Nhưng đoạn được trích dẫn nói về Mojo lại hoàn toàn không nói chuyện đó, nên khá khó hiểu là nó muốn nói gì
  • Theo tôi, vấn đề nằm ở chỗ nếu có tham chiếu &mut tới một giá trị nào đó, ta có thể di chuyển giá trị đó bằng những thứ như mem::swap/replace
    Nhưng trên thực tế hiếm khi cần làm vậy
    Nếu điều đó không được cho phép, thì có lẽ việc có tham chiếu &mut tới giá trị tự tham chiếu đã hoàn toàn an toàn
    Cũng có thể đã có một cách để chỉ khi cần mới chọn tường minh việc di chuyển thông qua tham chiếu, và nếu biến swapreplace thành unsafe thì có lẽ đã tránh được toàn bộ vấn đề này
    Tôi mong có ai đó khám phá không gian thiết kế này

    • Đúng vậy. Khi còn làm về vấn đề này, Aaron Turon từng mô tả &mutquá mạnh
      Nếu &mut không trao quyền di chuyển giá trị bên trong nó, toàn bộ thiết kế đã đơn giản hơn nhiều
      Tôi dự định sẽ nói về nội dung này trong bài tiếp theo
      Rust phải giữ tương thích ngược và đã quyết định rằng có thể di chuyển giá trị khỏi &mut, nhưng nếu không bị ràng buộc bởi các quyết định trong quá khứ thì chắc chắn có thể có một thiết kế gọn gàng hơn nhiều
    • Điều này đúng, nhưng không có tính mở rộng. Nó sẽ khó mà thành hiện thực ngay từ đầu vì sẽ phá vỡ quá nhiều mã hiện có
      mem::swap chỉ là một trong các cách di chuyển giá trị thông qua tham chiếu khả biến, và còn rất nhiều cách khác
      Option::take là một ví dụ tôi dùng khá thường xuyên; nếu nó là unsafe thì sẽ thật sự rất kỳ quặc
  • Rất thích phần câu chuyện bối cảnh này. WithoutBoats vốn đã có rất nhiều thảo luận sôi nổi về các chủ đề rất đúng thời điểm như bộ lặp bất đồng bộ, poll, pin
    https://news.ycombinator.com/from?site=without.boats
    Có vẻ không có nhiều cộng đồng đào sâu công khai đến vậy vào những chi tiết nội bộ của ngôn ngữ, và đọc rất thú vị

    • Điều đó thì hay thật, nhưng cũng có nghĩa là việc phát triển ngôn ngữ diễn ra rất chậm
      Async vẫn còn nửa chín nửa sống và rất phức tạp
      Tôi nói điều này với tư cách người đã viết mã Rust 40 giờ mỗi tuần trong 3 năm qua
  • Có thể hình dung một ngôn ngữ giống Rust có move constructor, trong đó mọi subtype Future được tạo ra đều opaque và tự động được cấp phát trên heap
    Khi đó người dùng không có cách nào phá hủy nó; vì nó opaque và nằm ở nơi khác trên heap nên cũng không có cách nào di chuyển nó, do đó có thể sẽ không cần Pin
    Việc có move constructor nghĩa là về mặt khái niệm, việc di chuyển là phá hủy rồi tạo lại

    • Pin là một trạng thái hơn là thuộc tính của bản thân dữ liệu
      Nhờ vậy có một hiệu ứng tốt là có thể hợp nhất và inline các Future trước khi chạy
      Điều này cũng tương tự tính bất biến của Rust. Không phải có bộ nhớ bất biến, mà chỉ có tham chiếu bất biến
    • Nếu mọi future đều được cấp phát trên heap thì không cần move constructor
      Nhưng như vậy mỗi lần gọi hàm bất đồng bộ sẽ phát sinh một lần cấp phát riêng, điều này rất tệ cho tính cục bộ bộ nhớ
      Một dạng stack ảo nào đó sẽ tốt hơn nhiều, nhưng để tối ưu stack mặc định nhỏ thì rốt cuộc sẽ cần garbage collection
    • Có thể đoán được thay đổi này sẽ gây phá vỡ lớn đến mức nào, nhưng tôi thật sự mong Rust đối diện trực diện với nó và thêm vào std một trait Move được tích hợp vào ngôn ngữ ở mức tương tự Copy
      Move sẽ định nghĩa một hàm di chuyển giá trị từ một địa chỉ trong bộ nhớ sang địa chỉ khác, và các struct không có impl Move sẽ không thể bị di chuyển
      Gần như mọi type sẽ gắn #[derive(Move)], tức chỉ cần triển khai một hàm di chuyển đơn giản sao chép các byte
      Nhưng làm vậy sẽ mở đường cho các type tự tham chiếu, future, và nhiều thứ cần hành vi di chuyển phức tạp hơn
      Trên thực tế, có lẽ hợp lý hơn nếu tách thành hai trait để phản ánh khác biệt giữa CopyClone
      Một cái là marker trait cho compiler biết rằng có thể cứ chuyển các byte đi, còn cái kia cho phép triển khai “move constructor” tùy chỉnh
      Pin quá khó hiểu nên tôi muốn có Move
      Một khái niệm phức tạp lại bị bọc trong phủ định kép, đôi khi là phủ định ba. Nhìn những thứ như fn(...) là thấy “gì đây?”, và đến mức pin projection unsafe thì tôi bỏ cuộc
      Tôi không biết khi nào là an toàn và khi nào là không an toàn, nên đành tránh xa
      Chuyển từ Rust không có Move sang Rust có Move sẽ bất tiện
      Hầu như mọi struct đã viết từ trước đến nay đều phải thêm #[derive(Move)], và std cũng vậy
      Với các type chưa được pin trong các edition cũ, compiler sẽ phải suy luận phần triển khai trait Move
      Về mặt cơ học chắc là làm được, chỉ là rất nhiều việc
      Rust bất đồng bộ thật kinh khủng. Đặc biệt là khi so với future/promise của hầu hết mọi ngôn ngữ khác
      Một ngày nào đó sẽ có ai đó cải thiện mô hình an toàn bộ nhớ của Rust, và tạo ra một ngôn ngữ hệ thống mới giống Rust, có trait Move và future tốt hơn
      Cá nhân tôi cũng muốn có thực thi lúc biên dịch thay cho hệ thống macro của Rust
      Tôi thích Rust, và cũng thích tất cả những gì đội ngũ đã làm trong nhiều năm qua
      Nhưng ngôn ngữ mà tôi thật sự mong chờ là ngôn ngữ sẽ đến sau Rust
      Vẫn là những ý tưởng đó, nhưng là một ngôn ngữ học được từ các sai lầm của Rust; và diện mạo của một ngôn ngữ kiểu Rust tốt hơn như vậy đang ngày càng rõ ràng
      Tôi thật sự rất mong chờ
  • Một bài viết xuất sắc khác của WithoutBoats
    Nói thật, đây là một trong những chỗ mà tôi thấy may vì trong Rust nó được trừu tượng hóa và chôn bên trong async runtime
    Dù vậy, ngoài việc triển khai Future tùy chỉnh, tôi vẫn tò mò Pin thực sự được dùng ở đâu

    • Tôi cũng tò mò
      Chỉ nhìn cách diễn đạt thì có vẻ có thể dùng trong FFI
      Ví dụ nếu một hàm extern trả về *mut T rồi nhận pointer đó, có vẻ như bọc bằng Pin<&mut T> có thể đem lại ngữ nghĩa tốt hơn
      Nhưng bài viết nói rằng “một sự thật khác về trạng thái type đã được pin là với phần lớn type, nó hoàn toàn không liên quan. Nếu giá trị của một type tuyệt đối không thể chứa tự tham chiếu, thì việc pin nó là vô ích”
      Tôi vẫn còn rất mới với FFI, nên muốn hiểu cách tốt nhất để bọc nó bằng Rust an toàn
    • Trong FFI, có trường hợp C API phơi bày đối tượng dưới dạng pointer thay vì tham chiếu, nên không được di chuyển nó
      Điều này cũng áp dụng khi tương tác với các type hệ thống phụ thuộc vào địa chỉ
      Chẳng hạn với mutex/futex của một số hệ điều hành, tài liệu kernel nói rằng đối tượng khóa trong user space không được đổi địa chỉ sau khi khởi tạo, nên theo tôi biết std dùng thứ tương đương với Pin
      Điểm đặc biệt là địa chỉ không được đổi ngay cả khi nó không bị khóa
      Thông thường điều kiện này chỉ áp dụng trong lúc đang khóa, và trong trường hợp đó không thể di chuyển đối tượng đang được tham chiếu, nên không cần Pin
  • Có vẻ như người ta đang làm một khối lượng công việc khổng lồ để tránh sửa vấn đề thật sự là tính kém hiệu quả của thread
    Toàn bộ mã bất đồng bộ, không ngoại lệ, đều là một kiểu hack triển khai lightweight thread bằng rất nhiều cú pháp “đường” để quản lý trạng thái
    Trong các ngôn ngữ như Rust, nó thêm vào một lượng phức tạp khổng lồ vốn lẽ ra không cần có
    Nếu sửa được vấn đề hiệu quả và khả năng mở rộng của thread, tất cả những thứ này sẽ biến mất
    Biến mất cái phụt
    Tương tự như việc null trong các ngôn ngữ như Java từng là “sai lầm tốn kém hàng nghìn tỷ đô”
    Một quyết định thiết kế, hoặc trong trường hợp này là sự thiếu vắng thiết kế, tạo ra sự phức tạp khổng lồ

    • Thread không hỗ trợ hủy theo cách hợp lý
      Hủy rất hữu ích trong ứng dụng mạng và GUI
      Thread khiến việc tận dụng đủ cả CPU lẫn mạng, nhưng không chiếm dụng quá mức bên nào, trở nên khó khăn
      Khi bắt đầu chuyển công việc giữa các thread pool, bạn đã bước vào con đường tái triển khai future
      Hoặc bạn sẽ phải làm việc bằng callback/event, khiến mã bị chia nhỏ, và async/await chính là thứ cú pháp “đường” muốn giải quyết điều đó
      Phương án thay thế cho hủy và timeout là luồn một đối tượng Context xuyên suốt mã như Go, nhưng khi đó nảy sinh vấn đề là mã ở tầng cuối có thể ngây thơ gọi một hàm không tuân thủ Context đúng cách
      Điều này chỉ nhỉnh hơn rất ít so với vấn đề hàm không bất đồng bộ bên trong mã bất đồng bộ
    • Tôi nghi ngờ liệu có thể viết lại kernel Linux để “sửa vấn đề hiệu quả và khả năng mở rộng của thread” hay không, và ngay cả nếu có thể, tập hợp chuyên gia Rust đã làm cho Pin hoạt động và tập hợp chuyên gia kernel có thể làm việc đó có lẽ cũng không phải là một
      Vì vậy tôi tò mò cụ thể lẽ ra họ phải làm gì
      Chẳng lẽ chỉ giơ tay lên và nói: “Một ngày nào đó ai đó có thể sửa Linux để thread nhanh như phép màu, nên chúng ta sẽ không thêm bất đồng bộ vào ngôn ngữ của mình”?
    • Việc phân biệt và đánh dấu các hàm có đồng bộ hóa với process đồng thời và các hàm không làm vậy thực ra là điều tốt
    • Đáng tiếc là việc vượt qua ranh giới user space vẫn có chi phí, dù “thread” có nhẹ đến đâu
      Ngoài ra, nếu biến hệ điều hành thành scheduler cho mọi tác vụ bất đồng bộ, thì mọi runtime đều phải dùng scheduler của hệ điều hành, khiến không thể có các thiết kế scheduler đa dạng
    • Những quyết định tai hại của Rust cho thấy một văn hóa đã ăn sâu: bằng mọi giá cứ tiếp tục đẩy các sai lầm trước đó đi tiếp
      Có vẻ như ngay cả sau khi “cách tiếp cận được ưu tiên” bị chứng minh là không khả thi, họ vẫn không đánh giá lại chi phí/lợi ích
      “Tôi muốn tính năng X, hậu quả thế nào không quan tâm” hiếm khi là nước đi thắng trong thiết kế ngôn ngữ