stats

Poczytaj mi Clojure, cz. 15

Współbieżność

Grafika

Wykonywanie współbieżne pozwala spożytkować więcej niż jedną jednostkę obliczeniową komputera podczas wykonywania zadań, które można zrównoleglić. Pomaga też lepiej zarządzać czasem, gdy pewne operacje muszą oczekiwać na obsługę komunikacji międzyprocesowej lub podsystemu wejścia/wyjścia. Clojure proponuje przejrzystą implementację wykonywania współbieżnego, wykorzystującą typy referencyjne i programową pamięć transakcyjną.

Współbieżność

Wykonywanie współbieżne (ang. concurrent) to cecha systemów i programów komputerowych, w których te same obliczenia dokonywane są jednocześnie przez więcej niż jeden komponent (np. uruchomiony program bądź wątek programu). Opcjonalnie komponenty działające równolegle mogą komunikować się między sobą i synchronizować wykonywane obliczenia, jeżeli wymaga tego przyjęty algorytm.

Praktyczną zaletą wykonywania współbieżnego jest możliwość spożytkowania mocy więcej niż jednej centralnej jednostki obliczeniowej (ang. Central Processing Unit, skr. CPU) w tym samym czasie. Jednostką tą będzie najczęściej rdzeń procesorowy (ang. CPU core). Zysk wydajnościowy możemy zaobserwować nawet wtedy, gdy mamy do czynienia z wieloma wątkami (ang. threads) realizowanymi przez ten sam układ: w przypadku obliczeń realizowanych w czasie zablokowania innego wątku podczas operacji wejścia/wyjścia, a także w procesorach superskalarnych z obsługą jednoczesnej wielowątkowości (ang. simultaneous multithreading, skr. SMT).

Przypomnijmy: wątek (ang. thread) to wykonywana współbieżnie część konkretnego procesu (uruchomionego programu). Ponieważ działa w obrębie tego samego procesu może uzyskiwać bezpośredni dostęp do jego przestrzeni adresowej i operować na pamięciowych zasobach na tych samych prawach, co inne wątki. Może też korzystać z zasobów systemowych należących do uruchamianego programu: mechanizmów komunikacji międzyprocesowej, deskryptorów plików itd. Programowanie z użyciem wątków musi więc uwzględniać odpowiednie mechanizmy izolacji i ochrony przed konfliktami spowodowanymi jednoczesnym i niekoordynowanym zapisem do pamięciowych struktur bądź systemowych obiektów.

Przetwarzanie równoległe

Istnieje wiele sposobów równoległego wykonywania zadań przez programy komputerowe i są one zależne nie tylko od przyjętych algorytmów czy używanego języka programowania, ale również od mechanizmów obsługi obecnych w systemie operacyjnym i/lub maszynie wirtualnej.

Popularne przetwarzanie wielowątkowe (ang. multi-threaded computing) jest często obarczone ryzykiem występowania błędów związanych z implementacjami w językach programowania. Dzieje się tak przede wszystkim ze względu na obecność danych mutowalnych, co wprowadza wymóg koordynowania zmian, czyli dbania o to, aby modyfikacje we współdzielonych przez kilka wątków strukturach były wykonywane synchronicznie i/lub atomowo.

Mamy tu na myśli zachowanie odpowiedniej kolejności wszystkich lub wybranych operacji przeprowadzanych na tych samych danych, a także wprowadzanie zmian w triadach odczyt–przeliczenie–zapis przy dostępie do pamięciowego obiektu na wyłączność (blokowanie). W efekcie wszystkie wątki obliczeniowe oczekują, aż jeden z nich zakończy pracę ze strukturą. W podejściu tym nie ma nic złego, ponieważ fundamentalnie wynika ono z architektury współczesnych komputerów, gdzie mamy do czynienia ze stałymi obszarami pamięciowymi. Problem pojawia się wtedy, gdy wykorzystywany język programowania wymaga, aby programista zagłębiał się w implementację tego typu mechanizmów i dbał o to, aby nie dochodziło do usterek.

