Semafory, sekcje krytyczne czy synchronizacja, to naprawdę wszystko, na co Cię stać w XXI wieku?

Chyba nie ma straszliwszego olśnienia nad to, w którym odkrywasz, że wcale nie trzeba było się tak męczyć.

- Luźny Interpretator -

Na początku miałem dla Ciebie gotowe opowiadanie o świecie spustoszonym na skutek komputerowego wirusa, który powodował blokowanie semaforów w sposób losowy.

Wirus rozprzestrzeniał się błyskawicznie i aktywował bardzo powoli, tak, że wszystko wyglądało na typowe błędy programistyczne. Gdy wirus dotarł już wszędzie, przestał bawić się w subtelności i wywrócił nasz świat do góry nogami.

Myślę, że potrafisz to sobie wyobrazić. Wszystko siadło w jednej chwili. Spadające samoloty, wyłączające się elektrownie, niedziałająca komunikacja. Dzień zagłady. 

Wszystko z powodu nieszczęsnych semaforów.

Uznałem, jednak, że nie masz ochoty czytać beletrystyki, tylko chcesz się dowiedzieć czegoś konkretnego i namacalnego.

Zacząłem więc od podstaw opisywać zasady tworzenia oprogramowania współbieżnego.  Tylko po to, by kilka godzin później też to wszystko wykasować. 

Przecież Ty to wszystko świetnie wiesz, a nawet jeśli nie, znajdziesz mnóstwo innych miejsc, gdzie możesz o tym poczytać.

Postanowiłem się zatem skoncentrować na najważniejszym elemencie całej układanki. Na programowaniu asynchronicznym. Potężnym narzędziu w rękach nowoczesnego programisty.

Tak czy inaczej, czeka nas odrobina podstaw 🙂

More...

Multitasking

Coś, z czym ludzie sobie nie radzą, za to komputery zostały do tego stworzone. Jest tylko jeden problem. My programiści, czasami wybieramy kiepskie narzędzia do wykonania naszej pracy.

Jak powinien działać idealny multitasking? Odpowiedź wydaje się prosta. Wiele rzeczy jest wykonywanych równolegle w tym samym czasie bez żadnych przestojów. Zgadza się?

Jak to jest w rzeczywistości? Świetnie wiesz. Konieczność zsynchronizowania komunikacji międzyprocesowej wprowadza blokady. 

Próba dostania się do tych samych danych przez kilka wątków? Semafor i blokada.. 1 wątek pracuje, reszta czeka.

Odczyt danych z pliku… Uff. ciężki temat… Czekamy…

Odczyt danych z socketu… Czekamy…

O… Deadlock…


Nie chcę, Cię martwić, ale chłopaki kilka lat temu stawiali wytrzymalsze budowle niż nasze dzisiejsze wirtualne pałace.

Jest jeszcze druga strona tego problemu. Częste blokowanie pamięci jest kosztowne. Miałem już kiedyś sytuację, gdy aplikacja dosyć regularnie powodowała maksymalne obciążenie wielordzeniowego serwera, z powodu bardzo wielu operacji na semaforach. Pomogła dopiero zmiana managera pamięci.

Odkąd jednak zacząłem pracować w Scali i porzuciłem ten przestarzały model pracy, życie stało się prostsze, a kod krótszy i szybszy.

Musiałem jednak zmienić sposób myślenia. Tworzenie kodu linearnego, a asynchronicznego to dwie zupełnie różne sprawy. Nawet teraz po tylu latach, czasami łapię się na tym, że myślę liniowo. Nic w tym dziwnego, po pierwsze, nie jesteśmy nawykli do rozwiązywania zadań w sposób asynchroniczny, a po drugie ciągle duża część mojej pracy opiera się niestety o języki, w których semafor króluje.

Czym jest Akka

Strona Akki funkcjonuje obecnie pod adresem https://akka.io, jest to biblioteka tworzona przez Lightbend. W zasadzie ciężko już mówić, że to jedna biblioteka. Zresztą twórcy sami używają określenia "zestaw narzędzi".

"Akka to zestaw narzędzi do tworzenia wysoce współbieżnych, rozproszonych i odpornych aplikacji opartych na komunikatach dla Java i Scali"

W największym uproszczeniu jest to implementacja modelu Aktorów w JVM.

Co da Ci zastosowanie tego modelu w Twojej aplikacji? Dlaczego warto?


1

Event-driven model

Aktory wykonują całą swoją pracę wyłącznie w oparciu o całkowicie asynchroniczne wiadomości, dzięki temu masz zapewnioną całkowicie nieblokującą komunikację pomiędzy aktorami.

2

Izolacja

Aktory nie posiadają swojego publicznego API, zamiast tego każdy aktor definiuje własny zestaw komunikatów, jaki obsługuje, gwarantuje to całkowitą niezależność pomiędzy nimi. Nie dzielą ze sobą żadnych zasobów. Jedyny sposób na to, by dowiedzieć się coś o innym aktorze, to wysłać mu wiadomość z prośbą o określone informacje.

