|
| 1 | +# Ders 11: Multi-Thread Programlama |
| 2 | + |
| 3 | +İşletim sistemi tarafında bir uygulama başlatıldığında tekil bir process içerisinde bir ana iş parçacığı _(main thread)_ |
| 4 | +açılır. Bazı durumlarda program tarafından birden fazla işin eş zamanlı olarak işletilmesi gerekir. Bu durumda |
| 5 | +genellikle yeni thread'ler oluşturulur. Birden fazla iş parçacığını aynı anda çalıştırmak performansı artırmak için |
| 6 | +idealdir ancak çalışması karmaşıktır. Problemlerden birisi farklı thread'lerin aynı veriye erişip kullanmaya |
| 7 | +çalışmasıdır. Thread'ler senkronize edilmediklerinde veriye tutarsız sıralarda erişebilirler. Bu genellikle **Race |
| 8 | +Condition** olarak adlandırılan problemin oluşmasına sebebiyet verir. Bir diğer sorun iki iş parçacığının birbirinin |
| 9 | +beklemesi durumudur ve Deadlock olarak adlandırılır. Her iki problem de eş zamanlı programlamanın _(Concurrent |
| 10 | +Programming)_ thread kullanılan senaryolarda en çok karşımıza çıkanlar arasındadır. |
| 11 | + |
| 12 | + |
| 13 | +_İşletim Sisteminde Process ve Thread Durumları_ |
| 14 | + |
| 15 | +## Thread Oluşturmak |
| 16 | + |
| 17 | +Aşağıdaki kod parçasında ana thread dışında yeni bir thread daha oluşturulması ele alınmaktadır. |
| 18 | + |
| 19 | +```rust |
| 20 | +use std::thread; |
| 21 | +use std::time::Duration; |
| 22 | + |
| 23 | +fn main() { |
| 24 | + start_a_simple_thread(); |
| 25 | + println!("After the thread calling"); |
| 26 | +} |
| 27 | + |
| 28 | +pub fn start_a_simple_thread() { |
| 29 | + let handle = thread::spawn(move || { |
| 30 | + println!("Starting thread..."); |
| 31 | + thread::sleep(Duration::new(3, 0)); |
| 32 | + println!("Thread stopped..."); |
| 33 | + }); |
| 34 | + handle.join().unwrap(); |
| 35 | +} |
| 36 | +``` |
| 37 | + |
| 38 | +Bir thread oluşturmak için spawn metodu kullanılır. Bu metot FnOnce trait uygulayan herhangi bir kod bloğunu parametre |
| 39 | +olarak alabilir. Dışırdan thread içerisine alınacak değişkenler için move operatörü kullanılır. Ayrıca spawn metodu |
| 40 | +geriye bir JoinHandle türü döndürür. Örnekte dikkat edileceği üzere handle üzerinden join çağrısı yapılmıştır. Bu |
| 41 | +yapılmadığı takdirde program çalışmakta olan ikinci thread'in işleyişinin bitmesini beklemeden devam eder ve sonlanır. |
| 42 | +Yani main thread'in, içeride başlatılan diğer thread'lerin işlerini bitirmeden sonlanmasını engellemek için JoinHandle |
| 43 | +nesne takibini yapmak gerekir. |
| 44 | + |
| 45 | +## move Operatörü |
| 46 | + |
| 47 | +Spawn metodu, parametre olarak bir closure kabul eder ve bu kod bloğuna istenirse dış thread üzerinden veri taşınabilir. |
| 48 | +Burada move operatörü ile bir bildirim yapılır. move operatörü ile ilgili durumu anlamak için aşağıdaki kod örneğini göz |
| 49 | +önüne alalım. |
| 50 | + |
| 51 | +```rust |
| 52 | +use std::thread; |
| 53 | + |
| 54 | +fn main() { |
| 55 | + move_keyword_error(); |
| 56 | + println!("After the thread calling"); |
| 57 | +} |
| 58 | + |
| 59 | +pub fn move_keyword_error() { |
| 60 | + let student_points = vec![30.50, 49.90, 65.55, 90.00, 81.00]; |
| 61 | + let handle = thread::spawn(|| { |
| 62 | + println!("Thread is starting..."); |
| 63 | + println!("{:?}", student_points); |
| 64 | + println!("Thread completed"); |
| 65 | + }); |
| 66 | + handle.join().unwrap(); |
| 67 | +} |
| 68 | +``` |
| 69 | + |
| 70 | +Ana thread içinde tanımlı olan student_points vektörünün yeni başlatılan bir thread içerisinde kullanılması |
| 71 | +örnekleniyor. Eğer move operatörünü kullanmazsak derleyici şöyle bir hata mesajı verecektir. |
| 72 | + |
| 73 | +```text |
| 74 | +error[E0373]: closure may outlive the current function |
| 75 | +, but it borrows `student_points`, which is owned by the current function |
| 76 | +``` |
| 77 | + |
| 78 | +Sorun, ana thread'in sahiplendiği bir referans türünün haricen açılmış bir thread'in işlettiği closure'a ödünç verilmeye |
| 79 | +çalışılmasıdır. Dolayısıyla move keyword'ünü kullanarak derleyici bu konuda bilgilendirmek ve yaşam süresinin |
| 80 | +ayarlanmasını sağlamak gerekir. İlginç olan bir durum ise, bu vektörün elemanlarının bir for döngüsü ile ela alınması |
| 81 | +halidir. Konuyu daha iyi anlamak için kodu aşağıdaki şekilde değiştirelim. |
| 82 | + |
| 83 | +```rust |
| 84 | +use std::thread; |
| 85 | + |
| 86 | +fn main() { |
| 87 | + move_keyword_error(); |
| 88 | + println!("After the thread calling"); |
| 89 | +} |
| 90 | + |
| 91 | +pub fn move_keyword_error() { |
| 92 | + let student_points = vec![30.50, 49.90, 65.55, 90.00, 81.00]; |
| 93 | + let handle = thread::spawn(|| { |
| 94 | + println!("Thread is starting..."); |
| 95 | + for point in student_points { |
| 96 | + println!("Processing for point {}", point); |
| 97 | + } |
| 98 | + println!("Thread completed"); |
| 99 | + }); |
| 100 | + handle.join().unwrap(); |
| 101 | +} |
| 102 | +``` |
| 103 | + |
| 104 | +Vektör türü ile ifade edilen veriler bilindiği üzere heap üzerinde konuşlandırlır. main thread içinde başlatılan ikinci |
| 105 | +thread bu vektörün sahipliğini _(ownership)_ closure ile açılan bloğa taşımak ister. Ancak ikinci thread main thread'den |
| 106 | +daha uzun süre çalışabilir ve bu güvenli bir çalışma biçimi değildir _(Memory Safe)_ Bu yüzden rust move operatörü ile |
| 107 | +durumun açıkla belirtilmesini bekler. |
| 108 | + |
| 109 | +Rust **thread safe** bir ortam sağlamaya çalışır. Birden fazla thread'in aynı bellek adresine işaret |
| 110 | +etmesi data race probleminin oluşmasına yol açabilir. **move** kullanıldığında closure ifadesi, student_points |
| 111 | +vektörünün sahipliğini alır. Vektörün sahipliği artık ana thread'e değil, closure bloğuna aittir. Rust |
| 112 | +thread’ler arası sahiplik sorununu bu şekilde çözer ve data race durumunun önüne geçilir. |
| 113 | + |
| 114 | +Peki vektör elemanlarını teker teker dolaştığımızda **move** kullanılmaması neden bir probleme sebebiyet vermez? Bunun |
| 115 | +nedeni closure bloğunun vektörün kendisini değil elemanlarını kullanmasıdır. Zira bu örnekte vektör elemanları boyutu |
| 116 | +belli olan primitive tiplerden **f32** türündendir ve dolayısıyla closure ifadesine kopyalanarak taşınabilirler. Bir |
| 117 | +başka deyişle closure bu kopyalanan veriler üzerinde çalışır ki orjinal vektörün sahiplenilmesi veya borç |
| 118 | +olara alınması söz konusu olmaz. |
| 119 | + |
| 120 | +Kurguya daha işe yarar bir senaryo haline getirelim. Bir sayı dizisindeki elemanların faktöryel değerlerini örneğin 4 |
| 121 | +farklı iş parçacığında ele almaya çalışalım. Aşağıdaki örnek kod parçasında bu durum ele alınmaktadır. |
| 122 | + |
| 123 | +```rust |
| 124 | +use std::thread; |
| 125 | + |
| 126 | +fn main() { |
| 127 | + multiple_threads_sample(); |
| 128 | + println!("After the thread calling"); |
| 129 | +} |
| 130 | + |
| 131 | +fn calc_factorial(n: u64) -> u64 { |
| 132 | + (1..=n).product() |
| 133 | +} |
| 134 | +pub fn multiple_threads_sample() { |
| 135 | + let numbers = vec![10, 3, 5, 13, 8, 9, 1, 2, 17, 11, 7, 6]; |
| 136 | + let threads_count = 4; |
| 137 | + let mut handles = vec![]; |
| 138 | + let chunk_size = numbers.len() / threads_count; |
| 139 | + |
| 140 | + for i in 0..threads_count { |
| 141 | + let chunk = numbers[i * chunk_size..(i + 1) * chunk_size].to_vec(); |
| 142 | + handles.push(thread::spawn(move || { |
| 143 | + let mut results = vec![]; |
| 144 | + for num in chunk { |
| 145 | + println!("Thread {} processing for {}", i, num); |
| 146 | + results.push((num, calc_factorial(num))); |
| 147 | + } |
| 148 | + results |
| 149 | + })); |
| 150 | + } |
| 151 | + |
| 152 | + let mut final_results = vec![]; |
| 153 | + |
| 154 | + for handle in handles { |
| 155 | + final_results.push(handle.join().unwrap()); |
| 156 | + } |
| 157 | + |
| 158 | + println!("{:?}", final_results); |
| 159 | +} |
| 160 | +``` |
| 161 | + |
| 162 | +Numbers isimli vektörün 12 elemanı bulunmaktadır. **threads_count** değişkeni ile belirtilen sayı kadar **thread** |
| 163 | +açılır ki örneğimize göre ana thread haricinde 4 farklı thred açılması söz konusudur. Sayı dizisindeki elemanları dört |
| 164 | +parçaya bölünür ve her blok açılan thread'lerden birisine işlenmek üzere gönderilir. Birden fazla thread söz konusu |
| 165 | +olduğundan her birinin işini bitirmesinin beklenmesi gerekir. Bu yüzden **spawn** çağrılarındaki **JoinHandle** |
| 166 | +nesneleri de bir vektörde toplanır ve son aşamada tamamının sonuçları üretilene kadar beklenir. |
| 167 | + |
| 168 | +## Atomic Reference Counting |
| 169 | + |
| 170 | +// todo@buraksenyurt Not Implemented Yet |
| 171 | + |
| 172 | +## Thread Poisoning |
| 173 | + |
| 174 | +// todo@buraksenyurt Not Implemented Yet |
| 175 | + |
| 176 | +## Concurrency vs Parallel Programming |
| 177 | + |
| 178 | +// todo@buraksenyurt Not Implemented Yet |
0 commit comments