Oczywistym negatywnym przykładem mechanizmów przetwarzania wielowątkowego będą blokady (ang. locks) dostępu do współdzielonych zasobów, których użycie skutkować może niespodziankami w postaci tzw. zakleszczeń (ang. deadlocks). Można unikać tego typu incydentów przez poświęcanie należytej uwagi procesowi tworzenia oprogramowania. Wymaga to jednak inwestowania przez programistę czasu w myślenie o obsłudze technicznej wielowątkowości, zabierając cenne minuty, które mógłby przeznaczyć na opracowywanie rozwiązań problemów związanych z logiką biznesową aplikacji.

Bezpieczeństwo

Chociaż Clojure korzysta z systemu wątków JVM, programowanie współbieżne w tym języku jest łatwiejsze niż w Javie z kilku powodów. Po pierwsze dane są niemutowalnestruktury danych trwałe, więc większość informacji można przetwarzać niezależnie od tego, z jak wieloma równoległymi operacjami mamy do czynienia. Oczywiście przy założeniu, że konkretny problem obliczeniowy można zrównoleglić.

Gdy mówimy o bezpieczeństwie w kontekście przetwarzania współbieżnego z użyciem wątków, mamy na myśli dwa procesy:

  • sterowanie równoległym wykonywaniem,
  • sterowanie widocznością obiektów pamięciowych.

Pierwszy z nich dotyczy sposobów realizowania obliczeń: rozpoczynania i kończenia ich pracy, zdolności do zrównoleglania operacji i kolejności wartościowania wyrażeń. Każdy z omówionych niżej mechanizmów obsługi współbieżności w Clojure różni się nieco pod tym względem.

Drugi proces związany jest z tym, czy i w jaki sposób współdzielone wartości będą udostępniane innym wątkom, jeżeli chodzi o ich odczytywanie czy aktualizowanie. Warto przy okazji wspomnieć, że obsługa współbieżności w JVM, a także w procesorach, korzysta z mechanizmów buforowania zawartości pamięci operacyjnej, tzn. operacje na współdzielonych obiektach mogą czasami odwoływać się do kopii danych z RAM-u znajdujących się w pamięciach podręcznych procesora (ang. CPU cache), a nie do właściwych komórek.

W Clojure większość mechanizmów obsługujących współbieżne i synchroniczne wykonywanie samodzielnie kontroluje dostęp do pamięci i nie pozwala na korzystanie z pamięci typu cache, chociaż istnieją w tym względzie wyjątki (np. omówione w dalszej części obiekty typu Volatile).

Typy referencyjne w Clojure

Zagnieżdżanie wywołań funkcji czy korzystanie z powiązań leksykalnych pozwalają tworzyć programy, które operują na stałych wartościach. Główna funkcja przyjmuje wartość lub zestaw wartości i przeprowadza na nich obliczenia, korzystając z wywołań innych funkcji. W ich ciałach mogą znaleźć się też konstrukcje, które wygenerują wartości tymczasowe, jednak one również będą niemodyfikowalne.

Takie podejście jest wystarczające, gdy pisany przez nas kod ma zrealizować jakieś (nawet skomplikowane) obliczenia, ale może nie być najlepszym pomysłem, kiedy potrzebujemy modelować rozwiązania bardziej skomplikowanych problemów otaczającego nas świata. Dlaczego? Mamy w nim często do czynienia ze zmiennymi stanami abstrakcyjnych obiektów o stałych tożsamościach. Innymi słowy: aby w łatwy sposób odzwierciedlać zmienne otoczenie, w którym program operuje, potrzebujemy obiektów o ustalonych nazwach, które w miarę upływu czasu mogłyby wyrażać różne wartości.

Przykładem stałej tożsamości o zmiennych stanach może być stan magazynowy – aktualizowany z wykorzystaniem różnych operacji, które sprawiają, że pojawiać się w nim będą bądź znikać obiekty składowe reprezentujące przedmioty. Wyrażając go w programie komputerowym, moglibyśmy użyć tylko argumentów funkcji i zwracanych przez nie wartości, ale byłoby to mało czytelne. Gdy dodamy do tego konieczność współbieżnej obsługi, możemy zauważyć, że kod będzie zagmatwany i podatny na pojawianie się w nim błędów.

Jeszcze jaskrawszą potrzebą obsługi zmiennych stanów będzie obsługa interfejsu użytkownika, którego części (zawartości okien, wciskane przyciski itd.) wciąż ulega zmianom, których źródłem jest nie tylko program, ale też użytkownik.