3

Dowolność lokalizacji

Gdy tworzysz aktora, dostajesz tylko referencję do jego instancji. Nie ma dla Ciebie znaczenia, gdzie on się konkretnie znajduje (co jak się dowiesz później może być skomplikowane, w modelu clustrowym, kiedy pojedynczy aktor może być utworzony na dowolnej z fizycznych maszyn). Każda instancja może być indywidualnie zatrzymana, zrestartowana, uruchomiona ponownie, czy odtworzona po padzie.


4

Lekkość

Każdy aktor to raptem kilkaset bajtów, co pozwala zarządzać nawet milionami aktorów na pojedynczej maszynie.


To tylko absolutnie podstawowe cechy aktorów. Akka ma w sobie jeszcze setki, jeśli nie tysiące dodatkowych niespodzianek, wszystko po to, abyś mógł wydajniej i przyjemniej tworzyć skomplikowany kod.


Po tym dosyć przydługim wstępie, czas na trochę praktyki.


Miałem w tym celu użyć gotowego przykładu serwowanego na stronie Akki, ale on jest nudny 😉


Co powiesz na to, żebyśmy odpalili sobie od razu 2 miliony aktorów? Brzmi jak zabawa? No to mamy plan 🙂


Zaczynamy zabawę


Na początek wyjaśnienie. To nie jest kurs Akki. Nie będę Ci wszystkiego tłumaczył (przynajmniej nie tym razem), to tylko prosty przykład pokazujący jak to wszystko w wielkim skrócie działa.


Najpierw utworzymy sobie aktora raportującego. Jego jedynym zadaniem jest wrzucanie tekstu do loga.


Zaczynamy od  companion object.

object Reporter {

  def props(): Props = Props(new Reporter)

  final case class BeginWorkNR()
  final case class ReportPartNR(done: Int, from: Int)
}

Nasz Reporter będzie obsługiwał dwa sygnały. 

Pozwól, że zaraz na początku wyjaśnię skąd ten NR w nazwie sygnałów. 

W uproszczeniu przyjmijmy, że można wysłać dwa rodzaje sygnałów, pierwszy z nich jest rodzaju "wyślij i zapomnij", a drugi "wyślij i czekaj na odpowiedź". To dosyć istotne, żeby pamiętać, który jest który 😉

Właśnie dlatego przyjąłem taki standard nazewnictwa dla sygnałów, które nie zwracają odpowiedzi NoReply.

Metoda props to po prostu fabryka dla naszego Aktora.

Lecimy dalej.

class Reporter extends Actor with ActorLogging {
  import Reporter._

  def receive = {
    case BeginWorkNR() =>
      log.info("FatBoss zaczyna służbę w aktorze: " + sender())

    case ReportPartNR(done, from) =>
      // nie chcemy raportować za często
      if (done % 1000 == 0)
        log.info(s"FatBoss raportuje wybudowanie $done warsztatów z planowanych $from.")
  }
}

Metoda receive to serce każdego aktora, miejsce, w którym następuje przetwarzanie otrzymanych przez aktora sygnałów. 

Ważne

1.

Każdy aktor przetwarza tylko JEDEN sygnał w danym momencie, reszta czeka w kolejce

2.

Jeśli z jednego aktora wyślesz kilka sygnałów do innego aktora, zostaną one przetworzone w kolejności wysłania, ale nie ma żadnej gwarancji, że w międzyczasie nie zostaną przetworzone sygnały z innych źródeł

To lecimy dalej z kodem

object FatBoss {

  // a co odpalimy sobie 2 mln aktorów, ktoś nam zabroni?
  val SOFT_LIMIT = 2000000
  var workersLimit: Int = SOFT_LIMIT
  var jobDone: Int = 0

  def props(system: ActorSystem, reporter: ActorRef): Props = Props(new FatBoss(system, reporter))

  // NR od NoReply, czyli po wykonaniu zadania nie jest oczekiwana odpowiedź
  final case class AskForWorkersNR(count: Int)
  final case class WorkCompletedNR(count: Int)
  final case class MakeFirstWorkshopNR()
}

class FatBoss(system: ActorSystem, reporter: ActorRef) extends Actor {

  import FatBoss._
  import Workshop._

  def updateWorkToDo(count: Int): Int = {
    // aktualizujemy pozostałą ilość pracowników
    workersLimit -= count
    count
  }

  def calculateWorkers(count: Int): Int =
    // wyznaczam ilość robotników do wysłania
    // wybieramy namniejszą wartość z 3 podanych
    // czyli albo ilość robotników o których proszono,
    // albo 100 bo Boss to świnia i nie daje więcej niż 100 pracowników
    // ale jak chce mniej to się nie wychyla i daje mniej
    // na końcu kontrolnie jeszcze lista pozostałych pracowników
    updateWorkToDo(List(count, 100, workersLimit).min)

