W jaki sposób Rust odbiega od funkcji współbieżności C ++?

35

pytania

Próbuję zrozumieć, czy Rust zasadniczo i dostatecznie poprawia funkcje współbieżności C ++, aby zdecydować, czy powinienem poświęcić czas na naukę Rust.

W szczególności, w jaki sposób idiomatyczna rdza poprawia lub w jakimkolwiek stopniu odbiega od udogodnień współbieżności idiomatic C ++?

Czy poprawa (lub rozbieżność) jest w większości składniowa, czy też zasadniczo poprawa (rozbieżność) w paradygmacie? A może to coś innego? Czy też wcale nie jest to poprawa (dywergencja)?


Racjonalne uzasadnienie

Niedawno próbowałem nauczyć się funkcji współbieżności C ++ 14 i wydaje mi się, że coś jest nie tak. Coś jest nie tak. Co czuje się nie tak? Ciężko powiedzieć.

Wydaje mi się, że kompilator tak naprawdę nie próbował mi pomóc w pisaniu poprawnych programów, jeśli chodzi o współbieżność. Wydaje się, że korzystam raczej z asemblera niż kompilatora.

Wprawdzie jest całkiem prawdopodobne, że cierpię na subtelną i wadliwą koncepcję współbieżności. Może jeszcze nie wyczuwam napięcia Bartosza Milewskiego między stanowym programowaniem a wyścigami danych. Być może nie do końca rozumiem, ile dźwięków jest w kompilatorze, a ile w systemie operacyjnym.

thb
źródło

Odpowiedzi:

56

Lepsza historia współbieżności jest jednym z głównych celów projektu Rust, więc należy oczekiwać ulepszeń, pod warunkiem, że ufamy, że projekt osiągnie swoje cele. Pełne wyłączenie odpowiedzialności: Mam wysokie zdanie na temat Rust i jestem w to zainwestowany. Zgodnie z prośbą postaram się unikać ocen wartości i opisywać różnice zamiast ulepszeń (IMHO) .

Bezpieczna i niebezpieczna rdza

„Rust” składa się z dwóch języków: jednego, który bardzo stara się odizolować od niebezpieczeństw związanych z programowaniem systemów, i bardziej zaawansowanego bez takich aspiracji.

Unsafe Rust to paskudny, brutalny język, który przypomina C ++. Pozwala robić dowolnie niebezpieczne rzeczy, rozmawiać ze sprzętem, (źle) zarządzać pamięcią ręcznie, strzelać sobie w stopę itp. Jest bardzo podobny do C i C ++, ponieważ poprawność programu jest ostatecznie w twoich rękach i ręce wszystkich innych zaangażowanych w to programistów. Za pomocą tego słowa kluczowego decydujesz się na ten język unsafe, i tak jak w C i C ++, pojedynczy błąd w jednym miejscu może spowodować awarię całego projektu.

Bezpieczna rdza jest „domyślna”, zdecydowana większość kodu Rust jest bezpieczna, a jeśli nigdy nie wspominasz słowa kluczowego unsafew kodzie, nigdy nie opuszczasz bezpiecznego języka. Reszta postu dotyczy głównie tego języka, ponieważ unsafekod może złamać wszelkie gwarancje, które bezpieczna rdza tak ciężko ci daje. Z drugiej strony, unsafekod nie jest zły i nie jest traktowany jako taki przez społeczność (jest jednak mocno odradzany, gdy nie jest konieczny).

Jest to niebezpieczne, tak, ale także ważne, ponieważ pozwala budować abstrakcje używane w bezpiecznym kodzie. Dobry niebezpieczny kod korzysta z systemu typów, aby zapobiec jego niewłaściwemu użyciu, dlatego obecność niebezpiecznego kodu w programie Rust nie musi zakłócać bezpiecznego kodu. Istnieją wszystkie następujące różnice, ponieważ systemy typu Rust mają narzędzia, których nie ma C ++, oraz ponieważ niebezpieczny kod implementujący abstrakcje współbieżności skutecznie wykorzystuje te narzędzia.

Brak różnicy: pamięć współdzielona / zmienna