Dzięki obecnym w Clojure typom referencyjnym (ang. reference types), możemy tworzyć globalne lub ograniczone kontekstem tożsamości, których stany mogą się zmieniać. W praktyce oznacza to obiekty pozwalające na odnoszenie się do umieszczonych w pamięci stałych wartości z możliwością aktualizowania tych odniesień na różne sposoby – zależnie od przyjętego modelu zarządzania równoległym wykonywaniem i widocznością obiektów.

Aktualną wartość skojarzoną z konkretnym obiektem typu referencyjnego będziemy nazywali jego wartością bieżącą lub wartością współdzieloną, ponieważ będzie to powiązania z nim wartość widoczna we wszystkich lub w wybranych wątkach programu.

Do dyspozycji mamy następujące klasy obiektów referencyjnych:

  • zmienne globalnedynamiczne – typ Var,
  • zmienne lokalne – typ Var,
  • Atomy – typ Atom,
  • Agenty  – typ Agent,
  • Refy – typ Ref,
  • Future’y – typ Future,
  • Promise’y – typ Promise,
  • Delay’e – typ Delay,
  • Volatile’e – typ Volatile.

Mechanizmy obsługi typów referencyjnych dostarczają odpowiedniej warstwy abstrakcji, pozwalającej tworzyć stałe tożsamości wyrażające zmieniające się stany, a także dbają o to, aby aktualizowanie tych stanów było bezpieczne w kontekście przetwarzania współbieżnego.

Sposoby dostępu

Typy referencyjne różnią się sposobami obsługi dostępu do potencjalnie współdzielonych wartości bieżących, do których przechowują odniesienia. Przez współdzielenie rozumiemy widoczność obiektu referencyjnego w więcej niż jednym wątku wykonywania. Opisując typy referencyjne, będziemy korzystali z następujących określeń, które je charakteryzują:

  • Dostęp synchroniczny (ang. synchronous) to taki, gdzie operacja aktualizacji odniesienia w obiekcie referencyjnym (ustawienia bądź przeliczenia wartości bieżącej) odbywa się do skutku, a wątek, w którym się ona wydarza nie realizuje innych zadań, lecz oczekuje na poprawne wykonanie tej czynności, co wiąże się z uzyskaniem chwilowej wyłączności w dostępie. Przykładem zastosowania może być zwiększanie wartości współdzielonego licznika, która później w tym samym bądź w innym wątku będzie odczytana i użyta. Po wykonaniu operacji uaktualnienia mamy pewność, że wartość bieżąca licznika została zmieniona wyłącznie przez jeden wątek, a więc nie doszło do pominięcia jakiejś wcześniejszej operacji. Warto zauważyć, że nie możemy mieć stuprocentowej pewności odnośnie kolejności operacji, które oczekują na wykonanie w przybliżonym czasie – wszystkie zostaną zrealizowane, każda z osobna, lecz możemy nie wiedzieć, który z wątków wprowadzi zmianę wcześniej, gdy kilka jednocześnie ją zleci. Jeżeli pojawi się taka potrzeba, można temu zaradzić przez wprowadzenie odpowiedniej struktury kontrolnej jako wartości bieżącej, która grupowała będzie właściwą wartość i informację na temat czasu bądź numeru wykonanej operacji (zależnie od przyjętej logiki).

  • Dostęp asynchroniczny (ang. asynchronous) to taki, gdzie operacja aktualizacji odniesienia w obiekcie referencyjnym może być zlecona do wykonania równolegle z innymi i nie powoduje zablokowania pracy realizującego ją wątku. Kod wykonywany w danym wątku nie będzie oczekiwał na jej powodzenie, lecz zajmie się obliczaniem wartości kolejnych wyrażeń. Informacja o bieżącym stanie (a także o tym, czy doszło do udanej aktualizacji wartości) nie jest dostępna od razu, lecz może być pozyskana z użyciem odpowiednich funkcji. W gestii programisty leży więc sprawdzanie czy doszło do poprawnego uaktualnienia referencji, jeżeli taka wiedza jest potrzebna. Przykładem dostępu asynchronicznego jest zwiększanie wartości licznika sumującego liczbę wykonań jakichś czynności (np. wpisów w raporcie zdarzeń), który zostanie odczytany później, gdy wątek lub cała aplikacja będzie kończyć pracę. Inny przykład to obsługa konwersacji w serwerze internetowych pogawędek, gdzie wymieniane przez użytkowników komunikaty są wysyłane do podprogramów działających w obrębie wątków obsługujących ich sesje. Incydentalne zmiany kolejności czy opóźnienia wysyłanych wypowiedzi (wysłanych w okolicach tej samej sekundy) nie stanowią tu dużego problemu.

  • Dostęp koordynowany (ang. coordinated) to taki, gdzie operacja odczytu powiązanej wartości bieżącej lub aktualizacji odniesienia w obiekcie referencyjnym zależy od rezultatów wykonania operacji na innych obiektach. Przykładem może być przelew z jednego rachunku bankowego na drugi, podczas którego należy w sposób atomowy zmienić stany obu rachunków jednocześnie lub zrezygnować, gdy ilość środków jest niewystarczająca.

  • Dostęp niekoordynowany (ang. uncoordinated) to taki, gdzie operacja odczytu powiązanej wartości bieżącej lub aktualizacji odniesienia w obiekcie referencyjnym odbywa się niezależnie od operacji na innych obiektach. Przykład to kapitalizacja odsetek na rachunku bankowym, która może być przeprowadzana równolegle na wielu rachunkach.

  • Dostęp izolowany w wątku (ang. thread-isolated) to taki, gdzie mamy do czynienia z odczytem bądź zmianą wartości bieżącej obiektu referencyjnego, ale tylko w obrębie bieżącego wątku wykonywania.

  • Dostęp współdzielony między wątkami (ang. thread-shared) to taki, gdzie mamy do czynienia z odczytem bądź zmianą wartości bieżącej obiektu referencyjnego w dowolnym z wątków.

