Một câu chuyện đồng thời tốt hơn là một trong những mục tiêu chính của dự án Rust, vì vậy cần cải thiện dự kiến, miễn là chúng tôi tin tưởng dự án để đạt được mục tiêu của mình. Từ chối trách nhiệm đầy đủ: Tôi có ý kiến cao về Rust và đang đầu tư vào nó. Theo yêu cầu, tôi sẽ cố gắng tránh các đánh giá giá trị và mô tả sự khác biệt thay vì cải tiến (IMHO) .
Rust an toàn và không an toàn
"Rust" bao gồm hai ngôn ngữ: Một ngôn ngữ rất cố gắng để cách ly bạn khỏi những nguy hiểm của lập trình hệ thống và một ngôn ngữ mạnh mẽ hơn mà không có bất kỳ khát vọng nào như vậy.
Unsafe Rust là một ngôn ngữ khó chịu, tàn bạo, cảm thấy rất giống C ++. Nó cho phép bạn làm những việc nguy hiểm tùy tiện, nói chuyện với phần cứng, (quản lý sai) thủ công bộ nhớ, tự bắn vào chân mình, v.v ... Nó rất giống với C và C ++ ở chỗ sự chính xác của chương trình cuối cùng nằm trong tay bạn và bàn tay của tất cả các lập trình viên khác tham gia vào nó. Bạn chọn ngôn ngữ này với từ khóa unsafe
và như trong C và C ++, một lỗi duy nhất ở một vị trí có thể khiến toàn bộ dự án sụp đổ.
Safe Rust là "mặc định", phần lớn mã Rust là an toàn và nếu bạn không bao giờ đề cập đến từ khóa unsafe
trong mã của mình, bạn sẽ không bao giờ rời khỏi ngôn ngữ an toàn. Phần còn lại của bài đăng sẽ chủ yếu liên quan đến ngôn ngữ đó, bởi vì unsafe
mã có thể phá vỡ bất kỳ và tất cả các đảm bảo rằng Rust an toàn hoạt động rất khó để cung cấp cho bạn. Mặt khác, unsafe
mã không phải là xấu và không được cộng đồng đối xử như vậy (tuy nhiên, nó được khuyến khích mạnh mẽ khi không cần thiết).
Điều đó nguy hiểm, đúng, nhưng cũng quan trọng, vì nó cho phép xây dựng các khái niệm trừu tượng mà mã an toàn sử dụng. Mã không an toàn tốt sử dụng hệ thống loại để ngăn người khác sử dụng sai, và do đó, sự hiện diện của mã không an toàn trong chương trình Rust không cần làm phiền mã an toàn. Tất cả những khác biệt sau tồn tại là do các hệ thống loại của Rust có các công cụ mà C ++ không có và vì mã không an toàn thực hiện các tóm tắt đồng thời sử dụng các công cụ này một cách hiệu quả.
Không khác biệt: Bộ nhớ chia sẻ / có thể thay đổi
Mặc dù Rust chú trọng nhiều hơn đến việc truyền thông điệp và kiểm soát rất chặt chẽ bộ nhớ dùng chung, nhưng nó không loại trừ sự tương tranh của bộ nhớ dùng chung và hỗ trợ rõ ràng các khái niệm trừu tượng chung (khóa, hoạt động nguyên tử, biến điều kiện, bộ sưu tập đồng thời).
Hơn nữa, giống như C ++ và không giống như các ngôn ngữ chức năng, Rust thực sự thích các cấu trúc dữ liệu bắt buộc truyền thống. Không có danh sách liên kết liên tục / bất biến trong thư viện chuẩn. Có std::collections::LinkedList
nhưng nó giống như std::list
trong C ++ và không được khuyến khích vì những lý do tương tự như std::list
(sử dụng sai bộ đệm).
Tuy nhiên, với tham chiếu đến tiêu đề của phần này ("bộ nhớ chia sẻ / bộ nhớ có thể thay đổi"), Rust có một điểm khác biệt với C ++: Nó khuyến khích mạnh mẽ rằng bộ nhớ là "chia sẻ XOR có thể thay đổi", nghĩa là bộ nhớ không bao giờ được chia sẻ và có thể thay đổi cùng một lúc thời gian. Thay đổi bộ nhớ như bạn muốn "trong sự riêng tư của chủ đề của riêng bạn", có thể nói như vậy. Tương phản điều này với C ++ trong đó bộ nhớ có thể thay đổi được chia sẻ là tùy chọn mặc định và được sử dụng rộng rãi.
Mặc dù mô hình chia sẻ xor-mutable rất quan trọng đối với những khác biệt dưới đây, nhưng đây cũng là một mô hình lập trình khá khác biệt cần một thời gian để làm quen và đặt ra những hạn chế đáng kể. Đôi khi, người ta phải từ chối mô hình này, ví dụ, với các loại nguyên tử ( AtomicUsize
là bản chất của bộ nhớ có thể thay đổi được chia sẻ). Lưu ý rằng các khóa cũng tuân theo quy tắc chia sẻ-xor-mutable, bởi vì nó loại trừ việc đọc và ghi đồng thời (trong khi một luồng ghi, không có luồng nào khác có thể đọc hoặc ghi).
Không khác biệt: Các cuộc đua dữ liệu là hành vi không xác định (UB)
Nếu bạn kích hoạt một cuộc đua dữ liệu trong mã Rust, thì đó là trò chơi kết thúc, giống như trong C ++. Tất cả các cược đã tắt và trình biên dịch có thể làm bất cứ điều gì nó muốn.
Tuy nhiên, có một đảm bảo chắc chắn rằng mã Rust an toàn không có các cuộc đua dữ liệu (hoặc bất kỳ UB nào cho vấn đề đó). Điều này mở rộng cả ngôn ngữ cốt lõi và thư viện tiêu chuẩn. Nếu bạn có thể viết chương trình Rust không sử dụng unsafe
(bao gồm trong các thư viện bên thứ ba nhưng không bao gồm thư viện chuẩn) kích hoạt UB, thì đó được coi là một lỗi và sẽ được sửa chữa (điều này đã xảy ra nhiều lần). Điều này tất nhiên trái ngược hoàn toàn với C ++, nơi việc viết chương trình với UB là chuyện nhỏ.
Sự khác biệt: Kỷ luật khóa chặt chẽ
Không giống như C ++, một khóa ở Rust ( std::sync::Mutex
, std::sync::RwLock
, vv) sở hữu dữ liệu nó bảo vệ. Thay vì lấy khóa và sau đó thao tác một số bộ nhớ dùng chung chỉ liên quan đến khóa trong tài liệu, dữ liệu được chia sẻ không thể truy cập được trong khi bạn không giữ khóa. Một người bảo vệ RAII giữ khóa và đồng thời cấp quyền truy cập vào dữ liệu bị khóa (điều này có thể được thực hiện bởi C ++, nhưng không phải bởi các std::
khóa). Hệ thống trọn đời đảm bảo rằng bạn không thể tiếp tục truy cập dữ liệu sau khi mở khóa (thả bộ bảo vệ RAII).
Tất nhiên bạn có thể có một khóa không chứa dữ liệu hữu ích ( Mutex<()>
) và chỉ chia sẻ một số bộ nhớ mà không liên kết rõ ràng với khóa đó. Tuy nhiên, có bộ nhớ chia sẻ có khả năng không đồng bộ đòi hỏi unsafe
.
Sự khác biệt: Ngăn ngừa chia sẻ tình cờ
Mặc dù bạn có thể có bộ nhớ chia sẻ, bạn chỉ chia sẻ khi bạn yêu cầu rõ ràng. Ví dụ: khi bạn sử dụng chuyển tin nhắn (ví dụ: các kênh từ std::sync
), hệ thống trọn đời đảm bảo rằng bạn không giữ bất kỳ tham chiếu nào đến dữ liệu sau khi bạn gửi nó đến một luồng khác. Để chia sẻ dữ liệu đằng sau khóa, bạn xây dựng khóa một cách rõ ràng và đưa nó cho một luồng khác. Để chia sẻ bộ nhớ không đồng bộ với unsafe
bạn, tốt, phải sử dụng unsafe
.
Điều này liên quan đến điểm tiếp theo:
Sự khác biệt: Theo dõi an toàn chủ đề
Hệ thống loại của Rust theo dõi một số khái niệm về an toàn luồng. Cụ thể, Sync
đặc điểm biểu thị các loại có thể được chia sẻ bởi một số luồng mà không có nguy cơ về các cuộc đua dữ liệu, trong khi Send
đánh dấu các loại có thể được chuyển từ luồng này sang luồng khác. Điều này được thực thi bởi trình biên dịch trong suốt chương trình, và do đó các nhà thiết kế thư viện dám thực hiện các tối ưu hóa sẽ nguy hiểm một cách ngu ngốc nếu không có các kiểm tra tĩnh này. Ví dụ, các C ++ std::shared_ptr
luôn sử dụng các hoạt động nguyên tử để thao tác số tham chiếu của nó, để tránh UB nếu một shared_ptr
số luồng được sử dụng bởi một số luồng. Rust có Rc
và Arc
, chỉ khác ở chỗ Rc
sử dụng các hoạt động hoàn trả phi nguyên tử và không phải là chủ đề an toàn (tức là không thực hiện Sync
hoặc Send
) trong khi Arc
rất giốngshared_ptr
(và thực hiện cả hai đặc điểm).
Lưu ý rằng nếu một loại không sử dụng unsafe
để thực hiện đồng bộ hóa thủ công, sự hiện diện hoặc vắng mặt của các đặc điểm sẽ được suy luận chính xác.
Sự khác biệt: quy tắc rất nghiêm ngặt
Nếu trình biên dịch không thể hoàn toàn chắc chắn rằng một số mã không có các cuộc đua dữ liệu và các UB khác, nó sẽ không biên dịch, định kỳ . Các quy tắc nói trên và các công cụ khác có thể giúp bạn đi khá xa, nhưng sớm hay muộn bạn sẽ muốn làm điều gì đó chính xác, nhưng vì những lý do tinh tế thoát khỏi thông báo của nhà soạn nhạc. Nó có thể là một cấu trúc dữ liệu không khóa khó khăn, nhưng nó cũng có thể là một thứ gì đó tầm thường như "Tôi viết đến các vị trí ngẫu nhiên trong một mảng được chia sẻ nhưng các chỉ số được tính toán sao cho mọi vị trí chỉ được ghi bởi một luồng".
Tại thời điểm đó, bạn có thể cắn viên đạn và thêm một chút đồng bộ hóa không cần thiết hoặc bạn đặt lại mã để trình biên dịch có thể thấy tính chính xác của nó (thường có thể thực hiện được, đôi khi khá khó, đôi khi không thể) hoặc bạn thả vào unsafe
mã. Tuy nhiên, đó là chi phí tinh thần cao hơn và Rust không cung cấp cho bạn bất kỳ sự đảm bảo nào về tính chính xác của unsafe
mã.
Sự khác biệt: Công cụ ít hơn
Do những khác biệt đã nói ở trên, trong Rust, hiếm khi người ta viết mã có thể có một cuộc đua dữ liệu (hoặc sử dụng sau khi miễn phí, hoặc miễn phí gấp đôi, hoặc ...). Mặc dù điều này là tốt, nhưng nó có tác dụng phụ đáng tiếc là hệ sinh thái để theo dõi các lỗi như vậy thậm chí còn kém phát triển hơn người ta mong đợi đối với giới trẻ và quy mô nhỏ của cộng đồng.
Mặc dù về nguyên tắc, các công cụ như trình khử trùng luồng của valgrind và LLVM có thể được áp dụng cho mã Rust, liệu công cụ này có thực sự hoạt động hay không thay đổi từ công cụ này sang công cụ khác (và ngay cả những công cụ có thể khó thiết lập, đặc biệt là vì bạn không thể tìm thấy bất kỳ tài nguyên -date về cách làm điều đó). Nó thực sự không giúp Rust hiện thiếu một đặc tả thực sự và đặc biệt là một mô hình bộ nhớ chính thức.
Nói tóm lại, viết unsafe
mã Rust chính xác khó hơn viết mã C ++ một cách chính xác, mặc dù cả hai ngôn ngữ đều gần như tương đương về khả năng và rủi ro. Tất nhiên điều này phải được đặt trọng số so với thực tế là một chương trình Rust điển hình sẽ chỉ chứa một phần unsafe
mã tương đối nhỏ , trong khi đó, một chương trình C ++ hoàn toàn là C ++.