Mimo że Rust kładzie większy nacisk na przekazywanie wiadomości i bardzo ściśle kontroluje pamięć współdzieloną, nie wyklucza współbieżności pamięci współdzielonej i wyraźnie obsługuje wspólne abstrakcje (blokady, operacje atomowe, zmienne warunkowe, współbieżne kolekcje).

Ponadto, podobnie jak C ++ i w przeciwieństwie do języków funkcjonalnych, Rust naprawdę lubi tradycyjne imperatywne struktury danych. W standardowej bibliotece nie ma trwałej / niezmiennej połączonej listy. Jest std::collections::LinkedListjednak jak std::listw C ++ i odradzane z tych samych powodów co std::list(złe użycie pamięci podręcznej).

Jednak w odniesieniu do tytułu tej sekcji („pamięć współużytkowana / zmienna”) Rust ma jedną różnicę w stosunku do C ++: Stanowczo zachęca, aby pamięć była „współużytkowana przez mutację XOR”, tzn. Że pamięć nigdy nie jest współużytkowana i można ją jednocześnie modyfikować czas. Mutuj pamięć tak, jak lubisz „w zaciszu własnego wątku”, że tak powiem. Porównaj to z C ++, gdzie współużytkowana zmienna pamięć jest domyślną opcją i jest powszechnie używana.

Chociaż paradygmat współdzielenia xor-mutable jest bardzo ważny dla poniższych różnic, jest to również zupełnie inny paradygmat programowania, do którego przyzwyczajenie się trochę czasu, i który nakłada znaczne ograniczenia. Czasami trzeba zrezygnować z tego paradygmatu, np. Z typami atomowymi ( AtomicUsizejest to istota wspólnej pamięci zmiennej). Zauważ, że zamki są również zgodne z regułą współdzielonego xor-mutable, ponieważ wyklucza to jednoczesne odczytywanie i zapisywanie (podczas gdy jeden wątek pisze, żaden inny wątek nie może czytać ani pisać).

Brak różnicy: wyścigi danych są niezdefiniowanym zachowaniem (UB)

Jeśli uruchomisz wyścig danych w kodzie Rust, gra się skończy, podobnie jak w C ++. Wszystkie zakłady są wyłączone, a kompilator może zrobić, co chce.

Jednak jest to twarda gwarancja, że bezpieczny kod Rust nie ma wyścigów danych (ani żadnego UB w tym zakresie). Dotyczy to zarówno podstawowego języka, jak i standardowej biblioteki. Jeśli możesz napisać program Rust, który nie używa unsafe(w tym w bibliotekach stron trzecich, ale wyklucza bibliotekę standardową), który wyzwala UB, to jest to uważane za błąd i zostanie naprawione (zdarzyło się to już kilka razy). Jest to oczywiście w jaskrawym kontraście do C ++, w którym pisanie programów za pomocą UB jest banalne.

Różnica: ścisła dyscyplina blokująca

W przeciwieństwie do C ++, zamek w Rust ( std::sync::Mutex, std::sync::RwLocketc.) posiada dane to zabezpieczających. Zamiast wziąć blokadę, a następnie manipulować pamięcią współużytkowaną, która jest powiązana z blokadą tylko w dokumentacji, udostępnione dane są niedostępne, dopóki nie przytrzymasz blokady. Strażnik RAII utrzymuje blokadę i jednocześnie zapewnia dostęp do zablokowanych danych (tyle może być zaimplementowane przez C ++, ale nie przez std::blokady). Dożywotni system zapewnia, że ​​po zwolnieniu blokady nie będziesz mieć dostępu do danych (upuść osłonę RAII).

Oczywiście możesz mieć blokadę, która nie zawiera użytecznych danych ( Mutex<()>), i po prostu współdzielić trochę pamięci bez jawnego powiązania jej z tą blokadą. Wymagana jest jednak potencjalnie niezsynchronizowana pamięć współdzielona unsafe.

Różnica: zapobieganie przypadkowemu udostępnieniu

Chociaż możesz współdzielić pamięć, udostępniasz ją tylko wtedy, gdy wyraźnie o to poprosisz. Na przykład, gdy używasz przekazywania wiadomości (np. Kanałów z std::sync), system lifetime zapewnia, że ​​nie zachowasz żadnych odniesień do danych po wysłaniu ich do innego wątku. Aby udostępnić dane za blokadą, wyraźnie skonstruuj blokadę i przekaż ją innemu wątkowi. Aby udostępnić Ci niezsynchronizowaną pamięć unsafe, musisz użyć unsafe.