Zmienne globalne i dynamiczne

Zmienne globalne pozwalają utrzymywać stałą tożsamość, która będzie wyrażała bardzo rzadko zmieniający się lub niezmienny stan. Podstawowym zastosowaniem globalnych zmiennych jest nazywanie innych obiektów, np. funkcji używanych w programie czy stałych parametrów aplikacji. Zmiany stanów są współdzielone między wątkami, włączając w to współdzielenie tzw. powiązania głównego, zwanego też wartością początkową. Zdarzać się będzie, że zmiennej globalnej użyjemy nie tylko do nadania nazwy funkcji czy stałej wartości, lecz do nazwania obiektu referencyjnego, aby uwidocznić go w programie, nadając mu symboliczny identyfikator.

Zmienne dynamiczne, będące wariantem zmiennych globalnych, zachowują się podobnie, jednak możliwe jest tworzenie z ich użyciem izolowanych w wątku przesłonięć wartości bieżącejzasięgu dynamicznym (w ciele makra binding i podobnych). Zachowane zostaje przy tym współdzielone między wątkami powiązanie główne, które wyraża wartość początkową. Warto pamiętać, że owe przesłonięcia będą dotyczyły odniesienia w obiekcie typu Var, a nie w obiekcie nim wskazywanym, którym może być na przykład inny konstrukt referencyjny.

Ze zmiennych dynamicznych korzysta się na m.in. do określania elementów konfiguracji, które mogą być modyfikowane w pewnych obszarach działania programu, a także przy tzw. programowaniu aspektowym, gdzie w zależności od kontekstu zmienia się charakter operacji wykonywanych na danych – dochodzi po prostu do podmiany obiektów funkcyjnych przy zachowaniu ich symbolicznych nazw w różnych kontekstach.

Zarówno zmienne globalne, jak również ich dynamiczny wariant, pozwalają na zmianę wartości bieżących w sposób niekoordynowany. Odczyty są nieblokujące, a ustawianie nowych wartości domyślnie dokonywane jest asynchronicznie (również nie powodując blokowania).

W celu synchronicznej zmiany powiązania głównego obiektu typu Var można jednak użyć blokującej formy alter-var-root, której jako jeden z argumentów wywołania przekazać należy funkcję obliczającą nową wartość. Będzie ona wywołana tylko raz.

Zobacz także:

Zmienne lokalne

