Save 37% off PRO during our Black Friday Sale! »

Was steckt hinter "Concurrency without Fear?"

Was steckt hinter "Concurrency without Fear?"

1c88d7906e3ffa450aedff2f5f1d1299?s=128

Florian Gilcher

January 16, 2018
Tweet

Transcript

  1. None
  2. $ cat .profile GIT_AUTHOR_NAME=Florian Gilcher GIT_AUTHOR_EMAIL=florian@rustfest.eu TWITTER_HANDLE=argorak GITHUB_HANDLE=skade

  3. • Backend-Entwickler

  4. • Community-Mensch • Rust UG Berlin • Vorstand Ruby Berlin

    e.V.
  5. • Mitglied des globalen Rust commu- nity teams • Organisator

    eurucamp/jruby- conf.eu/RustFest/isleofruby
  6. Rust

  7. • Erfunden von Graydon Hoare • Basiert auf linearen Typen

    • Regionen-basiertes Speicherman- agement
  8. • Entwickelt von Mozilla und der Community • Finanziert von

    Mozilla • Jetzt schon Firefox! (Firefox 56) • Insbesondere für parsing (URL und MP4-Metadata)
  9. Ziele von Rust • Speichersicherheit ohne Garbage Collection • Unterstützung

    bei Nebenläufigkeit • (Erwartbare) Geschwindigkeit
  10. Sicher • statisches Typsystem mit Typin- ferenz • Keine Null-Pointer

    • Kein Pointer-aliasing
  11. Nebenläufigkeit • Mutabilität als Konzept erster Klasse • Keine konkurrierenden

    Zugriffe auf mutable Daten • Wichtige Basistypen mitgeliefert
  12. Geschwindigkeit • Keine versteckten Allokationen • Erwartbare Deallokationen • Abstraktionen

    ohne Laufzeitkosten
  13. Das Ziel ist C • Kontrollierbares Memory-Layout • Kann C-ABI-kompatible

    Biblio- theken erstellen • Keine Laufzeit • optionale unsichere Subsprache (z.B. für FFI)
  14. "Concurrency without Fear"?

  15. Fear

  16. Nebenläufige und parallele Programme gelten als notorisch schwer zu schreiben,

    fehleranfällig und schwer zu debuggen.
  17. Bugs • Non-deterministisch • Wegen der kurzen Zeitspannen sel- ten

    • Nicht lokal
  18. Sequenziell 1 + 1 + 1 = 3

  19. Parallel 1 + 1 + 1 = 3, ausser in

    manchen seltenen Fällen
  20. Problemquellen

  21. Data races • Mehrere Teile des Programms ändern ohne Synchronisation

    Daten, auf die sie alle Zugriff haben
  22. Resource races • Mehrere Teile eines Programms entziehen sich gegenseitig

    Zugriff auf Resourcen, die sie aber alle benötigen
  23. Wir werden uns hier mehr mit data races beschäftigen.

  24. • Mehrere Teile des Programms ändern ohne Synchronisation Daten, auf

    die sie alle Zugriff haben
  25. Probleme • Teilen von Daten (Sharing) • Verändern von Daten

    (Mutation)
  26. Credo Mutables teilen von Daten ist böse.

  27. Achtung Nebenläufigkeit ist nicht nötig, um hiermit Bugs zu produzieren!

  28. fn main() { let mut v = vec![1; 10]; let

    element = &v[9] as *const i32; v.push(1); println!("{}", unsafe { *element }); }
  29. None
  30. None
  31. None
  32. None
  33. fn main() { let mut v = vec![1; 10]; let

    element = &v[3]; v.push(1); println!("{}", element); }
  34. error[E0502]: cannot borrow `v` as mutable because it is also

    bo –> vec_crash.rs:4:5 | 3 | let element = &v[3]; | - immutable borrow occurs here 4 | v.pop(); | ^ mutable borrow occurs here 5 | println!("{}", element ); 6 | } | - immutable borrow ends here error: aborting due to previous error
  35. Praktische Lösungen

  36. Keine Mutabilität (z.B. Haskell) "Mutability is the root of all

    evil"
  37. In funktionalen Programmiersprachen sind Daten generell nicht mutabel.

  38. Beispiel Das hinzufügen eines Elements zu einer Liste gibt stets

    eine neue Liste zurück.
  39. main = do let list = [1,2,3] print list let

    list2 = list ++ [4] print list print list2
  40. Solche Ansätze verlassen sich auf Optimierungen, zum Beispiel structural sharing.

  41. Share-nothing Keine 2 konkurrierenden Programmeinheiten dürfen gleichzeitig Zugriff auf Daten

    haben. Teilen funktioniert über Kopieren.
  42. Beispiel: Erlang Erlang erlaubt weder die Mutation von Daten, noch

    deren Sharing. Werden Daten zu anderen Prozessteilen geschickt, werden sie immer kopiert.
  43. Beide Strategien können ohne Unterstützung der verwendeten Programmiersprache verwendet werden

    (und sind empfohlen), aber natürlich mit Sprachunterstützung viel leichter umzusetzen.
  44. Sie sind auch zu empfehlen, wenn garnicht nebenläufig gearbeitet wird!

  45. Schwierigkeiten Schwierigkeiten schaffen Daten, an denen ein Zustand hängt.

  46. Filepointer Filepointer sind Daten mit einem impliziten Zustand. (offen, geschlossen)

  47. import System.IO main = do inFile <- openFile "foo" ReadMode

    contents <- hGetLine inFile putStrLn contents hClose inFile contents <- hGetLine inFile putStrLn contents hClose inFile
  48. test read_file: foo: hGetLine: illegal operation (handle is closed)

  49. -module(read_file). -export([read_text_file/1]). read_text_file(Filename) -> {ok, IoDevice} = file:open(Filename, [read]), read_text(IoDevice),

    file:close(IoDevice), read_text(IoDevice). read_text(IoDevice) -> case file:read_line(IoDevice) of {ok, Line} -> io:format("~s", [Line]), ok; eof -> ok end.
  50. 1> c(read_file). {ok,read_file} 2> read_file:read_text_file("foo"). test ** exception error: no

    case clause matching {error,terminated} in function read_file:read_text/1 (read_file.erl, line 11)
  51. Können wir auch solche Patterns unterbinden?

  52. Ownership

  53. • Alle Daten haben genau einen Be- sitzer • Besitz

    kann abgegeben werden • Wenn Daten das Ende eines Scopes erreichen, werden sie zerstört
  54. use std::fs::File; use std::io::Write; fn main() { let file =

    File::open("test") .expect("Unable to open file, bailing!"); take_and_write_to_file(file); // take_and_write_to_file(file); // ^^ Illegal } fn take_and_write_to_file(mut file: File) { writeln!(file, "{}", "Hello Konstanz!"); }
  55. • Zugriff kann verliehen werden (mu- tabel und immutabel) •

    Einmalig mutabel • Oder mehrfach immutabel • Exklusiv: mutable or immutabel, niemals beides
  56. use std::fs::File; use std::io::Write; fn main() { let mut file

    = File::open("test") .expect("Unable to open file, bailing!"); write_to_file(&mut file); write_to_file(&mut file); } fn write_to_file(file: &mut File) { writeln!(file, "{}", "Hello Konstanz!"); }
  57. • Ownership erlaubt uns share- nothing • Borrowing erlaubt uns

    sharing, aber ohne Mutation • Mutables borrowing garantiert exk- lusive Mutation
  58. Wir können also jederzeit von einem der Systeme ins andere

    wechseln, solange wir nur nach einem arbeiten.
  59. Zwischenfazit Rust minimiert gefährliche und versteckt problematische Patterns in sequentiellem

    Code.
  60. Wollten wir nicht über Concurrency reden?

  61. Option 1: Threads • Berechnungen werden nebenläufig ausgeführt • Ein

    Scheduler verwaltet die Ausführung • Haben einen eigenen Stack • Teilen den Heap • Können einzeln blockieren
  62. Vorteile • Folgen im Einzelnen einem (relativ) linearen Modell •

    Auf (fast) allen Betriebssystem nativ unterstützt • Threads haben einen eigenen Spe- icherbereich (thread-local storage) • Stacks können zum debuggen be- nutzt werden
  63. Nachteile • Jeder Thread braucht Platz für einen Stack •

    Es können recht einfach Synchro- nisierungsbugs auftreten
  64. Populäre Threaded-Systeme • Apache • Varnish • Apache Tomcat

  65. • Jede Aktion ist ein "Event" • Das System hält

    eine Liste zu bear- beitender Events • Eine große Schleife arbeitet diese Events nach und nach ab • Events können weitere Events auslösen • Aktionen haben keine Verbindung über den Stack
  66. Vorteile • Eine Aktion zu einer Zeit: weniger Synchronisierungsprobleme •

    Blockaden werden durch Einreihen ans Ende der Queue abgebildet • Blockaden sind sehr günstig
  67. Nachteile • Aktionen, die den ausführenden Thread doch blockieren, blockieren

    alles • Schwer zu folgen, da Reihenfolge selbst herzustellen ist
  68. Populäre Evented-Systeme • Java NIO ("new I/O") • V8/node.js •

    nginx • BEAM (Erlang-VM)
  69. Option 3: Beides Viel Spass!

  70. Populäre Systeme mit beidem • Datenbanken, z.B. Elasticsearch • Spiele

    • Fast jede moderne Desktop-App
  71. Verschärfte Probleme Die oben beschriebenen Probleme werden schwieriger, wenn die

    Abfolge von Aktionen nicht mehr garantiert ist.
  72. Dateien: Operationen • Öffnen von Dateien • Schließen von Dateien

    • Lesen von Dateien
  73. Aufgabe Öffne eine Datei, lese 2 Zeilen, schließe die Datei

    wieder.
  74. Abhängigkeiten Wir können nur aus offenen Dateien lesen.

  75. Schließen von Dateien unterbindet weiteres Lesen.

  76. Unabhängig Welcher Programmteil zuerst welche Zeile liest, ist egal.

  77. Send und Sync

  78. Send Ein Wert kann zwischen Threads abgegeben werden. Der sendende

    Thread verliert den Zugriff. Der Zugriff bleibt exklusiv.
  79. Der Compiler stellt "Send" automatisch fest, wenn alle inneren Werte

    Send sind.
  80. Insbesondere nicht Send sind alle Werte, die geteilte, mutable Daten

    enthalten.
  81. Sync Ein Wert kann zwischen Threads geteilt werden. Der sendende

    Thread behält den Zugriff. Der Zugriff wird geteilt.
  82. Der Compiler stellt "Sync" automatisch fest, wenn alle inneren Werte

    Sync sind.
  83. Insbesondere nicht Send sind alle Werte, die geteilte, mutable Daten

    enthalten.
  84. Send und Sync sind elegant Sie sagen nichts spezifisches über

    die verwendete Technologie aus. Sie erlauben aber spezifisches Arbeiten mit einer Technologie.
  85. Typ-Beispiele

  86. std::rc::Rc Ein Pointer, der mehrfaches, zur Laufzeit geprüften, immutablen Zugriff

    auf Daten erlaubt.
  87. std::rc::Rc Ist nicht Send und nicht Sync

  88. std::sync::Arc Ein Pointer, der mehrfachen, zur Laufzeit geprüften, immutablen Zugriff

    auf Daten erlaubt. Synchronisiert seinen internen Zähler.
  89. std::sync::Arc Ist Send und Sync.

  90. std::sync::Mutex Ein Container, der mutablen Zugriff auf die internen Daten

    erlaubt und zur Laufzeit prüft, dass er exklusiv ist.
  91. std::sync::Mutex Ist Send und Sync.

  92. Send und Sync abstrahieren über Daten. Was fehlt?

  93. Semantisches Problem Rust ist synchron und imperativ: Aktionen finden nacheinander

    statt und können nicht pausiert werden.
  94. Nebenläufige Programme sind: • Unvorhersagbar • Reaktiv • Zeitorientiert

  95. Result enum Result<T, E> { Ok(T), Err(E) }

  96. Futures trait Future { type Item; type Error; fn poll(&mut

    self) -> Result<Async<T>, E>; fn wait(self) -> Result<T, E>; }
  97. Async • Ok(Async::Ready(t)) -> Die Future ist fertig • Ok(Async::NotReady)

    -> Die Future arbeitet noch • Err(e) -> Ein Fehler ist aufgetreten
  98. Futures Futures abstrahieren eine Berechnung, die in der Zukunft beendet

    sein könnte und entweder erfolgreich oder zu einem Fehler führen wird.
  99. Sozusagen: ein Result in der Zukunft.

  100. Futures gibt es auch unter dem Namen "Promises".

  101. Lingo • Eine Future ist "aufgelöst", wenn das Ergebnis da

    ist. • "deferred computation" • Futures am Ende einer Berechnung heissen "Blätter"
  102. fn main() { let timer = Timer::default(); let sleep =

    timer.sleep(Duration::from_millis(1000)) .inspect("sleep"); let cpu_pool = CpuPool::new(4); let task = cpu_pool.spawn(sleep); println!("{:?}", task.wait()); }
  103. Future sleep polled: Ok(NotReady) Future sleep polled: Ok(Ready(())) Ok(())

  104. Exekutoren Exekutoren sind die Implementierung der Ausführungsstrategie von Futures.

  105. futures-cpupool Mehrere Threads führen Futures parallel aus. Die Ausführung findet

    stets parallel statt.
  106. tokio-core tokio-core hält eine Liste ausführbarer Futures und arbeitet diese

    eine nach dem anderen ab. Futures, die gerade nicht ausführbar sind, werden zurück in die Liste gebracht.
  107. use tokio_core::reactor::Core; fn main() { let timer = Timer::default(); let

    sleep = timer.sleep(Duration::from_millis(1000)) .inspect("sleep"); let mut core = Core::new().unwrap(); let task = core.run(sleep); println!("{:?}", task); }
  108. Future sleep polled: Ok(NotReady) Future sleep polled: Ok(Ready(())) Ok(())

  109. Protokoll • Futures werden beim ersten poll() gestartet • Sie

    können beliebig oft "NotReady" antworten • Ein weiterer Aufruf von poll() nach "Ready" ist nicht erlaubt
  110. Kombination von Futures fn main() { let timer = Timer::default();

    let sleep = timer.sleep(Duration::from_millis(1000)) .inspect("sleep") .and_then(|_| { timer.sleep(Duration::from_millis(500)) .inspect("sleep some more") }); let mut core = Core::new().unwrap(); let task = core.run(sleep); println!("{:?}", task); }
  111. Future sleep polled: Ok(NotReady) Future sleep polled: Ok(Ready(())) Future sleep

    some more polled: Ok(NotReady) Future sleep some more polled: Ok(Ready(())) Ok(())
  112. Futures beschreiben Abhängigkeiten zwischen zukünftigen Berechnungen.

  113. Result "Wenn jetzt X eintrat, dann..."

  114. Future "Wenn in Zukunft X eintritt, dann..."

  115. Kombinatoren: join_all join_all fügt mehrere Futures zu einer neuen Future

    zusammen, die aufgelöst wird, wenn alle Blätter aufgelöst werden.
  116. fn main() { let timer = Timer::default(); let sleep =

    timer.sleep(Duration::from_millis(1500)) .inspect("sleep"); let sleep_shorter = timer.sleep(Duration::from_millis(500)) .inspect("short sleep"); let join = join_all(vec![sleep, sleep_shorter]) .inspect("join"); let mut core = Core::new().unwrap(); let result = core.run(join); println!("{:?}", result); }
  117. Future sleep polled: Ok(NotReady) Future short sleep polled: Ok(NotReady) Future

    join polled: Ok(NotReady) Future sleep polled: Ok(NotReady) Future short sleep polled: Ok(Ready(())) Future join polled: Ok(NotReady) Future sleep polled: Ok(Ready(())) Future join polled: Ok(Ready([(), ()])) Ok([(), ()])
  118. Kombinatoren: select_all select_all fügt mehrere Futures zu einer neuen Future

    zusammen, die aufgelöst wird, wenn das erste Kind aufgelöst werden.
  119. fn main() { let timer = Timer::default(); let sleep =

    timer.sleep(Duration::from_millis(1000)) .inspect("sleep"); let short_sleep = timer.sleep(Duration::from_millis(500)) .inspect("short sleep");
  120. let select = select_all(vec![sleep, short_sleep]) .inspect("first select"); let mut core

    = Core::new().unwrap(); let (result, index, remaining_futures) = core.run(select).un println!("Future with index {} returned {:?}", index, result); let select = select_all(remaining_futures) .inspect("second select"); let (result, index, _) = core.run(select).unwrap(); println!("Future with index {} returned {:?}", index, result); }
  121. Future sleep polled: Ok(NotReady) Future short sleep polled: Ok(NotReady) Future

    first select polled: Ok(NotReady) Future sleep polled: Ok(NotReady) Future short sleep polled: Ok(Ready(())) Future first select polled: Ok(Ready(((), 1, [ InspectFuture { future: Sleep { timer: Timer, when: Instant Future with index 1 returned () Future sleep polled: Ok(NotReady) Future second select polled: Ok(NotReady) Future sleep polled: Ok(Ready(())) Future second select polled: Ok(Ready(((), 0, []))) Future with index 0 returned ()
  122. Eine Umformung let select = select_all(vec![sleep, short_sleep]) .inspect("select_all") .and_then(|(result, index,

    futures)| { println!("Future with index {} returned {:?}", index, result); select_all(futures) .inspect("nested select_all") }); let mut core = Core::new().unwrap(); let (result, index, _) = core.run(select).unwrap(); println!("Future with index {} returned {:?}", index, result);
  123. Future sleep polled: Ok(NotReady) Future short sleep polled: Ok(NotReady) Future

    select_all polled: Ok(NotReady) Future sleep polled: Ok(NotReady) Future short sleep polled: Ok(Ready(())) Future select_all polled: Ok(Ready(((), 1, [ InspectFuture { future: Sleep { timer: Timer, when: Instant Future with index 1 returned () Future sleep polled: Ok(NotReady) Future nested select_all polled: Ok(NotReady) Future sleep polled: Ok(Ready(())) Future nested select_all polled: Ok(Ready(((), 0, []))) Future with index 0 returned ()
  124. Jetzt! Futures können auch sofortige Berechnung darstellen. Hierzu können wir

    von Results zu einer Future übergehen.
  125. let file = File::open("absent"); let future = futures::result(file);

  126. Wird diese Future gepollt, ist sie sofort fertig.

  127. Futures sind ein kombinierbares Konzept zur Nebenläufigkeitsberechnung, das keine konkrete

    Ausführungstechnik vorgibt.
  128. Futures sind momentan DAS beherrschende Konzept zur Nebenläufigkeitsabstraktion.

  129. Beispiele • Java Futures • Promises/A in der JavaScript-Welt

  130. Vorsicht Verwendung von Futures benötigt immernoch nachdenken über Send, Sync,

    Ownership und Borrowing.
  131. Wie war das jetzt mit den Dateien?

  132. Optional: ’static

  133. Fazit Rust bietet ein zwar sehr komplexes, aber mächtiges Konzept

    zur Abstraktion über verschiedene Arten der Nebenläufigkeit.
  134. Es bietet Lösungen für Sharing, Synchronisation und Ordnung.

  135. Fehler in der Anwendung führen zu Compilerfehlern, nicht zu Laufzeitfehlern.

  136. Concurrency without fear.