To wiąże się z kolejnym punktem:

Różnica: śledzenie bezpieczeństwa wątków

System typu Rust śledzi pewne pojęcie bezpieczeństwa nici. W szczególności Synccecha ta oznacza typy, które mogą być współużytkowane przez kilka wątków bez ryzyka wyścigów danych, a jednocześnie Sendoznacza te, które można przenosić z jednego wątku do drugiego. Jest to egzekwowane przez kompilator w całym programie, dlatego projektanci bibliotek mają odwagę dokonywać optymalizacji, które byłyby głupio niebezpieczne bez tych statycznych kontroli. Na przykład C ++, std::shared_ptrktóry zawsze używa operacji atomowych do manipulowania liczbą referencji, aby uniknąć UB, jeśli shared_ptrzdarzy się, że zostanie użyte przez kilka wątków. Rdza ma Rci Arc, które różnią się tylko tym, że Rc używa nieatomowych operacji przeliczania i nie jest wątkowo bezpieczny (tzn. Nie implementuje Synclub Send), podczas gdy Arcjest bardzo podobnyshared_ptr (i implementuje obie cechy).

Zauważ, że jeśli typ nie używa unsafedo ręcznej implementacji synchronizacji, obecność lub brak cech jest poprawnie wywnioskowany.

Różnica: bardzo surowe zasady

Jeśli kompilator nie może być absolutnie pewien, że jakiś kod jest wolny od wyścigów danych i innych UB, nie skompiluje kropki . Wyżej wymienione reguły i inne narzędzia mogą doprowadzić cię dość daleko, ale wcześniej czy później będziesz chciał zrobić coś, co jest poprawne, ale z subtelnych powodów, które wymykają się uwadze kompilatora. Może to być podchwytliwa struktura danych bez blokady, ale może to być coś tak przyziemnego, jak: „Piszę do losowych lokalizacji we wspólnej tablicy, ale indeksy są obliczane w taki sposób, że każda lokalizacja jest zapisywana tylko przez jeden wątek”.

W tym momencie możesz albo ugryźć pocisk i dodać trochę niepotrzebnej synchronizacji, albo przeredagować kod tak, aby kompilator mógł zobaczyć jego poprawność (często wykonalną, czasem dość trudną, a czasem niemożliwą), lub wpadniesz w unsafekod. Mimo to jest to dodatkowe obciążenie umysłowe, a Rust nie daje żadnych gwarancji poprawności unsafekodu.

Różnica: mniej narzędzi

Ze względu na wspomniane różnice w Rust znacznie rzadziej pisze się kod, który może mieć wyścig danych (lub użycie po darmowym, podwójnym wolnym lub ...). Chociaż jest to miłe, ma niefortunny efekt uboczny, że ekosystem do śledzenia takich błędów jest jeszcze bardziej słabo rozwinięty, niż można by się spodziewać, biorąc pod uwagę młodość i niewielki rozmiar społeczności.

Podczas gdy narzędzia takie jak valgrind i dezynfekujący wątek LLVM można w zasadzie zastosować do kodu Rust, to, czy to faktycznie działa, różni się w zależności od narzędzia (a nawet te, które działają, mogą być trudne do skonfigurowania, zwłaszcza, że ​​nie możesz znaleźć żadnego aktualnego -date zasoby, jak to zrobić). Naprawdę nie pomaga to, że Rust obecnie nie ma prawdziwej specyfikacji, a zwłaszcza formalnego modelu pamięci.

Krótko mówiąc, unsafepoprawne pisanie kodu Rust jest trudniejsze niż prawidłowe pisanie kodu C ++, mimo że oba języki są w przybliżeniu porównywalne pod względem możliwości i ryzyka. Oczywiście należy to porównać z faktem, że typowy program Rust będzie zawierał tylko stosunkowo niewielką część unsafekodu, podczas gdy program C ++ jest w pełni C ++.


