today's studying
This commit is contained in:
parent
171c8d5489
commit
71db955e55
10 changed files with 446 additions and 2 deletions
175
20-dev/00-rust/30-fearless/exersizes.md
Normal file
175
20-dev/00-rust/30-fearless/exersizes.md
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
Прекрасная цель. Переход от однопоточного мышления к многопоточному (Concurrent Thinking) — это именно то, что отличает Junior/Middle от Senior Systems Engineer.
|
||||
|
||||
В однопоточном коде время линейно: **A -> B -> C**.
|
||||
В многопоточном коде время "разветвляется", и вы теряете детерминизм: **A** происходит, а **B** и **C** могут случиться в любом порядке, одновременно или **B** может начаться, прерваться на середине, пропустить **C**, а потом продолжиться.
|
||||
|
||||
Чтобы научиться "думать потоками", нужно пройти через три этапа "боли". Давайте я дам вам конкретные упражнения (Kata) без использования библиотек типа Rayon/Tokio, только `std::thread` и `std::sync`.
|
||||
|
||||
Вот ваша дорожная карта от "однопоточника" до "системщика".
|
||||
|
||||
### Уровень 1: Shared State (Разделяемое состояние)
|
||||
*Суть: Как не сломать данные, когда их трогают все сразу.*
|
||||
|
||||
**Задача: "Банковский симулятор"**
|
||||
У вас есть несколько счетов и несколько потоков, которые переводят деньги туда-сюда.
|
||||
* **Сложность:** Если поток А переводит со счета 1 на счет 2, а поток Б переводит со счета 2 на счет 1 одновременно, может возникнуть **Deadlock** (взаимная блокировка). Оба захватят первый мьютекс и будут вечно ждать второго.
|
||||
|
||||
**Челлендж для реализации:**
|
||||
1. Создайте структуру `Bank` с вектором `Mutex<i32>` (счета).
|
||||
2. Напишите функцию `transfer(from_id, to_id, amount)`.
|
||||
3. **Главная цель:** Реализовать это так, чтобы при 1000 одновременных рандомных переводов программа не зависла (deadlock) и сумма всех денег в банке осталась неизменной (invariant).
|
||||
|
||||
*Подсказка сеньора:* Блокировки всегда нужно брать в определенном порядке (например, всегда блокировать меньший ID счета первым).
|
||||
|
||||
### Уровень 2: Coordination & Signaling (Координация)
|
||||
*Суть: Как заставить потоки ждать друг друга без `sleep`.*
|
||||
|
||||
**Задача: "Пинг-Понг" (или Producer-Consumer)**
|
||||
Два потока. Один печатает "Ping", другой "Pong".
|
||||
Строго по очереди!
|
||||
Вывод должен быть: `Ping, Pong, Ping, Pong...`
|
||||
Нельзя использовать: `channel` (каналы).
|
||||
Нужно использовать: `Condvar` (Condition Variable) и `Mutex`.
|
||||
|
||||
**Зачем это нужно:**
|
||||
Вы поймете, что такое *spurious wakeups* (ложные пробуждения) и почему паттерн всегда выглядит как:
|
||||
```rust
|
||||
while !condition { // <-- Важно: WHILE, а не IF
|
||||
cvar.wait(guard).unwrap();
|
||||
}
|
||||
```
|
||||
Это база построения любых очередей задач.
|
||||
|
||||
### Уровень 3: Lock-free (Атомики и память)
|
||||
*Суть: Максимальная скорость, работа с инструкциями процессора.*
|
||||
|
||||
**Задача: "Spinlock" (Спин-блокировка)**
|
||||
Реализуйте свой собственный мьютекс, используя только `AtomicBool` и `Compare-and-Swap`.
|
||||
Стандартный `Mutex` усыпляет поток (syscall к ОС), а ваш должен крутиться в цикле `while`, пока не захватит флаг.
|
||||
|
||||
```rust
|
||||
pub struct MySpinLock<T> {
|
||||
locked: AtomicBool,
|
||||
data: UnsafeCell<T>, // Придется немного потрогать unsafe, это полезно для понимания
|
||||
}
|
||||
```
|
||||
|
||||
**Челлендж:**
|
||||
Поймите, почему ваш спинлок греет процессор на 100% и как `std::thread::yield_now()` помогает это исправить.
|
||||
|
||||
***
|
||||
|
||||
### Практический пример для разбора прямо сейчас
|
||||
|
||||
Давайте разберем **Deadlock** — самую частую ошибку новичков после гонок данных.
|
||||
|
||||
Попробуйте в уме (или в коде) предсказать, что случится здесь:
|
||||
|
||||
```rust
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::thread;
|
||||
|
||||
fn main() {
|
||||
let resource_a = Arc::new(Mutex::new(0));
|
||||
let resource_b = Arc::new(Mutex::new(0));
|
||||
|
||||
let a = resource_a.clone();
|
||||
let b = resource_b.clone();
|
||||
|
||||
// Поток 1: Хочет A, потом B
|
||||
let t1 = thread::spawn(move || {
|
||||
let _guard_a = a.lock().unwrap();
|
||||
println!("T1 взял A");
|
||||
thread::sleep(std::time::Duration::from_millis(100)); // Имитация работы
|
||||
let _guard_b = b.lock().unwrap(); // Ждет B
|
||||
println!("T1 взял B");
|
||||
});
|
||||
|
||||
let a = resource_a.clone();
|
||||
let b = resource_b.clone();
|
||||
|
||||
// Поток 2: Хочет B, потом A
|
||||
let t2 = thread::spawn(move || {
|
||||
let _guard_b = b.lock().unwrap();
|
||||
println!("T2 взял B");
|
||||
thread::sleep(std::time::Duration::from_millis(100));
|
||||
let _guard_a = a.lock().unwrap(); // Ждет A
|
||||
println!("T2 взял A");
|
||||
});
|
||||
|
||||
t1.join().unwrap();
|
||||
t2.join().unwrap();
|
||||
}
|
||||
```
|
||||
|
||||
**Результат:** Программа напечатает "T1 взял A", "T2 взял B" и зависнет навсегда.
|
||||
Это классический deadlock. Поток 1 держит A и ждет B. Поток 2 держит B и ждет A.
|
||||
|
||||
**Как мыслит Сеньор:**
|
||||
1. *"Ага, вложенные блокировки (`lock` внутри другого `lock`). Это красная зона."*
|
||||
2. *"Есть ли гарантия порядка захвата?"* (В коде выше — нет, порядок обратный).
|
||||
3. **Решение:** Всегда захватывать ресурсы в **одном и том же глобальном порядке**. Если всем потокам сначала нужен А, а потом Б — дедлока не будет.
|
||||
|
||||
**Предлагаю сделку:**
|
||||
Реализуйте простую версию **"Пинг-Понга"** (Уровень 2) на `Condvar`. Это лучшее упражнение, чтобы почувствовать, как потоки "разговаривают" друг с другом, передавая управление. Если застрянете — я подскажу, но сначала попробуйте сами. Это даст вам тот самый "щелчок" понимания.
|
||||
|
||||
|
||||
Да, на Exercism в треке Rust есть отличные упражнения именно на эту тему. Они там считаются "классикой", через которую обязан пройти каждый "раст-конкаренси-бой".[1][2]
|
||||
|
||||
Вот **Топ-3**, которые идеально ложатся на вашу цель (от простого к сложному):
|
||||
|
||||
### 1. Parallel Letter Frequency (`parallel-letter-frequency`)
|
||||
* **Суть:** То, что мы с вами обсуждали. Дан список текстов, нужно подсчитать частоту букв, используя потоки.[3][4]
|
||||
* **Чему учит:**
|
||||
* `std::thread::spawn` vs Rayon (можно решить и так, и так).
|
||||
* Как правильно делить данные (Data Partitioning).
|
||||
* Использование `HashMap` в многопоточной среде (Merge vs Shared Mutex).
|
||||
* **Ваш челлендж:** Решите это сначала через `thread::spawn` + `mpsc` (каналы), а потом через `thread::scope` и возвращаемые значения. Сравните бенчмарки.
|
||||
|
||||
### 2. Dining Philosophers ("Обедающие философы")
|
||||
* **Суть:** 5 философов, 5 вилок. Чтобы поесть, нужно две вилки. Если все возьмут левую вилку одновременно — **Deadlock** (все ждут правую вечно).[5][6][7]
|
||||
* **Чему учит:**
|
||||
* Управление ресурсами (`Mutex`).
|
||||
* Избегание Deadlock (Resource Ordering).
|
||||
* Работа с `Arc` для шаринга вилок.
|
||||
* **Ваш челлендж:** Напишите решение, где философы всегда едят, и никто не голодает. Попробуйте реализовать это через "Арбитра" (официанта) или через стратегию "Чётный-Нечётный".
|
||||
|
||||
### 3. Circular Buffer (Кольцевой буфер)
|
||||
* **Суть:** Реализовать структуру данных, в которую можно писать и читать.
|
||||
* **Чему учит:**
|
||||
* Если усложнить задачу до *Concurrent Circular Buffer* (хотя в базе она однопоточная), то это идеальный полигон для **Atomics**.
|
||||
* Попробуйте сделать его `Lock-Free`. Это уже уровень "God Mode".
|
||||
|
||||
### Где их найти:
|
||||
На сайте Exercism.org в треке Rust.
|
||||
1. Зарегистрируйтесь (бесплатно).
|
||||
2. `exercism download --exercise=parallel-letter-frequency --track=rust`
|
||||
3. Решайте локально в NeoVim/Helix, запускайте `cargo test`.
|
||||
|
||||
### Бонус: Comprehensive Rust (от Google)
|
||||
У Google есть курс **Comprehensive Rust**, и там есть отдельный раздел "Concurrency" с упражнениями.
|
||||
Особенно упражнение **"Dining Philosophers"** там разобрано очень детально, с вариантами решений через каналы и мьютексы.[6][8]
|
||||
* Ссылка: `google.github.io/comprehensive-rust/concurrency/`
|
||||
|
||||
**Совет:** Начните с **Dining Philosophers**. Это упражнение сломает вам мозг (в хорошем смысле) именно на тему "как потоки мешают друг другу", а не просто "как ускорить вычисления".
|
||||
|
||||
[1](https://exercism.org/tracks/rust/exercises)
|
||||
[2](https://exercism.org/tracks/rust)
|
||||
[3](https://exercism.org/tracks/rust/exercises/parallel-letter-frequency)
|
||||
[4](https://exercism.org/tracks/rust/exercises/parallel-letter-frequency/solutions/tommilligan)
|
||||
[5](http://www.akira.ruc.dk/~keld/teaching/ipdc_f10/exercises/exercise1.pdf)
|
||||
[6](https://google.github.io/comprehensive-rust/concurrency/sync-exercises/dining-philosophers.html)
|
||||
[7](https://stackoverflow.com/questions/31912781/why-doesnt-the-dining-philosophers-exercise-deadlock-if-done-incorrectly)
|
||||
[8](https://comprehensive-rust.pages.dev/exercises/concurrency/dining-philosophers.html)
|
||||
[9](http://forum.exercism.org/t/rust-syllabus-overview-tracking/5346)
|
||||
[10](http://forum.exercism.org/t/rust-syllabus-overview-tracking/5346?page=2)
|
||||
[11](https://exercism.org/tracks/rust/exercises/parallel-letter-frequency/solutions/LU15W1R7H)
|
||||
[12](http://forum.exercism.org/t/rust-track-exercise-order/10855)
|
||||
[13](https://exercism.org/exercises/parallel-letter-frequency)
|
||||
[14](https://github.com/exercism/rust)
|
||||
[15](https://github.com/google/comprehensive-rust/discussions/1313)
|
||||
[16](https://www.youtube.com/watch?v=Sf4tf-4d2ag)
|
||||
[17](http://forum.exercism.org/t/48in24-exercise-03-12-parallel-letter-frequency/9927)
|
||||
[18](https://exercism.org/tracks/tcl)
|
||||
[19](https://www.youtube.com/watch?v=bkNQ1_kGHYE)
|
||||
[20](http://forum.exercism.org/t/would-be-nice-to-add-concurrency-to-the-concept-tree-in-the-go-track/4539)
|
||||
Loading…
Add table
Add a link
Reference in a new issue