Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Зводимо все докупи: future, tasks і threads

Як ми бачили в Розділі 16, threads надають один підхід до конкурентності. У цьому розділі ми побачили інший підхід: використання async із future і streams. Якщо ви замислюєтеся, коли обрати один метод замість іншого, відповідь така: це залежить! І в багатьох випадках вибір — не threads або async, а радше threads і async.

Багато операційних систем уже десятиліттями надають моделі конкурентності на основі threads, і як наслідок багато мов програмування їх підтримують. Однак ці моделі не позбавлені своїх компромісів. На багатьох операційних системах вони використовують чималу кількість пам’яті для кожного thread. Threads також є варіантом лише тоді, коли ваші операційна система і апаратура підтримують їх. На відміну від поширених настільних і мобільних комп’ютерів, деякі вбудовані системи взагалі не мають ОС, тож у них також немає threads.

Модель async надає інший — і зрештою взаємодоповнювальний — набір компромісів. У моделі async паралельні операції не потребують власних threads. Замість цього вони можуть виконуватися на tasks, як тоді, коли ми використали trpl::spawn_task, щоб запустити роботу із синхронної функції в розділі про streams. Task подібний до thread, але замість того, щоб керуватися операційною системою, ним керує код рівня бібліотеки: runtime.

Є причина, чому API для створення threads і створення tasks такі схожі. Threads виступають межею для наборів синхронних операцій; конкурентність можлива між threads. Tasks виступають межею для наборів асинхронних операцій; конкурентність можлива як між, так і всередині tasks, тому що task може перемикатися між future у своєму тілі. Нарешті, future — це найдрібніша одиниця конкурентності в Rust, і кожен future може представляти дерево інших future. Runtime — зокрема, його executor — керує tasks, а tasks керують future. У цьому сенсі tasks подібні до легких threads, керованих runtime, з додатковими можливостями, що виникають завдяки тому, що ними керує runtime, а не операційна система.

Це не означає, що async tasks завжди кращі за threads (або навпаки). Конкурентність із threads у деяких аспектах є простішою моделлю програмування, ніж конкурентність з async. Це може бути як перевагою, так і недоліком. Threads певною мірою працюють за принципом “запустив і забув”; у них немає вбудованого аналога future, тож вони просто виконуються до завершення, не перериваючись нічим, окрім самої операційної системи.

І виявляється, що threads і tasks часто дуже добре працюють разом, тому що tasks можуть (принаймні в деяких runtime) переміщуватися між threads. Фактично, під капотом runtime, який ми використовували, — включно з функціями spawn_blocking і spawn_task — за замовчуванням є багатопотоковим! Багато runtime використовують підхід, який називається work stealing, щоб прозоро переміщувати tasks між threads залежно від того, як саме threads зараз використовуються, щоб покращити загальну продуктивність системи. Цей підхід насправді потребує threads і tasks, а отже й future.

Коли думаєте про те, який метод використовувати в певній ситуації, враховуйте такі правила:

  • Якщо робота дуже добре паралелізується (тобто є CPU-bound), наприклад, оброблення великого обсягу даних, де кожну частину можна обробити окремо, threads є кращим вибором.
  • Якщо робота дуже конкурентна (тобто є I/O-bound), наприклад, оброблення повідомлень із великої кількості різних джерел, які можуть надходити з різними інтервалами або з різною швидкістю, async є кращим вибором.

І якщо вам потрібні і паралелізм, і конкурентність, вам не потрібно обирати між threads і async. Ви можете вільно використовувати їх разом, дозволяючи кожному виконувати ту роль, у якій він найкращий. Наприклад, Уривок 17-25 показує досить поширений приклад такого поєднання в реальному Rust-коді.

extern crate trpl; // for mdbook test

use std::{thread, time::Duration};

fn main() {
    let (tx, mut rx) = trpl::channel();

    thread::spawn(move || {
        for i in 1..11 {
            tx.send(i).unwrap();
            thread::sleep(Duration::from_secs(1));
        }
    });

    trpl::block_on(async {
        while let Some(message) = rx.recv().await {
            println!("{message}");
        }
    });
}

Ми починаємо зі створення async-каналу, а потім створюємо thread, який заволодіває стороною відправника каналу за допомогою ключового слова move. Усередині thread ми надсилаємо числа від 1 до 10, засинаючи на секунду між кожним. Нарешті, ми запускаємо future, створений за допомогою async-блоку, переданого до trpl::block_on, так само, як ми робили протягом цього розділу. У цьому future ми чекаємо ці повідомлення, так само, як і в інших прикладах передавання повідомлень, які ми бачили.

Повертаючись до сценарію, з якого ми почали розділ, уявіть собі виконання набору задач кодування відео з використанням окремого thread (оскільки кодування відео є обчислювально навантаженим), але сповіщення UI про те, що ці операції завершено, за допомогою async-каналу. Існує безліч прикладів таких поєднань у реальних сценаріях використання.

Підсумок

Це не останнє, що ви побачите про конкурентність у цій книзі. Проєкт у Розділі 21 застосує ці концепції в більш реалістичній ситуації, ніж простіші приклади, розглянуті тут, і безпосередніше порівняє розв’язання проблем за допомогою threads, порівняно з tasks і future.

Незалежно від того, який із цих підходів ви оберете, Rust надає вам інструменти, потрібні для написання безпечного, швидкого, конкурентного коду — чи то для вебсервера з високою пропускною здатністю, чи то для вбудованої операційної системи.

Далі ми поговоримо про ідіоматичні способи моделювання проблем і структурування розв’язків у міру того, як ваші Rust-програми ставатимуть більшими. Крім того, ми обговоримо, як ідіоми Rust пов’язані з тими, які вам можуть бути знайомі з об’єктно-орієнтованого програмування.