źródło
6
Gdzie na moim ekranie jest przełącznik upvote +25? Nie mogę tego znaleźć! Ta pouczająca odpowiedź jest bardzo ceniona. Nie pozostawia mi to żadnych oczywistych pytań w kwestiach, które obejmuje. Tak więc, do innych kwestii: jeśli rozumiem dokumentację Rust, Rust ma [a] zintegrowane urządzenia testowe i [b] system kompilacji o nazwie Cargo. Czy według ciebie są one w miarę gotowe do produkcji? Co więcej, jeśli chodzi o Cargo, czy dobrze jest mi pozwolić na dodanie powłoki, skryptów Python i Perl, kompilacji LaTeX itp. Do procesu kompilacji?
thb
2
@thb Testowanie jest bardzo proste (np. bez szyderstwa), ale funkcjonalne. Cargo działa całkiem dobrze, choć jego koncentracja na Rust i odtwarzalności oznacza, że ​​może nie być najlepszym rozwiązaniem do pokrycia wszystkich etapów od kodu źródłowego do ostatecznych artefaktów. Możesz pisać skrypty kompilacji, ale może to nie być odpowiednie dla wszystkich wymienionych elementów. (Ludzie jednak regularnie używają skryptów kompilacji do kompilowania bibliotek C lub znajdowania istniejących wersji bibliotek C, więc nie jest tak, że Cargo przestaje działać, gdy używasz więcej niż czystej Rust).
2
Nawiasem mówiąc, jak na to, co jest warte, twoja odpowiedź wydaje się dość rozstrzygająca. Ponieważ lubię C ++, ponieważ C ++ ma przyzwoite udogodnienia dla prawie wszystkiego, co musiałem zrobić, ponieważ C ++ jest stabilny i szeroko stosowany, do tej pory byłem bardzo zadowolony z używania C ++ do każdego możliwego, lekkiego celu (nigdy nie zainteresowałem się Javą , na przykład). Ale teraz mamy współbieżność i wydaje mi się, że C ++ 14 ma z tym problem. Przez dekadę nie próbowałem dobrowolnie nowego języka programowania, ale (chyba że Haskell wydaje się lepszą opcją) myślę, że będę musiał spróbować 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.właściwie nadal działa nawet z unsafeelementami. Po prostu nieprzetworzone wskaźniki nie są Syncani, Shareco oznacza, że ​​domyślnie struktury zawierające je nie będą miały.
Hauleth,
@ ŁukaszNiemier Może się zdarzyć, że wszystko działa dobrze, ale istnieje miliard sposobów, w jakie może dojść do tego, że niebezpiecznie użytkowany typ może się skończyć, Senda Syncnawet tak naprawdę nie powinien.
-2

Rdza jest również bardzo podobna do Erlanga i Go. Komunikuje się za pomocą kanałów z buforami i warunkowym oczekiwaniem. Podobnie jak Go, rozluźnia ograniczenia Erlanga, umożliwiając współdzielenie pamięci, obsługę zliczania i blokowania referencji atomowych oraz przepuszczanie kanałów od wątku do wątku.

Jednak Rust idzie o krok dalej. Podczas gdy Go ufa ci, że robisz dobrze, Rust przypisuje mentora, który siedzi z tobą i narzeka, jeśli spróbujesz zrobić coś złego. Mentorem Rust jest kompilator. Dokonuje zaawansowanej analizy w celu ustalenia własności wartości przekazywanych wokół wątków i zapewnia błędy kompilacji, jeśli występują potencjalne problemy.

Poniżej cytat z dokumentów RUST.

Reguły własności odgrywają istotną rolę w wysyłaniu wiadomości, ponieważ pomagają nam pisać bezpieczny, współbieżny kod. Zapobieganie błędom w równoczesnym programowaniu jest zaletą, którą uzyskujemy, kompromis polegający na konieczności myślenia o własności w naszych programach Rust. - Przekazywanie wiadomości z własnością wartości.

Jeśli Erlang jest smokowcem, a Go jest stanem wolnym, to Rust jest stanem niani.

Możesz znaleźć więcej informacji z Ideologii współbieżności języków programowania: Java, C #, C, C +, Go i Rust

srinath_perera
źródło
2
Witamy w Stack Exchange! Pamiętaj, że ilekroć prowadzisz do swojego bloga, musisz to wyraźnie zaznaczyć; zobacz centrum pomocy .
Glorfindel,