Zmienne lokalne pozwalają utrzymywać stałą tożsamość, która będzie wyrażała zmieniający się stan izolowany w bieżącym wątku, a dodatkowo o leksykalnym zasięgu ograniczonym ciałem specjalnej formy. Są odpowiednikiem konwencjonalnych zmiennych znanych z imperatywnie zakorzenionych języków programowania, a służą do wyrażania algorytmów, które trudno byłoby zapisać funkcyjnie.

Zmienne lokalne korzystają z nieinternalizowanych obiektów typu Var, czyli takich, które nie są umieszczane w przestrzeniach nazw. Powiązania dla nich utrzymywane są na lokalnym stosie, tworzonym na potrzeby zrealizowania kodu makra with-local-vars.

W przypadku zmiennych lokalnych zmiany są niekoordynowanesynchroniczne, przy czym ta ostatnia cecha wynika z ich uwidocznienia wyłącznie w lokalnym wątku wykonywania.

Zobacz także:

Atomy

Atom jest mechanizmem obsługi zmian stanów dla tożsamości, które są współdzielone między wątkami, przy czym modyfikacje odniesień odbywać się mogą w sposób synchronicznyniekoordynowany.

Aktualizacje wartości bieżących Atomów blokują bieżący wątek, natomiast odczytynieblokujące. Podczas podmiany wartości bieżącej przekazana funkcja przeliczająca może zostać wywołana więcej niż raz, więc nie powinna ona mieć efektów ubocznych.

Z Atomów skorzystamy na przykład w implementacjach liczników czy w odpowiednikach semaforów, które strzegą dostępu do współdzielonej struktury danych (np. bufora pamięci podręcznej). Dają one pewność, że podczas zmiany współdzielonej wartości dojdzie do uwzględnienia poprzedniej, możliwie najbardziej aktualnej, ale nie mamy pewności, czy będzie to wartość ostatnio zlecona w którymś z wątków. Oznacza to, że w przypadku struktur złożonych z wielu elementów nie możemy polegać na kolejności ich modyfikowania.

Zobacz także:

Agenty

Agent to mechanizm referencyjny podobny do Atomów. Również pomaga wyrażać częste, niekoordynowane zmiany współdzielonych stanów ustalonej tożsamości, jednak odbywa się to w sposób asynchroniczny.

Operacja aktualizacji odniesienia nie blokuje wykonywania się programu, lecz natychmiast trafia do specjalnego bufora, w którym oczekuje na realizację w odpowiednim czasie. Odczyty wartości bieżących są również nieblokujące.

Agenty wykorzystywane są tam, gdzie zależy nam na zmianie stanu współdzielonego obiektu, lecz nie zależy nam na tym kiedy do tej zmiany dojdzie, bo ważniejsza jest dalsza, niezakłócona praca bieżącego wątku. Zastosowania Agentów to wymiana danych w programowaniu polegającym na zarządzaniu zdarzeniami (np. w systemach gromadzenia raportów czy sterowania niezależnymi procesami).

Funkcja aktualizująca przekazywana w celu zmiany stanu Agenta nie jest wywoływana w bieżącym wątku, lecz umieszczana w odpowiedniej kolejce, gdzie oczekuje na realizację. Operując na Agentach, nie mamy więc pewności, czy aktualizacja wartości bieżącej nastąpi po aktualizacji dopiero zleconej, a nawet czy w ogóle do niej dojdzie, dopóki tego nie sprawdzimy, dokonując zbadania stanu obiektu.

Zobacz także:

Refy

Bardziej złożone operacje, wymagające koordynowania zmian stanów wybranej liczby ustalonych tożsamości, które są współdzielone między wątkami w sposób synchroniczny, umożliwiają referencje transakcyjne (skr. Refy). Operacje przeprowadzane na nich korzystają z programowej pamięci transakcyjnej (ang. software transactional memory, skr. STM). Dzięki niej można odwoływać się do pamięciowych struktur w podobny sposób, w jaki dochodzi do wykonywania operacji na bazach danych – mamy na przykład do czynienia z systemem transakcji.

Podczas realizowania transakcji bieżący wątek jest blokowany do momentu jej zakończenia. Z kolei odczyty Refów są operacjami nieblokującymi, a zapisy trafiają do odpowiednich kolejek, gdzie po uszeregowaniu mogą wpływać na blokowanie zarówno transakcji bieżącej, jak i innych transakcji.