  def receive = {
    case AskForWorkersNR(count: Int) =>
      calculateWorkers(count) match {
        case c: Int if c > 0 =>
          sender() ! StartWorkNR(c)

        case _ => ()
      }

    case WorkCompletedNR(count: Int) =>
      jobDone += count
      reporter ! Reporter.ReportPartNR(jobDone, SOFT_LIMIT)

    case MakeFirstWorkshopNR() =>
      system.actorOf(Workshop.props(system, self))
      reporter ! Reporter.BeginWorkNR()
  }
}

No to znasz już Grubego Szefa 🙂 Świnia z niego straszna. Nie ważne o ilu pracowników go poprosisz, nigdy nie da Ci więcej niż 100. Jeśli tego nie wiesz i poprosisz o 80, to Grubas da Ci tylko 80, bo taka już jego natura.

Zajmuje się on też przy okazji, nadzorem nad kadrami. W sumie ma 2 miliony techników (bo to w Chinach się dzieje), ale z każdym kolejnym warsztatem ludzi ubywa aż do zera.

To czas na warsztat.

object Workshop {
  val rng = scala.util.Random

  def props(system: ActorSystem, fatBoss: ActorRef): Props = Props(new Workshop(system, fatBoss))

  final case class StartWorkNR(count: Int)
}

class Workshop(system: ActorSystem, fatBoss: ActorRef) extends Actor {

  fatBoss ! FatBoss.AskForWorkersNR(Workshop.rng.nextInt(200))

  def receive = {
    case Workshop.StartWorkNR(count: Int) =>
      // każdy uzyskany robotnik ma za zadanie wybudować
      // nowy warsztat
      for (i <- 1 to count) system.actorOf(Workshop.props(system, fatBoss))
      fatBoss ! FatBoss.WorkCompletedNR(count)
  }
}

Zwróć uwagę, że w konstruktorze klasy Workshop jest wysyłana prośba do FatBossa o przydział robotników.

Na koniec warto to wszystko wprawić w ruch.

object AkkaFatBoss extends App {
  val system: ActorSystem = ActorSystem("fatBossAkka")

  val reporter = system.actorOf(Reporter.props(), "reporter-actor")
  val fatBoss = system.actorOf(FatBoss.props(system, reporter), "fatboss-actor")
  fatBoss ! FatBoss.MakeFirstWorkshopNR()
}

To jak to wszystko działa

Zasada jest prosta. Przygotowałem Ci nawet mały diagram. Proszę się nie śmiać. Moje zdolności plastyczne pozostały na poziomie 3 klasy szkoły podstawowej 🙂

  1. 1
    Startujemy FatBossa przy pomocy sygnału MakeFirstWorkshopNR
  2. 2
    Zatem Boss zleca wybudowanie pierwszego warsztatu.
  3. 3
    Po jego utworzeniu z warsztatu jest do Szefa wysyłany sygnał AskForWorkersNR z prośbą o pracowników.
  4. 4
    Grubas sprawdza, czy ma moce przerobowe i jeśli tak, wyznacza, ile osób chce wysłać.
  5. 5
    Gdy warsztat otrzymuje pracowników przy pomocy sygnału StartWorkNR, zabiera się do pracy i każdy robotnik tworzy swój własny warsztat.
  6. 6
     Na zakończenie cyklu - z warsztatu jest do Bossa wysyłany sygnał WorkCompletedNR.
  7. 7
     Informację tę FatBoss z zadowoleniem raportuje do Reportera przy pomocy ReportPartNR 

 Punkty od 3 do 7 są powtarzane do momentu zużycia wszystkich dostępnych pracowników.

W rezultacie uruchomiłeś właśnie 2 000 000 aktorów. Gratuluję.

Nie utworzyłeś przy tym ani jednego semafora, sekcji specjalnej czy innego "standardowego" narzędzia do pracy wielowątkowej. 

Jakie to uczucie? 🙂

Ahh, byłbym zapomniał. 

Na sam koniec, mam dla Ciebie mały bonusik. Głos wyszedł trochę cicho, mam nadzieje, że mi wybaczysz (nie opanowałem jeszcze do końca nowego Tascama).

Istnieje wysokie prawdopodobieństwo, że masz ochotę pobawić się tym kawałkiem kodu, ale nie do końca wiesz jak się za to zabrać.

Dlatego przygotowałem dla Ciebie krótki film instruktażowy, pokazujący jak uruchomić ten przykład z poziomu IntelliJ IDEA (wystarczy Ci do tego darmowa wersja Community z zainstalowanym pluginem Scala).

Kod można sklonować z GitHuba ->

https://github.com/emssik/FatBossAkkaExample.git

To wszystko na dzisiaj


Dziękuję Ci pięknie za poświęcony czas i zapraszam do dzielenia się swoimi uwagami.

Do zobaczenia wkrótce.

Daj znać o czym chcesz przeczytać 🙂

Chcesz Nauczyć Się Scali?

Jestem tutaj, by Ci pomóc.