Tìm hiểu về `Pin` trong Rust
(without.boats)Pintrong 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à
?Movelầ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 traitUnpin, phần lớn kiểu vẫn có thể di chuyển như trước - Điểm khó của
Pinkhô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ớiDropđề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,
Pinvà pinning 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
Pinkhô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 barphải lưu cảzlẫn FutureFootham chiếu tớizvào cùng một trạng thái Future tại điểmfoo(&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ênMove- 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
- Phần lớn kiểu sẽ implement
- 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ằngOption::take
- Ví dụ có thể muốn lưu tạm giá trị vào
- Vấn đề lớn hơn là tương thích ngược
- Không thể biến
Movethà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
- Không thể biến
- Cách thêm dưới dạng
?Movecũ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
?Traitcho 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,TargetcủaDerefMut, 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
- Vị trí thêm ràng buộc
- 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
Pinlà 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
Unpinvẫn an toàn nếu bị di chuyển ra ngoàiPin
- 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
Pinvà modulepin - Ư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
- Các API có thể di chuyển dữ liệu đang được tham chiếu, như
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
Pinkhó 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à
Pinchỉ 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 Tkhông implementCopy, 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 *xthay chox
- Đó là vì compiler ngầm reborrow như thể chèn
Pin<&mut T>là kiểu thư viện thông thường và không implementCopy, 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
- Nếu dùng
- 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
Pinthì phải học phương thứcset- Việc phải dùng nhiều API đặc biệt như vậy đến từ chỗ
Pinlà kiểu thư viện, không có hỗ trợ cú pháp từ ngôn ngữ
- Việc phải dùng nhiều API đặc biệt như vậy đến từ chỗ
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
DropDrop::dropnhậ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-litexử 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ướcPin, 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
Pincho 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à
Pinthự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
Ý 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
Tmà 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 đó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
stdTô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 đầuKhi 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àUnpinthì đ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 đượcThay 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
InnerTypetạo thêm các phương thức và API nội bộ dùngunsafeđể thao tác an toàn với đối tượng đã được ghim cố địnhMụ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
&muthay lấy ra khỏiBoxrồ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”
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ữaChí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
polldo 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ó
unsafevà 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
Unpinkhông bị ảnh hưởng bởiPin, 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 đượcMóc Velcro không bám vào bề mặt nhẵn:
Pin→ Velcro,Unpin→ bề mặt nhẵnNam 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 thauKeo dán không bám vào bề mặt chống dính:
Pin→ keo dán,Unpin→ chống dínhNhư 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
Tôi nghĩ
Unpincó nghĩa là đối tượng luôn sẵn sàng được ghim cố định bất cứ lúc nàoTô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 + !Unpingiống như tờ giấy chỉ có thể cố định bằng ghim bấm, cònT: Pin + Unpingiố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ơnThuậ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
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ố
inoutcủa Swift để có cả cách truyền tham chiếu bất biếnNế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
&muttới một giá trị nào đó, ta có thể di chuyển giá trị đó bằng những thứ nhưmem::swap/replaceNhư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
&muttới giá trị tự tham chiếu đã hoàn toàn an toànCũ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
swapvàreplacethànhunsafethì có lẽ đã tránh được toàn bộ vấn đề nàyTôi mong có ai đó khám phá không gian thiết kế này
&mutlà quá mạnhNếu
&mutkhô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ềuTô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ềumem::swapchỉ 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ácOption::takelà một ví dụ tôi dùng khá thường xuyên; nếu nó làunsafethì sẽ thật sự rất kỳ quặcRấ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,pinhttps://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ị
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 heapKhi đó 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
Nhờ vậy có một hiệu ứng tốt là có thể hợp nhất và inline các
Futuretrướ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
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
stdmột traitMoveđược tích hợp vào ngôn ngữ ở mức tương tựCopyMovesẽ đị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 Movesẽ không thể bị di chuyểnGầ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 byteNhư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
CopyvàCloneMộ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 projectionunsafethì tôi bỏ cuộcTô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ó
Movesang Rust cóMovesẽ bất tiệnHầu như mọi struct đã viết từ trước đến nay đều phải thêm
#[derive(Move)], vàstdcũng vậyVớ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
MoveVề 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
Movevà future tốt hơnCá 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
Futuretùy chỉnh, tôi vẫn tò mò Pin thực sự được dùng ở đâuChỉ nhìn cách diễn đạt thì có vẻ có thể dùng trong FFI
Ví dụ nếu một hàm
externtrả về*mut Trồi nhận pointer đó, có vẻ như bọc bằngPin<&mut T>có thể đem lại ngữ nghĩa tốt hơnNhư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
Đ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
stddù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
nulltrong 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ồ
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/awaitchí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
Contextxuyê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ộ
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”?
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
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ữ