Modyfikacje różnych odniesień dokonywane w ramach pojedynczej transakcji są atomowe (mogą w całości udać się lub zostać odrzucone), natomiast odczyty referencji w jej obrębie sprawiają, że tworzone są tzw. migawki (ang. snapshots), czyli niezmienne wartości wyrażające stany tożsamości w konkretnych momentach. Referencje transakcyjne znajdują zastosowanie w komunikacji z zewnętrznymi źródłami danych, a także przy zarządzaniu zbiorami informacji, które cechować musi wewnętrzna spójność pod względem relacji między elementami.

Zobacz także:

Future’y

Operacje, które powinny być realizowane równolegle (w osobnym wątku) można zlecać tworząc obiekty typu Future. Ten typ referencyjny pozwala wyrażać niekoordynowaną, pojedynczą zmianę współdzielonego stanu, zaś obliczenia prowadzące do uzyskania wartości prowadzone będą w sposób asynchroniczny.

Próba odczytu wartości bieżącej obiektu referencyjnego spowoduje zablokowanie bieżącego wątku do momentu jej uzyskania (jeżeli wartościowanie jeszcze się nie zakończyło), z kolei zapis jest nieblokujący (odbywa się zawsze w innym wątku).

W praktyce Future’ów użyjemy tam, gdzie chcemy, aby jakieś wyrażenie było zrealizowane w osobnym wątku, aby móc później odczytać wynik. Częstą praktyką jest wykorzystywanie formy specjalnej tworzącej obiekty tego typu po to, aby zrównoleglić jakieś zadanie, nawet bez przejmowania się rezultatem, a ze względu na efekty uboczne (zapis do pliku, kontrola interfejsu użytkownika, wykonanie testu itp.), aby nie blokować bieżącego wątku.

Opcjonalnie możemy sprawdzić, czy rezultat zleconych obliczeń jest już gotowy bez odczytywania wartości bieżącej. Wynik obliczeń jest zapamiętywany, więc każda kolejna dereferencja spowoduje skorzystanie z zapisanej wcześniej wartości.

Zauważmy, że korzystając z Future’ów, nie możemy wybrać, w jakim wątku (z dostępnej puli) zostaną przeprowadzone obliczenia. Wiemy tylko, że będzie to wątek różny od lokalnego.

Zobacz także:

Promise’y

Operacje, które powinny być realizowane równolegle można też zlecać tworząc obiekty typu Promise. Jest to typ referencyjny, dzięki któremu daje się wyrazić niekoordynowaną, pojedynczą zmianę współdzielonego stanu, do której już doszło lub dopiero dojdzie w dowolnym wątku, zaś realizowanie obliczeń prowadzących do uzyskania wartości odbędzie się w sposób asynchroniczny.

Próba odczytu wartości bieżącej spowoduje zablokowanie bieżącego wątku do momentu jej pojawienia się, natomiast zapis jest operacją nieblokującą.

Z Promise’ów skorzystamy, aby obliczyć wartość, która będzie następnie ustawiona jako bieżąca i współdzielona między wszystkimi wątkami, przy czym mogą one na nią oczekiwać. Jest to mechanizm komunikacji między wątkami, w którym jeden wątek spodziewa się rezultatu obliczeń dostarczanego przez inny.

Wątek oczekujący na rezultat może dokonać blokującej dereferencji, natomiast wątek realizujący obliczenia musi użyć funkcji wysyłającej wartość, tzn. dokonującej powiązania z nią obiektu typu Promise. Operacja ta może być przeprowadzona tylko raz, a każda dereferencja spowoduje skorzystanie z wcześniej ustawionej wartości.

Różnica w stosunku do Future’ów polega na tym, że mamy wpływ na to, w którym wątku dokonamy obliczeń, aby pozostałe wątki miały dostęp do ich rezultatów. W praktyce wygląda to tak, że w wybranym wątku operujemy na danych w celu uzyskania wartości, a następnie używamy jej, aby zmienić stan obiektu typu Promise (wytwarzając z nią powiązanie).

Zobacz także:

Delay’e

