Programowanie współbieżne
Stos i sterta
W Javie wszystkie obiekty i tablice mieszczą się w obszarze pamięci nazywanym stertą (ang. heap) – jest jedna na proces, wspólna dla wszystkich jego wątków. Nie ma specjalnej struktury, jest nieuporządkowana (z punktu widzenia programu), stąd nazwa. Każdy wątek ponadto ma w pamięci swój stos (stack), gdzie mieszczą się informacje o obecnie trwających wywołaniach funkcji, w tym argumenty funkcji i zmienne lokalne.
Wykonanie programu polega na wywołaniu (w wątku głównym) funkcji main; podczas tego wywołania możemy przejść do wywołania funkcji wewnątrz main, a w niej do kolejnej funkcji, i tak dalej.
W danym momencie stos musi więc pamiętać sekwencję obecnie trwających, zagnieżdżonych wywołań funkcji – czyli wszystkie wywołania które zaczęliśmy, a z których jeszcze nie wyszliśmy.
Stos składa się z sekwencji ramek stosu (stack frame) odpowiadających kolejnym wywołaniom: od main, aż po obecnie wykonywaną funkcję.
Każdy inny wątek będzie miał własny stos, tylko zaczynający się od innej funkcji niż main.
Na ramkę stosu składa się odnośnik do wykonywanej funkcji, oraz argumenty wywołania i zmienne lokalne.
Ale, co ważne, prymitywne zmienne (int/float/boolean/char/short/long/double/byte) są zapisane w ramce stosu bezpośrednio, przez wartość, natomiast dla obiektów i tablic zapisujemy w ramce stosu tylko referencję (wskaźnik 32 lub 64-bitowy) do pozycji obiektu/tablicy na wspólnej stercie.
Wywołanie metody obiektu foo działa tak jak wywołanie funkcji, której niejawnie przekazujemy dodatkowo argument this – referencję na obiekt foo.
Przykład
Spójrzmy na TwoWritersSynchronized. Dodałem opcjonalne this. przed first, last i id dla czytelności.
public class TwoWritersSynchronized {
private static volatile boolean lettersDone = false;
private static volatile boolean digitsDone = false;
private static volatile int currentId;
private static class Writer implements Runnable {
private static final int LINES_COUNT = 100;
private static final int LINE_LENGTH = 50;
private final char first;
private final char last;
private final int id;
public Writer(char first, char last, int id) {
this.first = first;
this.last = last;
this.id = id;
}
@Override
public void run() {
char c = this.first;
for (int i = 0; i < LINES_COUNT; ++i) {
while (currentId != this.id) {}
for (int j = 0; j < LINE_LENGTH; ++j) {
System.out.print(c);
++c;
if (c > this.last) {
c = this.first;
}
}
System.out.println();
currentId = 1 - currentId;
}
if (id == 0) {
lettersDone = true;
} else {
digitsDone = true;
}
}
}
public static void main(String[] args) {
Thread letters = new Thread(new Writer('a', 'z', 0));
Thread digits = new Thread(new Writer('0', '9', 1));
currentId = 0;
System.out.println("Początek");
letters.start();
digits.start();
while (!lettersDone) {}
while (!digitsDone) {}
System.out.println("Koniec");
}
}
W połowie wykonania programu mamy na stercie (w przypadkowej, nieistotnej kolejności):
- obiekt
Writer('a', 'z', 0)(w nim zmiennefirst,last,id) i obiektThreadwskazujący na niego. - obiekt
Writer('0', '9', 1)(w nim zmiennefirst,last,id) i obiektThreadwskazujący na niego. - zmienne
lettersDone,digitsDone,currentId(bo są oznaczonestatic)
Do tego mamy trzy stosy:
- stos głównego wątku, z ramką
main, a w niej zmiennelettersidigitsbędące referencjami do powyższych obiektów Thread (i nic więcej, wywołaniestart()było króciutkie i już z niego wyszliśmy). - stos wątku
letters, z ramkąrun, a w niej zmiennathiswskazująca na obiekt0. Writerna stercie, oraz zmienne lokalnei,j,c. - stos wątku
digits, z ramkąrun, a w niej zmiennathiswskazująca na obiekt1. Writerna stercie, oraz zmienne lokalnei,j,c.
Opis tych stosów, tzw. stacktrace/backtrace, można zobaczyć wstrzymując wątek wyjątkiem lub w debuggerze. Jeśli trafimy odpowiedni moment, na stosach wątków możemy zobaczyć jeszcze ramki print/println (i kolejne ramki wykonywanych wewnątrz print/println funkcji).
Współbieżność
Zmiennych na stosie nie da się sięgnąć z innego wątku, więc ich zachowanie jest proste i bezpieczne.
Na zmienne na stercie już musimy uważać, natomiast dla wielu nadal możemy łatwo zobaczyć, że sięga do nich tylko jeden wątek.
Przykładem są zmienne w obiekcie Writer danego wątku – w odwołaniach do this.first, this.last, this.id nie musimy na nic uważać.
No chyba że jakoś przekażemy innym wątkom this z naszego wątku lub sięgniemy do letters/digits z głównego wątku (moglibyśmy np. zapisać sobie w main referencję do Writer przed przekazaniem go do konstruktora Thread).
Zmienna lettersDone (i podobnie digitsDone) może być zapisana tylko przez jeden wątek, co mocno ułatwia analizę; natomiast jest czytana przez inny wątek w celu synchronizacji, więc volatile jest konieczne.
Wreszcie zmienna currentId jest zapisywana i czytana przez dwa wątki, więc volatile jest konieczne i analiza tego co się z nią dzieje już wymaga pomyślenia o jakichś niezmiennikach.
W językach C, C++, Rust
Krótko mówiąc jest tak samo, ale rozróżniamy w kodzie obiekt od wskaźnika na obiekt; zmienne lokalne zawsze są na stosie – sami decydujemy czy trzymamy obiekt bezpośrednio na stosie, czy trzymamy na stosie tylko wskaźnik do obiektu utworzonego na stercie.
(Uwaga: volatile z C/C++ daje zupełnie inne gwarancje niż volatile z Javy. Bliższy jest typ std:atomic<int> z C/C++, który dodatkowo gwarantuje atomowość (niepodzielność) między innymi inkrementacji ++. Przypomnijmy: volatile z Javy atomowość gwarantuje tylko dla pojedynczego odczytu lub dla pojedynczego zapisu.)