Làm thế nào để Rust phân kỳ từ các cơ sở đồng thời của C ++?


35

Câu hỏi

Tôi đang cố gắng tìm hiểu liệu Rust có cải thiện căn bản và đầy đủ các tiện ích đồng thời của C ++ để quyết định xem tôi có nên dành thời gian để tìm hiểu Rust hay không.

Cụ thể, Rust thành ngữ cải thiện như thế nào, hoặc ở bất kỳ tỷ lệ nào từ các cơ sở tương tranh của C ++ thành ngữ?

Là sự cải tiến (hoặc phân kỳ) chủ yếu là cú pháp, hay thực chất nó là một sự cải tiến (phân kỳ) trong mô hình? Hay nó là cái gì khác? Hay nó không thực sự là một sự cải thiện (phân kỳ) nào cả?


Cơ sở lý luận

Gần đây tôi đã cố gắng tự dạy mình các phương tiện đồng thời của C ++ 14, và một cái gì đó cảm thấy không hoàn toàn đúng. Một cái gì đó cảm thấy tắt. Những gì cảm thấy tắt? Khó nói.

Cảm giác gần như trình biên dịch không thực sự cố gắng giúp tôi viết chương trình chính xác khi nói đến sự tương tranh. Tôi cảm thấy gần như là tôi đang sử dụng một trình biên dịch chứ không phải là trình biên dịch.

Phải thừa nhận rằng, tôi hoàn toàn có thể gặp phải một khái niệm sai lầm, tinh tế khi nói đến sự tương tranh. Có lẽ tôi chưa làm giảm căng thẳng của Bartosz Milewski giữa các cuộc đua lập trình và dữ liệu. Có lẽ tôi không hiểu lắm về phương pháp đồng thời âm thanh trong trình biên dịch và bao nhiêu phần trong hệ điều hành.

Câu trả lời:


56

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 unsafevà 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 unsafetrong 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ì unsafemã 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, unsafekhông phảixấ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::LinkedListnhưng nó giống như std::listtrong 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ử ( AtomicUsizelà 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 unsafebạ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_ptrluô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_ptrsố luồng được sử dụng bởi một số luồng. Rust có RcArc, 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 Synchoặc Send) trong khi Arcrấ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 unsafemã. 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 unsafemã.

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 unsafemã 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 unsafemã tương đối nhỏ , trong khi đó, một chương trình C ++ hoàn toàn là C ++.


6
Trường hợp trên màn hình của tôi là công tắc upvote +25? Tôi không thể tìm thấy nó! Câu trả lời thông tin này được nhiều đánh giá cao. Nó khiến tôi không có câu hỏi rõ ràng về các điểm nó bao gồm. Vì vậy, đến những điểm khác: Nếu tôi hiểu tài liệu của Rust, Rust có [a] phương tiện thử nghiệm tích hợp và [b] một hệ thống xây dựng có tên là Cargo. Là những sản phẩm hợp lý đã sẵn sàng trong quan điểm của bạn? Ngoài ra, liên quan đến Cargo, có hài hước về việc cho phép tôi thêm shell, Python và Perl script, trình biên dịch LaTeX, v.v., vào quy trình xây dựng không?
THB

2
@thb Công cụ kiểm tra rất đơn giản (ví dụ: không chế nhạo) nhưng có chức năng. Hàng hóa hoạt động khá tốt, mặc dù tập trung vào Rust và tái sản xuất có nghĩa là nó có thể không phải là lựa chọn tốt nhất để bao gồm tất cả các bước từ mã nguồn đến các tạo phẩm cuối cùng. Bạn có thể viết các tập lệnh xây dựng nhưng điều đó có thể không phù hợp với tất cả những điều bạn đề cập. (Người ta, tuy nhiên, thường xuyên sử dụng xây dựng các kịch bản để biên soạn các thư viện C hoặc tìm các phiên bản hiện có của thư viện C vì vậy nó không giống như Cargo ngừng hoạt động khi bạn sử dụng nhiều hơn tinh khiết Rust.)

2
Nhân tiện, với những gì đáng giá, câu trả lời của bạn có vẻ khá kết luận. Vì tôi thích C ++, vì C ++ có các phương tiện phù hợp cho hầu hết mọi thứ tôi cần làm, vì C ++ ổn định và được sử dụng rộng rãi, nên tôi đã rất hài lòng khi sử dụng C ++ cho mọi mục đích không trọng lượng có thể (Tôi chưa bao giờ quan tâm đến Java , ví dụ). Nhưng bây giờ chúng tôi có sự tương tranh, và C ++ 14 dường như tôi đang phải vật lộn với nó. Tôi đã không tự nguyện thử một ngôn ngữ lập trình mới trong một thập kỷ, nhưng (trừ khi Haskell sẽ xuất hiện một lựa chọn tốt hơn) Tôi nghĩ rằng tôi sẽ phải thử Rust.
THB

Note that if a type doesn't use unsafe to manually implement synchronization, the presence or absence of the traits are inferred correctly.thực tế nó vẫn làm ngay cả với unsafecác yếu tố Chỉ cần con trỏ nguyên không phải là Synccũng không Sharecó nghĩa là rằng bằng struct mặc định chứa chúng sẽ có không.
Hauleth

@ UkaszNiemier Nó có thể xảy ra để giải quyết ổn thỏa, nhưng có một tỷ cách mà một loại sử dụng không an toàn có thể kết thúc Sendhoặc Syncmặc dù nó thực sự không nên.

-2

Rust cũng giống như Erlang và Go. Nó giao tiếp bằng cách sử dụng các kênh có bộ đệm và chờ đợi có điều kiện. Giống như Go, nó giúp giảm bớt các hạn chế của Erlang bằng cách cho phép bạn thực hiện bộ nhớ dùng chung, hỗ trợ đếm và khóa tham chiếu nguyên tử và bằng cách cho phép bạn chuyển các kênh từ luồng sang luồng.

Tuy nhiên, Rust tiến thêm một bước. Trong khi Go tin tưởng bạn làm điều đúng đắn, Rust chỉ định một người cố vấn ngồi cùng bạn và phàn nàn nếu bạn cố gắng làm điều sai trái. Cố vấn của Rust là trình biên dịch. Nó thực hiện phân tích tinh vi để xác định quyền sở hữu các giá trị được truyền xung quanh các luồng và cung cấp các lỗi biên dịch nếu có vấn đề tiềm ẩn.

Sau đây là một trích dẫn từ các tài liệu RUST.

Các quy tắc sở hữu đóng một vai trò quan trọng trong việc gửi tin nhắn vì chúng giúp chúng ta viết mã an toàn, đồng thời. Ngăn ngừa lỗi trong lập trình đồng thời là lợi thế chúng ta có được bằng cách đánh đổi việc phải suy nghĩ về quyền sở hữu trong suốt các chương trình Rust của chúng tôi. - Thông điệp chuyển với quyền sở hữu các giá trị.

Nếu Erlang là hà khắc và Go là trạng thái tự do, thì Rust là trạng thái bảo mẫu.

Bạn có thể tìm thêm thông tin từ các hệ tư tưởng đồng thời của Ngôn ngữ lập trình: Java, C #, C, C +, Go và Rust


2
Chào mừng bạn đến với Sàn giao dịch Stack! Xin lưu ý rằng bất cứ khi nào bạn liên kết đến blog của riêng bạn, bạn cần phải nêu rõ ràng; xem trung tâm trợ giúp .
Glorfindel
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.