Obsah
1. Programovací jazyk Rust: vlákna a sdílení objektů mezi nimi
2. Metoda thread::sleep a výpočet časového intervalu pro tuto metodu
3. Vytvoření a spuštění nového vlákna
4. Použití anonymní funkce představující tělo nového vlákna
5. Uzávěry a problematika předání vlastnictví objektů
6. Jak tedy do vláken předávat parametry?
7. Korektní implementace uzávěru představujícího tělo nového vlákna
8. Čekání na dokončení běhu vlákna
9. Alokace objektů na haldě s atomickým počítáním referencí (thread-safe)
10. Vytvoření několika threadů s referencemi na společný objekt
11. Repositář s demonstračními příklady
1. Programovací jazyk Rust: vlákna a sdílení objektů mezi nimi
V dnešní části seriálu o programovacím jazyku Rust se seznámíme se způsobem vytváření nových vláken (threads), čekání na dokončení běhu vláken (tato operace se někdy nazývá join) a taktéž s tím, jak lze mezi jednotlivými vlákny sdílet objekty uložené na haldě (heapu). Uvidíme, že i v této oblasti se pro zajištění korektnosti programu ve velké míře uplatňuje typový systém Rustu.
2. Metoda thread::sleep a výpočet časového intervalu pro tuto metodu
Pro zahřátí si ukažme velmi jednoduchý příklad, v němž se sice žádná vlákna nevytváří, ovšem modul std::thread je zde použit. V tomto příkladu se desetkrát vypíše hodnota počitadla, přičemž mezi každým výpisem je běh programu (resp. přesněji řečeno hlavního vlákna programu) pozastaven na dobu přibližně jedné sekundy. Pozastavení zajišťuje funkce thread::sleep(), které se předává objekt typu Duration (ten obsahuje specifikaci časového intervalu v sekundách a nanosekundách). Pro převod mezi milisekundami a typem Duration se používá funkce time::Duration::from_millis(). Důležité je, že tato funkce je nezávislá na typu operačního systému (ovšem při převodu se může provést zaokrouhlení časového intervalu podle možností konkrétního jádra):
use std::thread; use std::time; fn delay(ms : u64) { let amount = time::Duration::from_millis(ms); thread::sleep(amount); } fn main() { for i in 1..10 { println!("counting: {}", i); delay(1000); } }
Příklad spuštění tohoto programu:
counting: 1 counting: 2 counting: 3 counting: 4 counting: 5 counting: 6 counting: 7 counting: 8 counting: 9
3. Vytvoření a spuštění nového vlákna
Vytvoření a spuštění nového vlákna je v programovacím jazyku Rust velmi jednoduché. Postačuje pouze použít funkci thread::spawn, které se předá anonymní funkce, jenž tvoří tělo nového vlákna. Mezi dvojici znaků || se vkládají jména a popř. i typy parametrů (viz též https://www.root.cz/clanky/rust-funkce-lambda-vyrazy-a-rozhodovaci-konstrukce-match/#k06):
thread::spawn(|| print_hello());
Ukažme si příklad, který po svém spuštění vytvoří deset vláken, z nichž každé vypíše na standardní výstup stejnou zprávu:
use std::thread; fn print_hello() { println!("Hello from a thread..."); } fn main() { println!("Starting"); for i in 1..10 { thread::spawn(|| print_hello()); } println!("Stopping"); }
Konkrétní chování programu závisí na tom, kolik vláken stihne vypsat zprávu před ukončením hlavního vlákna, v němž je spuštěno tělo funkce main:
Starting Hello from a thread... Hello from a thread... Hello from a thread... Hello from a thread... Hello from a thread... Stopping
Program si můžeme nepatrně upravit, aby bylo patrné, jak se prolíná běh hlavního vlákna s ostatními vlákny:
use std::thread; fn print_hello() { println!("Hello from a thread..."); } fn main() { println!("Starting"); for i in 1..10 { println!("{}", i); thread::spawn(|| print_hello()); } println!("Stopping"); }
Jeden z příkladů spuštění programu (po každém spuštění se může pořadí zpráv nepatrně lišit):
1 2 3 4 Hello from a thread... 5 Hello from a thread... Hello from a thread... 6 Hello from a thread... Hello from a thread... 7 8 9 Stopping
4. Použití anonymní funkce představující tělo nového vlákna
Vzhledem k tomu, že se do funkce thread::spawn() předává kód, který se spustí v nově vytvořeném vlákně, můžeme na tomto místě použít i anonymní funkci, s čímž se v praxi setkáme poměrně často. Konkrétně to znamená, že namísto:
thread::spawn(|| print_hello());
je možné psát:
thread::spawn(|| {println!("Hello from a thread...");});
Celý program, který po svém spuštění vytvoří deset vláken, může být zkrácen takto:
use std::thread; fn main() { println!("Starting"); for i in 1..10 { thread::spawn(|| {println!("Hello from a thread...");}); } println!("Stopping"); }
V kódu nikde nečekáme na ukončení vláken, což znamená, že jedno spuštění programu může dopadnout dobře:
Starting Hello from a thread... Hello from a thread... Hello from a thread... Hello from a thread... Hello from a thread... Stopping Hello from a thread... Hello from a thread... Hello from a thread...
Zatímco při dalším spuštění dojde k běhové chybě, protože se nějaké vlákno bude snažit zapisovat do standardního výstupu, který je však již uzavřen:
Starting Hello from a thread... Hello from a thread... Hello from a thread... Hello from a thread... Hello from a thread... Stopping Hello from a thread... Hello from a thread... Hello from a thread... thread '<unnamed>' panicked at 'cannot access stdout during shutdown', ../src/libcore/option.rs:700 note: Run with `RUST_BACKTRACE=1` for a backtrace.
5. Uzávěry a problematika předání vlastnictví objektů
Při vytvoření a spuštění nového vlákna nemusí být tělo vlákna tvořeno „pouhou“ funkcí, ale může se jednat o uzávěr (closure). Uzávěry, s nimiž jsme se ve stručnosti seznámili ve třetí části tohoto článku, jsou funkce (typicky anonymní), které na sebe mají navázánu alespoň jednu tzv. volnou proměnnou. Ukažme si velmi jednoduchý příklad použití. Budeme potřebovat, aby se do každého vlákna předalo identifikační číslo vlákna, protože v těle vlákna budeme chtít vytisknout jeho číslo:
{println!("Hello from a thread #{}", i);}
Intuitivně by se implementace pro deset vláken mohla napsat následovně:
use std::thread; fn main() { println!("Starting"); for i in 1..10 { thread::spawn(|| {println!("Hello from a thread #{}", i);}); } println!("Stopping"); }
Poznámka: znaky || již známe – lze do nich zapsat případné parametry předávané do uzávěru, což ovšem v případě vláken nemá význam.
Ve skutečnosti ovšem toto řešení není korektní, protože program nebude možné přeložit. Při pokusu o překlad se vypíše toto chybové hlášení:
error[E0373]: closure may outlive the current function, but it borrows `i`, which is owned by the current function --> 112_thread_clojure_error.rs:6:23 | 6 | thread::spawn(|| {println!("Hello from a thread #{}", i);}); | ^^ - `i` is borrowed here | | | may outlive borrowed value `i` | help: to force the closure to take ownership of `i` (and any other referenced variables), use the `move` keyword, as shown: | thread::spawn(move || {println!("Hello from a thread #{}", i);}); error: aborting due to previous error
6. Jak tedy do vláken předávat parametry?
O co se jedná? Uzávěry skutečně mohou přistupovat k původně volným proměnným, které jsou na ně navázány (tj. v našem případě konkrétně k proměnné i), což většinou nezpůsobuje žádný problém. I proto uzávěry přistupují k proměnným přes reference, tj. v těle uzávěru vlastně máme k dispozici &i a ne i (toto je implementační vlastnost, se kterou se setkáme i v mnoha dalších jazycích, i když většinou taktéž skrytě).
Právě zde ovšem leží jádro problému – ve chvíli, kdy je tělo uzávěru spuštěno v jiném vlákně, může dojít k tomu, že oblast platnosti proměnné i skončí dříve, než běh vlákna – jinými slovy smyčka for i in 1..10 úspěšně nastartuje všechna vlákna a přejde k příkazu println!{„Stopping“};, zatímco vlákno může stále běžet. To by vedlo ke vzniku takzvaného dangling pointeru nebo wild pointeru, protože z vlákna by bylo možné přes referenci na i přistupovat do paměti, která již původnímu i nepatří (zde se sice bavíme o proměnné i, ale vždy máme na mysli objekt, který je na i navázán).
Programovací jazyk Rust tuto skutečnost, která může vést k velmi špatně detekovatelným chybám, které se dokonce ani nemusí projevit při testování, kontroluje a proto vypsal zmíněné chybové hlášení.
7. Korektní implementace uzávěru představujícího tělo nového vlákna
Korektní implementace uzávěru (resp. vlákna, kterému potřebujeme předat parametry) musí být nepatrně změněna z :
thread::spawn(|| {println!("Hello from a thread #{}", i);});
na:
thread::spawn(move || {println!("Hello from a thread #{}", i);});
Modifikátorem move umístěným před |názvy_parametrů| specifikujeme, že uzávěr nezíská pouze referenci na navázané proměnné, ale přímo jejich vlastnictví (interně se pro uzávěr vytvoří vlastní rámec na zásobníku). Teoreticky by tedy použití tohoto modifikátoru mělo vést k chybě překladu, protože proměnná i je použita jako počitadlo smyčky. Jenže zde se navíc projeví další vlastnost – celočíselné datové typy (a nejenom ony) implementují trait Copy, takže uzávěr sice skutečně získá vlastnictví, ovšem vlastnictví kopie původní hodnoty. A to je přesně to chování, kterého potřebujeme dosáhnout:
use std::thread; fn main() { println!("Starting"); for i in 1..10 { thread::spawn(move || {println!("Hello from a thread #{}", i);}); } println!("Stopping"); }
Poznámka: zde bych chtěl znovu upozornit na to, jak návrh typového systému Rustu ovlivňuje i další vlastnosti tohoto jazyka. Díky typovému systému, metodám pro předávání vlastnictví objektů a taktéž traitu Copy nám Rust „nutí“ korektní implementaci vláken, a to bez toho, aby vznikaly problémy při přístupu ke sdíleným prostředkům. Pokud je totiž zapotřebí skutečně sdílet společné objekty, jsme (většinou) donuceni využít dále popsaný Arc.
Spuštění může vypadat například takto (ovšem chování se samozřejmě bude odlišovat podle toho, jak přesně scheduler přepíná mezi vlákny):
Starting Hello from a thread #3 Hello from a thread #2 Hello from a thread #1 Hello from a thread #4 Hello from a thread #6 Hello from a thread #7 Hello from a thread #5 Stopping Hello from a thread #9
8. Čekání na dokončení běhu vlákna
Předchozí demonstrační příklady ve skutečnosti nebyly napsány příliš korektně, protože po určitém počtu pokusů o spuštění dojde k běhové chybě – nějaké vlákno se začne pokoušet o zápis na standardní výstup v době, kdy už je tento výstup zavřený z hlavního vlákna, v němž je spuštěna funkce main. Ovšem i při běžném programování se setkáme se situacemi, kdy potřebujeme počkat na ukončení běhu jiného vlákna. K docílení tohoto stavu lze využít hned několik technik, ovšem nejjednodušší je zavolat metodu join() na objekt vrácený funkcí thread::spawn(). Prozatím jsme tento objekt nijak nevyužívali (a tudíž ani neukládali do proměnné), ale samozřejmě je to možné. Typ tohoto objektu je std::thread::JoinHandle. Kód, který vytvoří nové vlákno a ihned poté počká ne jeho dokončení, bude vypadat následovně:
use std::thread; fn main() { println!("Starting"); for i in 1..10 { let thr = thread::spawn(move || {println!("Hello from a thread #{}", i);}); thr.join(); } println!("Stopping"); }
Typ proměnné thr je Rustem odvozen automaticky, ale lze ho samozřejmě napsat i explicitně. Nesmíme zapomenout na to, že se jedná o generický typ (podle „typu“ uzávěru), takže se používá zápis Typ<_>:
use std::thread; fn main() { println!("Starting"); for i in 1..10 { let thr : thread::JoinHandle<_> = thread::spawn(move || {println!("Hello from a thread #{}", i);}); thr.join(); } println!("Stopping"); }
Po spuštění zjistíme, že díky čekání na dokončení vlákna jsou jednotlivá vlákna spouštěna sériově a nikoli paralelně:
Starting Hello from a thread #1 Hello from a thread #2 Hello from a thread #3 Hello from a thread #4 Hello from a thread #5 Hello from a thread #6 Hello from a thread #7 Hello from a thread #8 Hello from a thread #9 Stopping
9. Alokace objektů na haldě s atomickým počítáním referencí (thread-safe)
S problematikou vláken velmi úzce souvisí i způsob vytváření objektů na haldě se sdílením těchto objektů (resp. referencí na tyto objekty) mezi více vlákny. V předchozím článku jsme se seznámili s počítáním referencí realizovaných přes objekt typu Rc. Připomeňme si, že je možné vytvořit objekt na haldě, „zabalit“ ho do Rc a posléze na tento objekt vytvořit libovolné množství referencí přes funkci Rc::clone(). Díky zabalení objektu do Rc je možné počítat reference na objekt na haldě s tím, že pokud počitadlo referencí klesne na nulu, je možné objekt automaticky dealokovat (a předtím zavolat jeho destruktor). To je sice pěkné a funkční řešení, jenže jak se bude chovat počitadlo referencí v případě, že objekt bude sdílen mezi větším množstvím vláken?
Odpověď na tuto otázku je jednoznačná – funkčnost není zaručena a je nutné využít jiný mechanismus. Tento mechanismus spočívá v použití datového typu Arc<T> namísto nám již známého typu Rc<T>. Název Arc nemá nic společného s obloukem, protože se jedná o zkratku názvu „Atomic Reference Counting“. Slovo „Atomic“ je zde důležité, protože podstatným rozdílem mezi Rc<T> a Arc<T> je použití počitadla, jehož hodnota se zvyšuje, snižuje a testuje na nulu v atomické operaci, kterou nemohou ostatní vlákna přerušit a přístup mají až k výsledku. To je sice poměrně neefektivní operace, která může (pokud nemáme vhodně navržený hardware) dokonce vést k tomu, že budou ostatní vlákna pozastavena, na druhou stranu se však jedná o jednu z mála možností, jak zaručit, že nedojde například k přepsání jednoho výsledku (hodnoty počitadla) výsledkem jiným (v mezním případě může dojít k přetečení či častěji k podtečení hodnoty počitadla přes 0).
10. Vytvoření několika threadů s referencemi na společný objekt
Podívejme se na jednoduché použití sdílení objektu mezi více vlákny s použitím Arc<T>. Vrátíme se k našemu příkladu s objektem (datovou strukturou) reprezentujícím komplexní číslo. Konstrukce komplexního čísla a jeho alokace na haldu se provede následovně (oproti demonstračnímu příkladu z předchozího článku jsme zde pouze nahradili Rc a Arc):
let c = Arc::new(Complex::new(1.0, 1.0));
Aby se v demonstračním příkladu skutečně ukázalo, jakým způsobem se řeší sdílení jednoho objektu mezi vlákny, vytvoříme novou datovou strukturu ComplexNumberOwner i s pomocnou metodou. Opět zde došlo k náhradě Rc za Arc:
struct ComplexNumberOwner { id: i32, value: Arc<Complex> } impl ComplexNumberOwner { fn print(&self) { println!("owner: number #{} with value {}+{}i", self.id, self.value.real, self.value.imag); } }
Dále ve funkci start_threads() vytvoříme jeden objekt typu komplexní číslo a umístíme ho na haldu:
let c = Arc::new(Complex::new(1.0, 1.0));
Posléze se v programové smyčce vytvoří deset samostatně běžících vláken a každému vláknu se předá nový „vlastník“ komplexního čísla, který jako svůj prvek obsahuje referenci na objekt alokovaný na haldě. Důležité je, že volání c.clone() vytvoří novou referenci a současně atomicky zvýší počitadlo referencí. Vlákna skončí až po uplynutí přibližně 400 ms, aby bylo možné dobře sledovat práci alokátoru a dealokátoru:
fn start_threads() { let c = Arc::new(Complex::new(1.0, 1.0)); for id in 0..10 { let owner = ComplexNumberOwner{id:id, value: c.clone()}; thread::spawn(move || { owner.print(); delay(400); }); } }
Úplný zdrojový kód tohoto příkladu vypadá následovně:
use std::sync::Arc; use std::thread; use std::time; struct Complex { real: f32, imag: f32, } impl Complex { fn new(real: f32, imag: f32) -> Complex { println!("Constructing complex number: {:}+{:}i", real, imag); Complex{real:real, imag:imag} } fn print(&self) { println!("complex number: {:?}+{:?}i", self.real, self.imag); } } impl Drop for Complex { fn drop(&mut self) { println!("Dropping complex number: {:}+{:}i", self.real, self.imag); } } struct ComplexNumberOwner { id: i32, value: Arc<Complex> } impl ComplexNumberOwner { fn print(&self) { println!("owner: number #{} with value {}+{}i", self.id, self.value.real, self.value.imag); } } fn delay(ms : u64) { let amount = time::Duration::from_millis(ms); thread::sleep(amount); } fn start_threads() { let c = Arc::new(Complex::new(1.0, 1.0)); for id in 0..10 { let owner = ComplexNumberOwner{id:id, value: c.clone()}; thread::spawn(move || { owner.print(); delay(400); }); } } fn main() { println!("starting threads"); start_threads(); println!("all threads started"); delay(2000); }
Ukázka chování tohoto příkladu po jeho spuštění. Povšimněte si, že se skutečně zavolá jen jediný konstruktor a po ukončení vláken i destruktor:
starting threads Constructing complex number: 1+1i owner: number #0 with value 1+1i owner: number #2 with value 1+1i owner: number #1 with value 1+1i owner: number #3 with value 1+1i owner: number #4 with value 1+1i owner: number #5 with value 1+1i owner: number #6 with value 1+1i owner: number #7 with value 1+1i all threads started owner: number #8 with value 1+1i owner: number #9 with value 1+1i Dropping complex number: 1+1i
11. Repositář s demonstračními příklady
Všechny dnes popisované demonstrační příklady byly, podobně jako ve všech předchozích částech tohoto seriálu, uloženy do Git repositáře dostupného na adrese https://github.com/tisnik/presentations. Příklady si můžete v případě potřeby stáhnout i jednotlivě bez nutnosti klonovat celý repositář:
Příklad | Odkaz |
---|---|
109_thread_sleep.rs | https://github.com/tisnik/presentations/blob/master/rust/109_thread_sleep.rs |
110_thread_spawn.rs | https://github.com/tisnik/presentations/blob/master/rust/110_thread_spawn.rs |
111_thread_lambda.rs | https://github.com/tisnik/presentations/blob/master/rust/111_thread_lambda.rs |
112_thread_clojure_error.rs | https://github.com/tisnik/presentations/blob/master/rust/112_thread_clojure_error.rs |
113_thread_clojure_correct.rs | https://github.com/tisnik/presentations/blob/master/rust/113_thread_clojure_correct.rs |
114_thread_join.rs | https://github.com/tisnik/presentations/blob/master/rust/114_thread_join.rs |
115_thread_arc.rs | https://github.com/tisnik/presentations/blob/master/rust/115_thread_arc.rs |
12. Odkazy na Internetu
- Concurrency
https://doc.rust-lang.org/book/concurrency.html - Learning Rust With Entirely Too Many Linked Lists
http://cglab.ca/~abeinges/blah/too-many-lists/book/ - Testcase: linked list
http://rustbyexample.com/custom_types/enum/testcase_linked_list.html - Operators and Overloading
https://doc.rust-lang.org/book/operators-and-overloading.html - Function std::thread::sleep
https://doc.rust-lang.org/std/thread/fn.sleep.html - Struct std::thread::JoinHandle
https://doc.rust-lang.org/std/thread/struct.JoinHandle.html - Function std::thread::spawn
https://doc.rust-lang.org/std/thread/fn.spawn.html - Module std::ops
https://doc.rust-lang.org/std/ops/index.html - Module std::cmp
https://doc.rust-lang.org/std/cmp/index.html - Trait std::ops::Add
https://doc.rust-lang.org/stable/std/ops/trait.Add.html - Trait std::ops::AddAssign
https://doc.rust-lang.org/std/ops/trait.AddAssign.html - Trait std::ops::Drop
https://doc.rust-lang.org/std/ops/trait.Drop.html - Trait std::cmp::Eq
https://doc.rust-lang.org/std/cmp/trait.Eq.html - Struct std::boxed::Box
https://doc.rust-lang.org/std/boxed/struct.Box.html - Explore the ownership system in Rust
https://nercury.github.io/rust/guide/2015/01/19/ownership.html - Rust's ownership and move semantic
http://www.slideshare.net/saneyuki/rusts-ownership-and-move-semantics - Trait std::marker::Copy
https://doc.rust-lang.org/stable/std/marker/trait.Copy.html - Trait std::clone::Clone
https://doc.rust-lang.org/stable/std/clone/trait.Clone.html - The Stack and the Heap
https://doc.rust-lang.org/book/the-stack-and-the-heap.html - Rust Compare: Pointers & References
http://www.rust-compare.com/site/pointers.html - Rust Compare: Parameters
http://www.rust-compare.com/site/params.html - Why does this compile? Automatic dereferencing?
https://users.rust-lang.org/t/why-does-this-compile-automatic-dereferencing/2183 - Understanding Pointers, Ownership, and Lifetimes in Rust
http://koerbitz.me/posts/Understanding-Pointers-Ownership-and-Lifetimes-in-Rust.html - Rust lang series episode #25 — pointers (#rust-series)
https://steemit.com/rust-series/@jimmco/rust-lang-series-episode-25-pointers-rust-series - Rust – home page
https://www.rust-lang.org/en-US/ - Rust – Frequently Asked Questions
https://www.rust-lang.org/en-US/faq.html - Destructuring and Pattern Matching
https://pzol.github.io/getting_rusty/posts/20140417_destructuring_in_rust/ - The Rust Programming Language
https://doc.rust-lang.org/book/ - Rust (programming language)
https://en.wikipedia.org/wiki/Rust_%28programming_language%29 - Go – home page
https://golang.org/ - Stack Overflow – Most Loved, Dreaded, and Wanted language
https://stackoverflow.com/research/developer-survey-2016#technology-most-loved-dreaded-and-wanted - Rust vs Go (dva roky staré hodnocení, od té doby došlo k posunům v obou jazycích)
http://jaredforsyth.com/2014/03/22/rust-vs-go/ - Rust vs Go: My experience
https://www.reddit.com/r/golang/comments/21m6jq/rust_vs_go_my_experience/ - Friends of Rust (Organizations running Rust in production)
https://www.rust-lang.org/en-US/friends.html - Rust programs versus C++ g++
https://benchmarksgame.alioth.debian.org/u64q/compare.php?lang=rust&lang2=gpp - Další benchmarky (nejedná se o reálné příklady „ze života“)
https://github.com/kostya/benchmarks - Go na Redditu
https://www.reddit.com/r/golang/ - Rust vs. Go
http://vschart.com/compare/rust/vs/go-language - Abstraction without overhead: traits in Rust
https://blog.rust-lang.org/2015/05/11/traits.html - Method Syntax
https://doc.rust-lang.org/book/method-syntax.html - Traits in Rust
https://doc.rust-lang.org/book/traits.html - Functional Programming in Rust – Part 1 : Function Abstraction
http://blog.madhukaraphatak.com/functional-programming-in-rust-part-1/ - Of the emerging systems languages Rust, D, Go and Nim, which is the strongest language and why?
https://www.quora.com/Of-the-emerging-systems-languages-Rust-D-Go-and-Nim-which-is-the-strongest-language-and-why - Chytré ukazatele (moderní verze jazyka C++) [MSDN]
https://msdn.microsoft.com/cs-cz/library/hh279674.aspx - UTF-8 Everywhere
http://utf8everywhere.org/ - Rust by Example
http://rustbyexample.com/ - Rust oficiálně ve Fedoře
https://mojefedora.cz/rust-oficialne-ve-fedore/ - Resource acquisition is initialization
https://en.wikipedia.org/wiki/Resource_acquisition_is_initialization - TIOBE index (October 2016)
http://www.tiobe.com/tiobe-index/ - Porovnání Go, D a Rustu na OpenHubu:
https://www.openhub.net/languages/compare?language_name[]=-1&language_name[]=-1&language_name[]=dmd&language_name[]=golang&language_name[]=rust&language_name[]=-1&measure=commits - String Types in Rust
http://www.suspectsemantics.com/blog/2016/03/27/string-types-in-rust/ - Trait (computer programming)
https://en.wikipedia.org/wiki/Trait_%28computer_programming%29 - Type inference
https://en.wikipedia.org/wiki/Type_inference