Java i C # zapewniają bezpieczeństwo pamięci, sprawdzając granice tablic i dereferencje wskaźnika.
Jakie mechanizmy można zaimplementować w języku programowania, aby zapobiec możliwym warunkom wyścigowym i impasom?
Java i C # zapewniają bezpieczeństwo pamięci, sprawdzając granice tablic i dereferencje wskaźnika.
Jakie mechanizmy można zaimplementować w języku programowania, aby zapobiec możliwym warunkom wyścigowym i impasom?
Odpowiedzi:
Wyścigi występują, gdy masz jednocześnie aliasing obiektu i przynajmniej jeden z nich jest mutowany.
Aby zapobiec wyścigom, musisz sprawić, by jeden lub więcej z tych warunków był nieprawdziwy.
Różne podejścia dotyczą różnych aspektów. Programowanie funkcjonalne podkreśla niezmienność, która usuwa zmienność. Blokowanie / atomika usuwają jednoczesność. Typy afiniczne usuwają aliasing (Rust usuwa modyfikowalne aliasing). Modele aktorów zwykle usuwają aliasing.
Możesz ograniczyć obiekty, które można aliasować, aby łatwiej było uniknąć powyższych warunków. Tam właśnie wkraczają kanały i / lub style przekazywania wiadomości. Nie możesz aliasować dowolnej pamięci, tylko koniec kanału lub kolejki, która jest skonfigurowana tak, aby była wolna od wyścigów. Zwykle przez unikanie jednoczesności, tj. Zamków lub atomów.
Minusem tych różnych mechanizmów jest to, że ograniczają programy, które można pisać. Im bardziej tępe ograniczenie, tym mniej programów. Więc żadne aliasing lub modyfikowalność nie działają i są łatwe do uzasadnienia, ale są bardzo ograniczające.
Właśnie dlatego Rust wywołuje takie poruszenie. Jest to język inżynierski (w porównaniu z językiem akademickim), który obsługuje aliasing i zmienność, ale ma kompilator sprawdzający, czy nie występują one jednocześnie. Chociaż nie jest to idealne, pozwala bezpiecznie pisać większą klasę programów niż wiele jego poprzedników.
Java i C # zapewniają bezpieczeństwo pamięci, sprawdzając granice tablic i dereferencje wskaźnika.
Ważne jest, aby najpierw pomyśleć o tym, jak robią to C # i Java. Robią to, konwertując to, co jest niezdefiniowane zachowanie w C lub C ++ w określone zachowanie: zawiesić program . Dereferencje zerowe i wyjątki indeksu tablicowego nigdy nie powinny być wychwytywane w poprawnym programie C # lub Java; nie powinny się zdarzyć w pierwszej kolejności, ponieważ program nie powinien mieć tego błędu.
Ale nie sądzę, że masz na myśli to pytanie! Moglibyśmy dość łatwo napisać środowisko uruchomieniowe „zakleszczone”, które okresowo sprawdza, czy n wzajemnie na siebie nie czeka i kończy program, jeśli tak się stanie, ale nie sądzę, żeby to cię zadowoliło.
Jakie mechanizmy można zaimplementować w języku programowania, aby zapobiec możliwym warunkom wyścigowym i impasom?
Kolejnym problemem, przed którym stoimy, jest to, że „warunki wyścigu”, w przeciwieństwie do impasu, są trudne do wykrycia. Pamiętaj, że naszym celem w bezpieczeństwie nici nie jest eliminowanie wyścigów . Chcemy, aby program był poprawny bez względu na to, kto wygra wyścig ! Problem z warunkami wyścigu nie polega na tym, że dwa wątki biegną w nieokreślonej kolejności i nie wiemy, kto skończy jako pierwszy. Problem z warunkami wyścigu polega na tym, że programiści zapominają, że niektóre zamówienia wykończenia wątków są możliwe i nie uwzględniają tej możliwości.
Więc twoje pytanie sprowadza się w zasadzie do „czy jest jakiś sposób, że język programowania może zapewnić, że mój program jest poprawny?” a odpowiedź na to pytanie brzmi w praktyce nie.
Do tej pory krytykowałem tylko twoje pytanie. Pozwól mi spróbować zmienić bieg tutaj i zająć się duchem twojego pytania. Czy projektanci języków mogą dokonać wyboru, który złagodziłby straszną sytuację związaną z wielowątkowością?
Sytuacja jest naprawdę okropna! Poprawienie wielowątkowego kodu, szczególnie w przypadku słabych architektur modeli pamięci, jest bardzo, bardzo trudne. Warto zastanowić się, dlaczego jest to trudne:
Jest więc oczywisty sposób, że projektanci języków mogą poprawić sytuację. Porzuć wydajność wygrywa nowoczesne procesory . Spraw, aby wszystkie programy, nawet te wielowątkowe, miały wyjątkowo silny model pamięci. Spowoduje to, że programy wielowątkowe będą o wiele, wiele razy wolniejsze, co działa bezpośrednio przeciwko powodom posiadania programów wielowątkowych w pierwszej kolejności: w celu zwiększenia wydajności.
Nawet pomijając model pamięci, istnieją inne powody, dla których wielowątkowość jest trudna:
Ten ostatni punkt zawiera dalsze wyjaśnienia. Przez „kompozycyjny” rozumiem co następuje:
Załóżmy, że chcemy obliczyć int int dublet. Piszemy poprawną implementację obliczeń:
int F(double x) { correct implementation here }
Załóżmy, że chcemy obliczyć ciąg podany jako int:
string G(int y) { correct implementation here }
Teraz jeśli chcemy obliczyć ciąg znaków, biorąc pod uwagę podwójne:
double d = whatever;
string r = G(F(d));
G i F można skomponować jako prawidłowe rozwiązanie bardziej złożonego problemu.
Ale zamki nie mają tej właściwości z powodu zakleszczeń. Prawidłowa metoda M1, która przyjmuje blokady w kolejności L1, L2, oraz poprawna metoda M2, która przyjmuje blokady w kolejności L2, L1, nie mogą być używane w tym samym programie bez utworzenia niepoprawnego programu. Zamki sprawiają, że nie można powiedzieć „każda metoda jest poprawna, więc wszystko jest prawidłowe”.
Co więc możemy zrobić jako projektanci języków?
Po pierwsze, nie idź tam. Wiele wątków kontroli w jednym programie jest złym pomysłem, a dzielenie pamięci między wątkami jest złym pomysłem, więc nie umieszczaj go w języku ani w środowisku wykonawczym.
To najwyraźniej nie jest starter.
Zwróćmy zatem uwagę na bardziej fundamentalne pytanie: dlaczego w ogóle mamy wiele wątków? Są dwa główne powody i często łączą się w to samo, choć są bardzo różne. Są skonfliktowane, ponieważ oba dotyczą zarządzania opóźnieniami.
Kiepski pomysł. Zamiast tego należy użyć asynchronicznej jednowątkowej za pomocą coroutines. C # robi to pięknie. Java, nie tak dobrze. Jest to jednak główny sposób, w jaki obecny projektanci języków pomagają rozwiązać problem wątków. await
Operator C # (inspirowany f # asynchroniczne Procedury i drugi stan techniki) jest włączone w coraz większej liczbie języków.
Projektanci języków mogą pomóc, tworząc funkcje językowe, które działają dobrze z równoległością. Pomyśl na przykład o tym, jak LINQ rozszerza się tak naturalnie na PLINQ. Jeśli jesteś rozsądną osobą i ograniczasz swoje operacje TPL do operacji związanych z procesorem, które są wysoce równoległe i nie dzielą pamięci, możesz tutaj uzyskać duże wygrane.
Co jeszcze możemy zrobić?
C # nie pozwala ci czekać w zamku, ponieważ jest to przepis na zakleszczenia. C # nie pozwala na zablokowanie typu wartości, ponieważ zawsze jest to niewłaściwa czynność; blokujesz pudełko, a nie wartość. C # ostrzega cię, jeśli masz alias niestabilny, ponieważ alias nie narzuca semantyki pobierania / wydawania. Kompilator może wykrywać typowe problemy i im zapobiegać na wiele innych sposobów.
C # i Java popełniły ogromny błąd projektowy, pozwalając na użycie dowolnego obiektu referencyjnego jako monitora. To zachęca do wszelkiego rodzaju złych praktyk, które utrudniają wyśledzenie impasu i trudniej jest zapobiegać im statycznie. I marnuje bajty w każdym nagłówku obiektu. Należy wymagać, aby monitory pochodziły z klasy monitorów.
STM to piękny pomysł i bawiłem się implementacjami zabawek w Haskell; pozwala znacznie bardziej elegancko komponować prawidłowe rozwiązania z prawidłowych części niż rozwiązania oparte na blokadzie. Nie wiem jednak wystarczająco dużo na temat szczegółów, aby stwierdzić, dlaczego nie można było sprawić, by działało na dużą skalę; spytaj Joe Duffy'ego następnym razem, gdy go zobaczysz.
Przeprowadzono wiele badań nad językami opartymi na rachunku procesowym i nie rozumiem tej przestrzeni zbyt dobrze; spróbuj sam przeczytać kilka artykułów na ten temat i sprawdź, czy uzyskasz jakieś spostrzeżenia.
Po tym, jak pracowałem w Microsoft na Roslyn, pracowałem w Coverity, a jedną z rzeczy, które zrobiłem, było uzyskanie frontonu analizatora przy użyciu Roslyn. Dysponując dokładną analizą leksykalną, składniową i semantyczną firmy Microsoft, mogliśmy skoncentrować się na ciężkiej pracy nad pisaniem detektorów, które wykryły typowe problemy z wielowątkowością.
Podstawowym powodem, dla którego mamy wyścigi, impasy i tak dalej, jest to, że piszemy programy, które mówią, co robić , i okazuje się, że wszyscy jesteśmy gówniani w pisaniu programów imperatywnych; komputer robi to, co mu powiesz, a my mówimy, żeby robił złe rzeczy. Wiele współczesnych języków programowania coraz częściej mówi o programowaniu deklaratywnym: powiedz, jakie wyniki chcesz, i pozwól kompilatorowi znaleźć skuteczny, bezpieczny i prawidłowy sposób na osiągnięcie tego wyniku. Ponownie pomyśl o LINQ; chcemy, żebyś powiedział from c in customers select c.FirstName
, co wyraża intencję . Pozwól kompilatorowi dowiedzieć się, jak napisać kod.
Algorytmy uczenia maszynowego są znacznie lepsze w niektórych zadaniach niż algorytmy kodowane ręcznie, choć oczywiście istnieje wiele kompromisów, w tym poprawność, czas potrzebny na szkolenie, błędy wynikające z niewłaściwego szkolenia i tak dalej. Jest jednak prawdopodobne, że bardzo wiele zadań, które obecnie kodujemy „ręcznie”, wkrótce będzie można zastosować do rozwiązań generowanych maszynowo. Jeśli ludzie nie piszą kodu, nie piszą błędów.
Przepraszam, że trochę się tam włóczyło; jest to ogromny i trudny temat, a społeczność PL nie wypracowała wyraźnego konsensusu w ciągu 20 lat, gdy śledzę postępy w tej dziedzinie problemów.