Operacje, które powinny być odłożone w czasie można zlecać tworząc obiekty typu Delay. Jest to typ referencyjny, dzięki któremu daje się wyrazić niekoordynowaną, pojedynczą zmianę współdzielonego stanu. Aktualizacja referencji odbywa się w sposób asynchroniczny.

Odczyty wartości bieżących obiektów typu Delayblokujące – pierwsza próba odczytu sprawia, że w bieżącym wątku rozpoczyna się wykonywanie odłożonych wcześniej obliczeń. Z kolei zapis jest operacją nieblokującą.

Delay’ów użyjemy tam, gdzie chcemy, aby obliczenie wartości wyrażenia zostało wykonane później i na żądanie, jeżeli w ogóle pojawi się taka potrzeba. Jest to sposób na przeprowadzanie tzw. wykonywania zwłocznego (ang. delayed execution), które pozwala programować z użyciem leniwego wartościowania (ang. lazy evaluation).

Pierwsza dereferencja obiektu typu Delay w dowolnym wątku sprawi, że zostanie on zablokowany do czasu zrealizowania odłożonych wcześniej obliczeń i uzyskania rezultatu. Wynikowa wartość zostanie powiązana z obiektem referencyjnym, a operacja ta będzie wykonana tylko raz i tylko w obrębie jednego wątku w danym kwancie czasu. Wynik obliczeń będzie zapamiętany, a każda kolejna dereferencja (w dowolnym wątku) spowoduje skorzystanie z zapisanej wcześniej wartości.

Jesteśmy w stanie wybrać, w którym wątku wyrażenie zostanie przeliczone, jeżeli tylko możemy odwołać się w nim do współdzielonego obiektu typu Delay po raz pierwszy.

Zobacz także:

Volatile’e

Volatile to mechanizm podobny do Atomów, jednak pozbawiony obsługi funkcji służących do badania poprawności wartości bieżących (walidatorów) i nie wyposażony w możliwość rejestrowania tzw. funkcji obserwujących. Jest to sposób utrzymywania lokalnego stanu zbliżony do zmiennych znanych z innych języków programowania, a wykorzystywany głównie w tzw. transduktorach.

Z uwagi na brak gwarancji, że operacje będą atomowe, nie powinno się używać Volatile’i do zarządzania współdzielonymi stanami, ale do szybkiej wymiany informacji w obrębie pojedynczego, izolowanego wątku z opcjonalnymi odczytami wartości współdzielonych w innych wątkach.

Obiekty typu Volatile zachowują się podobnie do zmiennych lokalnych, z tą różnicą, że izolowanie w lokalnym wątku nie jest wymuszane.

Zobacz także:

Podsumowanie zastosowań

W wyborze właściwego obiektu referencyjnego dla konkretnego zastosowania mogą nam pomóc proste sformułowania:

Zdanie Typ
Chcę w programie nazwać wartość, funkcję lub obiekt referencyjny i odwoływać się do takiej tożsamości z użyciem symbolicznego identyfikatora widocznego w dowolnym miejscu. Var
Chcę odpowiednika zmiennej lokalnej dla pewnego obszaru leksykalnego. Var
Chcę szybkiego licznika lub akumulatora z możliwością odczytywania go w innych wątkach. Volatile
Potrzebuję pewności, że każda wcześniejsza zmiana stanu zostanie uwzględniona podczas aktualizacji. Atom
Potrzebuję zlecić obliczenia i zapomnieć, ewentualnie sprawdzając co jakiś czas, czy już się udało. Agent
Potrzebuję pewności, że wszystkie kolejne zmiany stanów jednego lub kilku obiektów zostaną uwzględnione, a gdy będzie ich kilka, aktualizacja nastąpi w tym samym czasie. Ref
Mam obliczenia, które być może będą zrealizowane, a przy ewentualnym odczycie poczekam na ich zakończenie. Delay
Chcę rozpocząć obliczenia w innym wątku, a w przyszłości mogę poczekać na ich zakończenie, jeżeli jeszcze będą trwały. Future
Chcę odpowiednika szyny danych, żeby jeden wątek mógł coś na niej umieścić, a inne mogły to odczytać, oczekując aż się pojawi. Promise
Jesteś w sekcji .
Tematyka:

Taksonomie: