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 unsafe
w kodzie, nigdy nie opuszczasz bezpiecznego języka. Reszta postu dotyczy głównie tego języka, ponieważ unsafe
kod może złamać wszelkie gwarancje, które bezpieczna rdza tak ciężko ci daje. Z drugiej strony, unsafe
kod 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::LinkedList
jednak jak std::list
w 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 ( AtomicUsize
jest 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::RwLock
etc.) 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 Sync
cecha ta oznacza typy, które mogą być współużytkowane przez kilka wątków bez ryzyka wyścigów danych, a jednocześnie Send
oznacza 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_ptr
który zawsze używa operacji atomowych do manipulowania liczbą referencji, aby uniknąć UB, jeśli shared_ptr
zdarzy się, że zostanie użyte przez kilka wątków. Rdza ma Rc
i Arc
, które różnią się tylko tym, że Rc
używa nieatomowych operacji przeliczania i nie jest wątkowo bezpieczny (tzn. Nie implementuje Sync
lub Send
), podczas gdy Arc
jest bardzo podobnyshared_ptr
(i implementuje obie cechy).
Zauważ, że jeśli typ nie używa unsafe
do 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 unsafe
kod. Mimo to jest to dodatkowe obciążenie umysłowe, a Rust nie daje żadnych gwarancji poprawności unsafe
kodu.
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, unsafe
poprawne 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ęść unsafe
kodu, podczas gdy program C ++ jest w pełni C ++.