- PVSM.RU - https://www.pvsm.ru -
В последнее время мне довелось много заниматься распараллеливанием однопоточного кода и показалось уместным свести воедино более-менее всё, что нам известно про разные типы многозадачности, с примерами и комментариями.
Представьте себе коммунальную кухню в советской квартире. Шесть конфорок, двенадцать жильцов, и у каждого — неотложная потребность сварить борщ именно сейчас. Вопрос распределения ресурсов встаёт ребром, причём ребром острым, способным поранить неосторожного соседа.
В мире вычислительной техники роль коммунальной кухни исполняет процессор, а жильцами выступают процессы и потоки, каждый из которых свято убеждён в собственной исключительной важности. И точно так же, как в коммуналке, существует два принципиально разных подхода к решению проблемы: либо назначить коменданта с секундомером, который будет безжалостно сгонять зазевавшихся с плиты, либо положиться на сознательность граждан и их готовность добровольно уступать место проголодавшимся соседям.
Первый подход называется вытесняющей многозадачностью (preemptive multitasking), второй — кооперативной (cooperative multitasking). И если вы думаете, что выбор между ними очевиден, то позвольте вас разочаровать: история вычислительной техники полна примеров того, как оба подхода приводили к катастрофическим последствиям, просто разными путями.
Кооперативная многозадачность — это система, построенная на вере в человеческую (точнее, программистскую) порядочность. Каждая задача получает процессор и держит его до тех пор, пока сама, добровольно, по велению совести и здравого смысла, не решит отдать управление другим. Звучит утопично? Так оно и есть.
В Ruby, языке, который исторически славится своей элегантностью и полным пренебрежением к производительности, кооперативная многозадачность реализована через файберы (Fiber):
# Классический пример кооперативной многозадачности в Ruby
# Два файбера, мирно делящих процессорное время
fiber_a = Fiber.new do
3.times do |i|
puts "Файбер A: итерация #{i}"
puts "Файбер A: добровольно уступаю место"
Fiber.yield # Вот оно — добровольное отречение от власти
end
"Файбер A завершён"
end
fiber_b = Fiber.new do
3.times do |i|
puts "Файбер B: итерация #{i}"
puts "Файбер B: я тоже умею делиться"
Fiber.yield
end
"Файбер B завершён"
end
# Дирижёр, управляющий оркестром
6.times do
fiber_a.resume if fiber_a.alive?
fiber_b.resume if fiber_b.alive?
end
▸ попробовать вживую [1]
Обратите внимание на Fiber.yield — это момент истины, точка, в которой задача добровольно отдаёт бразды правления. Без этого вызова файбер будет крутиться вечно, как пластинка, застрявшая на одной дорожке, и никакая сила в мире не заставит его остановиться. Кроме, разумеется, завершения программы или отключения электричества.
В Go ситуация несколько сложнее. Язык позиционируется как современный и прогрессивный, но под капотом горутины (goroutines) демонстрируют черты обоих подходов. До версии 1.14 Go использовал кооперативную модель:
package main
import (
"fmt"
"runtime"
)
func cooperativeTask(name string) {
for i := 0; i < 5; i++ {
fmt.Printf("%s: выполняю работу %dn", name, i)
// Явная точка кооперации — без неё в старых версиях Go
// другие горутины могли голодать до смерти
runtime.Gosched()
}
}
func main() {
// Ограничиваем до одного процессора для наглядности
runtime.GOMAXPROCS(1)
go cooperativeTask("Горутина Alpha")
go cooperativeTask("Горутина Beta")
go cooperativeTask("Горутина Gamma")
// Ждём завершения (грубо, но наглядно)
var input string
fmt.Scanln(&input)
}
▸ попробовать вживую [2]
Вызов runtime.Gosched() — это эквивалент того самого добровольного шага в сторону. Программист явно говорит: «Я, конечно, мог бы ещё поработать, но давайте дадим шанс другим». Благородно? Безусловно. Надёжно? Как карточный домик на сквозняке.
Главная беда кооперативной многозадачности — это программист, забывший (или намеренно отказавшийся) вызвать точку кооперации. Рассмотрим патологический случай:
# Файбер-эгоист, пожирающий все ресурсы
selfish_fiber = Fiber.new do
counter = 0
loop do
counter += 1
# Бесконечный цикл без Fiber.yield
# Остальные файберы могут сушить вёсла
puts "Эгоист: #{counter}" if counter % 1_000_000 == 0
end
end
poor_fiber = Fiber.new do
puts "Бедняга: я когда-нибудь выполнюсь?"
# Спойлер: нет, не выполнится
end
selfish_fiber.resume # Всё, приехали
# poor_fiber.resume никогда не будет вызван
В Java, где потоки традиционно работают в вытесняющем режиме, можно симулировать кооперативное поведение через Thread.yield():
public class CooperativeSimulation {
static volatile boolean running = true;
public static void main(String[] args) throws InterruptedException {
Thread politeThread = new Thread(() -> {
while (running) {
System.out.println("Вежливый поток: работаю немного");
doSomeWork(1000);
Thread.yield(); // Кооперативная уступка
}
}, "PoliteThread");
Thread rudeThread = new Thread(() -> {
while (running) {
System.out.println("Грубый поток: работаю МНОГО");
doSomeWork(100_000_000); // Тяжёлая работа
// Никакого yield — пусть другие подождут
}
}, "RudeThread");
politeThread.start();
rudeThread.start();
Thread.sleep(5000);
running = false;
}
static void doSomeWork(int iterations) {
double result = 0;
for (int i = 0; i < iterations; i++) {
result += Math.sin(i) * Math.cos(i);
}
}
}
▸ попробовать вживую [3] — чтобы увидеть блокировку, нужно ограничить виртуальную машину одним процессором, или запустить много rudeThread, причем лучше всего, если это будет Java8.
Потому что Thread.yield() в Java — это лишь подсказка планировщику, а не приказ. JVM может проигнорировать её с олимпийским спокойствием, особенно на современных многоядерных системах, где вытесняющий планировщик операционной системы всё равно возьмёт своё.
Вытесняющая многозадачность — это когда операционная система, подобно строгому учителю с секундомером, даёт каждой задаче определённый квант времени. Истёк квант — будь добр освободить место, независимо от того, закончил ты свои дела или нет. Никаких «ещё пять минуточек», никаких «я почти доделал».
Современные операционные системы — Windows, Linux, macOS — все используют вытесняющую многозадачность. И на то есть веские причины: система не должна зависеть от порядочности отдельных программ. Один зависший процесс не должен утащить за собой в небытие всю систему.
В современных виртуальных машинах Java потоки изначально работают в вытесняющем режиме:
public class PreemptiveDemo {
public static void main(String[] args) {
// Создаём потоки с разными приоритетами
Thread highPriority = new Thread(() -> {
long count = 0;
while (count < 1_000_000_000L) {
count++;
// Никаких yield, никакой кооперации
// Планировщик сам разберётся
}
System.out.println("Высокий приоритет: " + count);
});
highPriority.setPriority(Thread.MAX_PRIORITY);
Thread lowPriority = new Thread(() -> {
long count = 0;
while (count < 1_000_000_000L) {
count++;
}
System.out.println("Низкий приоритет: " + count);
});
lowPriority.setPriority(Thread.MIN_PRIORITY);
// Оба стартуют одновременно
highPriority.start();
lowPriority.start();
// Планировщик ОС распределит время между ними
// Высокий приоритет получит больше, но низкий не умрёт с голоду
}
}
В Go начиная с версии 1.14 горутины также получили настоящую вытесняющую многозадачность:
package main
import (
"fmt"
"runtime"
"sync"
"time"
)
func cpuBoundTask(id int, wg *sync.WaitGroup) {
defer wg.Done()
start := time.Now()
var counter int64
// Чистые вычисления, никаких точек кооперации
for counter < 1_000_000_000 {
counter++
}
elapsed := time.Since(start)
fmt.Printf("Горутина %d: завершена за %vn", id, elapsed)
}
func main() {
// Даже с одним процессором горутины будут вытесняться
runtime.GOMAXPROCS(1)
var wg sync.WaitGroup
// Запускаем несколько CPU-bound горутин
for i := 1; i <= 4; i++ {
wg.Add(1)
go cpuBoundTask(i, &wg)
}
// Эта горутина тоже получит процессорное время
// благодаря асинхронному вытеснению
go func() {
for i := 0; i < 10; i++ {
fmt.Println("Фоновая задача: я жива!")
time.Sleep(100 * time.Millisecond)
}
}()
wg.Wait()
}
▸ посмотреть вживую [4]
Вытесняющая многозадачность не бесплатна. Каждое переключение контекста — это сохранение состояния текущей задачи, загрузка состояния следующей, сброс кэшей процессора и прочие накладные расходы. На современных системах это микросекунды, но в высоконагруженных приложениях они складываются в ощутимые потери.
По крайней мере, так написано в книжках. На практике мне не удалось зафиксировать сколько-нибудь значимые потери.
package main
import (
"fmt"
"runtime"
"sync"
"time"
)
func measureContextSwitchOverhead() {
const iterations = 1_000_000
// Вариант 1: последовательное выполнение
start := time.Now()
sum := uint64(0)
for i := uint64(0); i < iterations; i++ {
sum += i
}
sequential := time.Since(start)
// Вариант 2: параллельное выполнение с множеством горутин
runtime.GOMAXPROCS(8) // Форсируем переключения
start = time.Now()
var wg sync.WaitGroup
var mu sync.Mutex
parallelSum := uint64(0)
for i := uint64(0); i < iterations; i++ {
wg.Add(1)
go func(n uint64) {
defer wg.Done()
mu.Lock()
parallelSum += n
mu.Unlock()
}(i)
}
wg.Wait()
parallel := time.Since(start)
fmt.Printf("Последовательно: %vn", sequential.Nanoseconds())
fmt.Printf("Параллельно (1 CPU): %vn", parallel.Nanoseconds())
fmt.Printf("Накладные расходы: %.2fxn", float64(parallel)/float64(sequential))
}
func main() {
measureContextSwitchOverhead()
}
▸ посмотреть вживую [5]
Реальный мир редко бывает чёрно-белым, и многозадачность не исключение. Современные системы часто используют гибридные подходы, сочетающие элементы обоих методов.
Ruby с его EventMachine, Node.JS с его event loop, Python с asyncio — все они представляют собой вариации на тему кооперативной многозадачности, работающей внутри одного потока операционной системы. При этом сам поток управляется вытесняющим планировщиком ОС.
require 'async'
# Современный Ruby с async gem
# Кооперативная многозадачность внутри event loop
Async do |task|
# Задача 1: HTTP-запрос (имитация)
subtask1 = task.async do
puts "Задача 1: начинаю запрос"
sleep 2 # Async::Task.current.sleep под капотом
puts "Задача 1: получен ответ"
"Результат 1"
end
# Задача 2: работа с файлом (имитация)
subtask2 = task.async do
puts "Задача 2: читаю файл"
sleep 1
puts "Задача 2: файл прочитан"
"Результат 2"
end
# Задача 3: вычисления
subtask3 = task.async do
puts "Задача 3: считаю"
sleep 0.5
puts "Задача 3: посчитал"
"Результат 3"
end
# Все задачи выполняются конкурентно в одном потоке
results = [subtask1.wait, subtask2.wait, subtask3.wait]
puts "Все результаты: #{results}"
end
▸ вживую мне не известна ни одна песочница, позволяющая инсталлировать чужие джемы, поэтому вот картинка:
Задача 1: начинаю запрос
Задача 2: читаю файл
Задача 3: считаю
Задача 3: посчитал
Задача 2: файл прочитан
Задача 1: получен ответ
Все результаты: ["Результат 1", "Результат 2", "Результат 3"]
=> #<Async::Task:0x0000000000000168>
Ключевой момент: sleep в контексте async — это не блокирующий вызов, а точка кооперации. Задача говорит: «Мне нужно подождать, пусть пока другие поработают». Это элегантно, эффективно и… довольно хрупко.
Go с его горутинами, Erlang с его процессами, Java с виртуальными потоками (Project Loom [6]) — все они реализуют концепцию «зелёных потоков»: множество легковесных задач, мультиплексируемых на меньшее количество системных потоков.
// Java 21+ с виртуальными потоками (Project Loom)
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.time.Duration;
import java.time.Instant;
public class VirtualThreadsDemo {
public static void main(String[] args) throws Exception {
final int TASK_COUNT = 1000;
// Старый способ: пул обычных потоков
Instant start = Instant.now();
try (ExecutorService executor = Executors.newFixedThreadPool(100)) {
for (int i = 0; i < TASK_COUNT; i++) {
final int taskId = i;
executor.submit(() -> {
try {
Thread.sleep(100); // Имитация I/O
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return taskId;
});
}
}
Duration platformDuration = Duration.between(start, Instant.now());
// Новый способ: виртуальные потоки
start = Instant.now();
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < TASK_COUNT; i++) {
final int taskId = i;
executor.submit(() -> {
try {
Thread.sleep(100); // То же самое
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return taskId;
});
}
}
Duration virtualDuration = Duration.between(start, Instant.now());
System.out.printf("Обычные потоки: %s%n", platformDuration);
System.out.printf("Виртуальные потоки: %s%n", virtualDuration);
}
}
▸ результат выполнения:
jshell> VirtualThreadsDemo.main(new String[0])
Обычные потоки: PT1.012539805S
Виртуальные потоки: PT0.115042997S
Виртуальные потоки Java — это кооперативная многозадачность, замаскированная под привычный API потоков. Блокирующий вызов Thread.sleep() превращается в точку кооперации, позволяя планировщику переключиться на другой виртуальный поток.
Представьте ситуацию: высокоприоритетная задача ждёт ресурс, захваченный низкоприоритетной. Средний приоритет вытесняет низкий. В итоге высокий приоритет ждёт среднего, который ждёт низкого. Это называется инверсией приоритетов, и это классическая проблема вытесняющей многозадачности.
import java.util.concurrent.locks.ReentrantLock;
public class PriorityInversionDemo {
private static final ReentrantLock sharedResource = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
// Низкоприоритетный поток захватывает ресурс
Thread lowPriority = new Thread(() -> {
sharedResource.lock();
try {
System.out.println("Низкий: захватил ресурс");
// Долгая работа
simulateCpuWork(2000);
System.out.println("Низкий: освобождаю ресурс");
} finally {
sharedResource.unlock();
}
}, "Low");
lowPriority.setPriority(Thread.MIN_PRIORITY);
// Высокоприоритетный поток хочет тот же ресурс
Thread highPriority = new Thread(() -> {
try { Thread.sleep(100); } catch (InterruptedException e) {}
System.out.println("Высокий: хочу ресурс");
sharedResource.lock();
try {
System.out.println("Высокий: получил ресурс!");
} finally {
sharedResource.unlock();
}
}, "High");
highPriority.setPriority(Thread.MAX_PRIORITY);
// Средний приоритет — вычислительная задача
Thread mediumPriority = new Thread(() -> {
try { Thread.sleep(50); } catch (InterruptedException e) {}
System.out.println("Средний: начинаю вычисления");
simulateCpuWork(1000);
System.out.println("Средний: закончил");
}, "Medium");
mediumPriority.setPriority(Thread.NORM_PRIORITY);
lowPriority.start();
highPriority.start();
mediumPriority.start();
lowPriority.join();
highPriority.join();
mediumPriority.join();
}
static void simulateCpuWork(int millis) {
long start = System.currentTimeMillis();
while (System.currentTimeMillis() - start < millis) {
Math.random();
}
}
}
▸ результат выполнения:
jshell> PriorityInversionDemo.main(new String[0])
Низкий: захватил ресурс
Средний: начинаю вычисления
Высокий: хочу ресурс
Средний: закончил
Низкий: освобождаю ресурс
Высокий: получил ресурс!
Mars Pathfinder, космический аппарат NASA, пострадал от этой проблемы в 1997 году. Система периодически перезагружалась из-за инверсии приоритетов между задачей сбора данных и информационной шиной. Решение нашли дистанционно, включив наследование приоритетов в VxWorks. История поучительная: даже космические инженеры иногда забывают о подводных камнях многозадачности.
Вытесняющая многозадачность приносит с собой недетерминизм. Вы никогда точно не знаете, в какой момент планировщик решит переключить контекст. Это создаёт благодатную почву для гонок данных:
package main
import (
"fmt"
"sync"
)
// Классическая гонка данных
func raceConditionDemo() {
counter := 0
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
// Это не атомарная операция!
// 1. Прочитать counter
// 2. Увеличить значение
// 3. Записать обратно
// Планировщик может вытеснить на любом шаге
counter++
}()
}
wg.Wait()
fmt.Printf("Ожидали 1000, получили: %dn", counter)
}
// Исправленная версия
func fixedDemo() {
var counter int64
var wg sync.WaitGroup
var mu sync.Mutex
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
mu.Lock()
counter++
mu.Unlock()
}()
}
wg.Wait()
fmt.Printf("С мьютексом: %dn", counter)
}
func main() {
raceConditionDemo()
fixedDemo()
}
▸ посмотреть вживую [7]
В кооперативной многозадачности гонки данных встречаются реже — вы контролируете точки переключения. Но «реже» не означает «никогда». Один забытый await, и хаос врывается в вашу упорядоченную вселенную.
Классическая проблема обедающих философов демонстрирует, как легко создать ситуацию взаимной блокировки:
require 'thread'
# Проблема обедающих философов
# Пять философов, пять вилок, бесконечные размышления и еда
class DiningPhilosophers
def initialize(count)
@count = count
@forks = Array.new(count) { Mutex.new }
@philosophers = []
end
def start_deadlock_prone
@count.times do |i|
@philosophers << Thread.new do
loop do
think(i)
# Каждый берёт сначала левую, потом правую вилку
# Это прямой путь к deadlock
left_fork = @forks[i]
right_fork = @forks[(i + 1) % @count]
left_fork.synchronize do
puts "Философ #{i}: взял левую вилку"
sleep(0.1) # Окно для deadlock
right_fork.synchronize do
puts "Философ #{i}: взял правую вилку, ем"
eat(i)
end
end
end
end
end
@philosophers.each(&:join)
end
def start_safe
@count.times do |i|
@philosophers << Thread.new do
loop do
think(i)
# Решение: всегда брать вилки в порядке возрастания номера
first = [i, (i + 1) % @count].min
second = [i, (i + 1) % @count].max
@forks[first].synchronize do
@forks[second].synchronize do
puts "Философ #{i}: ем"
eat(i)
end
end
end
end
end
@philosophers.each(&:join)
end
private
def think(id)
puts "Философ #{id}: размышляю о бренности бытия"
sleep(rand * 0.5)
end
def eat(id)
sleep(rand * 0.3)
end
end
# dinner = DiningPhilosophers.new(5)
# dinner.start_deadlock_prone # Не запускайте, если цените своё время
▸ если все-таки запустить, никакие Ctrl+C не помогут:
…
Философ 3: взял правую вилку, ем
[6] pry(main)> Философ 1: взял левую вилку
Философ 3: размышляю о бренности бытия
Философ 2: взял правую вилку, ем
[6] pry(main)> Философ 4: взял левую вилку
Философ 0: взял левую вилку
[6] pry(main)> Философ 2: размышляю о бренности бытия
Философ 3: взял левую вилку
Философ 1: взял правую вилку, ем
…
Если deadlock — это когда все стоят и ждут, то livelock — это когда все активно работают, но ничего не происходит. Как два вежливых человека в коридоре, бесконечно уступающих друг другу дорогу:
package main
import (
"fmt"
"sync"
"time"
)
type PoliteWorker struct {
name string
working bool
mu sync.Mutex
}
func (w *PoliteWorker) Work(other *PoliteWorker) {
for i := 0; i < 10; i++ {
w.mu.Lock()
// Проверяем, не работает ли другой
other.mu.Lock()
if other.working {
// Слишком вежливы — уступаем
fmt.Printf("%s: вижу, что %s работает, уступаюn", w.name, other.name)
other.mu.Unlock()
w.mu.Unlock()
time.Sleep(time.Millisecond) // И снова пробуем
continue
}
other.mu.Unlock()
// Начинаем работать
w.working = true
fmt.Printf("%s: работаю!n", w.name)
time.Sleep(10 * time.Millisecond)
w.working = false
w.mu.Unlock()
}
}
func main() {
alice := &PoliteWorker{name: "Алиса"}
bob := &PoliteWorker{name: "Борис"}
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
alice.Work(bob)
}()
go func() {
defer wg.Done()
bob.Work(alice)
}()
wg.Wait()
}
▸ посмотреть вживую [8]
I/O-bound задачи: Если ваша программа бо́льшую часть времени ждёт ответа от базы данных, сети или файловой системы, кооперативная многозадачность — отличный выбор. Накладные расходы минимальны, код остаётся предсказуемым.
require 'async'
require 'async/http/internet'
Async do
internet = Async::HTTP::Internet.new
urls = %w[
https://example.com
https://example.org
https://example.net
]
# Все запросы выполняются конкурентно
# Кооперативное переключение на await
tasks = urls.map do |url|
Async do
response = internet.get(url)
puts "#{url}: #{response.status}"
response.finish
end
end
tasks.each(&:wait)
ensure
internet&.close
end
Игры и симуляции: Когда вам нужен детерминизм и воспроизводимость. В игре важно, чтобы каждый кадр обрабатывался одинаково, независимо от загрузки системы.
Встроенные системы с ограниченными ресурсами: Когда каждый байт памяти на счету, и вы не можете позволить себе накладные расходы вытесняющего планировщика.
CPU-bound задачи: Если задача интенсивно использует процессор, вытесняющая многозадачность гарантирует, что другие задачи тоже получат своё время.
import java.util.concurrent.*;
import java.util.stream.*;
public class CpuBoundTasks {
public static void main(String[] args) throws Exception {
ExecutorService executor = Executors.newFixedThreadPool(
Runtime.getRuntime().availableProcessors()
);
// CPU-bound задачи: вычисление простых чисел
List<Future<Long>> futures = IntStream.range(0, 8)
.mapToObj(i -> executor.submit(() -> countPrimes(i * 1_000_000, (i + 1) * 1_000_000)))
.collect(Collectors.toList());
long total = 0;
for (Future<Long> future : futures) {
total += future.get();
}
System.out.printf("Всего простых чисел: %d%n", total);
executor.shutdown();
}
static long countPrimes(int from, int to) {
return IntStream.range(from, to)
.filter(CpuBoundTasks::isPrime)
.count();
}
static boolean isPrime(int n) {
if (n < 2) return false;
for (int i = 2; i <= Math.sqrt(n); i++) {
if (n % i == 0) return false;
}
return true;
}
}
Ненадёжный код: Когда вы запускаете код, которому не доверяете (плагины, пользовательские скрипты), вытесняющая многозадачность защитит систему от зависаний.
Интерактивные приложения: Пользовательский интерфейс должен отзываться на действия пользователя, даже если фоновые задачи выполняют тяжёлые вычисления.
Современные приложения часто комбинируют оба подхода:
package main
import (
"context"
"fmt"
"runtime"
"sync"
"time"
)
type HybridScheduler struct {
cpuWorkers int
ioWorkers int
cpuTasks chan func()
ioTasks chan func()
wg sync.WaitGroup
}
func NewHybridScheduler() *HybridScheduler {
s := &HybridScheduler{
cpuWorkers: runtime.NumCPU(),
ioWorkers: 100, // Много легковесных для I/O
cpuTasks: make(chan func(), 100),
ioTasks: make(chan func(), 1000),
}
// CPU-воркеры: по одному на ядро
for i := 0; i < s.cpuWorkers; i++ {
s.wg.Add(1)
go s.cpuWorker()
}
// I/O-воркеры: много легковесных
for i := 0; i < s.ioWorkers; i++ {
s.wg.Add(1)
go s.ioWorker()
}
return s
}
func (s *HybridScheduler) cpuWorker() {
defer s.wg.Done()
for task := range s.cpuTasks {
task()
}
}
func (s *HybridScheduler) ioWorker() {
defer s.wg.Done()
for task := range s.ioTasks {
task()
}
}
func (s *HybridScheduler) SubmitCPU(task func()) {
s.cpuTasks <- task
}
func (s *HybridScheduler) SubmitIO(task func()) {
s.ioTasks <- task
}
func (s *HybridScheduler) Shutdown() {
close(s.cpuTasks)
close(s.ioTasks)
s.wg.Wait()
}
func main() {
scheduler := NewHybridScheduler()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
var resultsWg sync.WaitGroup
// CPU-bound задачи
for i := 0; i < 10; i++ {
resultsWg.Add(1)
taskID := i
scheduler.SubmitCPU(func() {
defer resultsWg.Done()
result := heavyComputation(taskID)
fmt.Printf("CPU задача %d: результат %dn", taskID, result)
})
}
// I/O-bound задачи
for i := 0; i < 50; i++ {
resultsWg.Add(1)
taskID := i
scheduler.SubmitIO(func() {
defer resultsWg.Done()
simulateIO(taskID)
fmt.Printf("I/O задача %d: завершенаn", taskID)
})
}
done := make(chan struct{})
go func() {
resultsWg.Wait()
close(done)
}()
select {
case <-done:
fmt.Println("Все задачи завершены")
case <-ctx.Done():
fmt.Println("Таймаут")
}
scheduler.Shutdown()
}
func heavyComputation(id int) int {
sum := 0
for i := 0; i < 10_000_000; i++ {
sum += i % (id + 1)
}
return sum
}
func simulateIO(id int) {
time.Sleep(time.Duration(50+id%50) * time.Millisecond)
}
Выбор между вытесняющей и кооперативной многозадачностью — это не вопрос «что лучше», а вопрос «что подходит для данной задачи». Кооперативная многозадачность проще в реализации, предсказуема и эффективна для I/O-bound нагрузок. Вытесняющая многозадачность устойчива к ошибкам программиста, справедлива к ресурсам и необходима для CPU-bound задач.
Современные языки и runtime всё чаще предлагают гибридные решения: горутины Go, виртуальные потоки Java, async/await в Ruby — все они пытаются совместить простоту кооперативной модели с надёжностью вытесняющей.
Но какую бы модель мы ни выбрали, надо помнить: конкурентность — это не два байта переслать. Гонки данных, взаимные блокировки, инверсия приоритетов — эти проблемы не исчезают с выбором правильной модели многозадачности. Они лишь принимают разные формы, подобно многоголовой гидре, у которой на месте отрубленной головы вырастают две новые.
Единственное надёжное средство — это понимание. Понимание того, как работает ваш планировщик, где находятся точки переключения, какие данные разделяются между задачами. И, конечно, тесты. Много тестов. Очень много тестов.
А ещё — здоровая паранойя. Потому что в мире конкурентного программирования то, что может пойти не так, непременно пойдёт не так. Обычно в пятницу вечером.
Автор: oeditus
Источник [9]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/mnogopotochnost/445074
Ссылки в тексте:
[1] попробовать вживую: https://try.ruby-lang.org/playground/#code=fiber_a+%3D+Fiber.new+do%0A++3.times+do+%7Ci%7C%0A++++puts+%22%D0%A4%D0%B0%D0%B9%D0%B1%D0%B5%D1%80+A%3A+%D0%B8%D1%82%D0%B5%D1%80%D0%B0%D1%86%D0%B8%D1%8F+%23%7Bi%7D%22%0A++++puts+%22%D0%A4%D0%B0%D0%B9%D0%B1%D0%B5%D1%80+A%3A+%D0%B4%D0%BE%D0%B1%D1%80%D0%BE%D0%B2%D0%BE%D0%BB%D1%8C%D0%BD%D0%BE+%D1%83%D1%81%D1%82%D1%83%D0%BF%D0%B0%D1%8E+%D0%BC%D0%B5%D1%81%D1%82%D0%BE%22%0A++++Fiber.yield++%23+%D0%92%D0%BE%D1%82+%D0%BE%D0%BD%D0%BE+%E2%80%94+%D0%B4%D0%BE%D0%B1%D1%80%D0%BE%D0%B2%D0%BE%D0%BB%D1%8C%D0%BD%D0%BE%D0%B5+%D0%BE%D1%82%D1%80%D0%B5%D1%87%D0%B5%D0%BD%D0%B8%D0%B5+%D0%BE%D1%82+%D0%B2%D0%BB%D0%B0%D1%81%D1%82%D0%B8%0A++end%0A++%22%D0%A4%D0%B0%D0%B9%D0%B1%D0%B5%D1%80+A+%D0%B7%D0%B0%D0%B2%D0%B5%D1%80%D1%88%D1%91%D0%BD%22%0Aend%0A%0Afiber_b+%3D+Fiber.new+do%0A++3.times+do+%7Ci%7C%0A++++puts+%22%D0%A4%D0%B0%D0%B9%D0%B1%D0%B5%D1%80+B%3A+%D0%B8%D1%82%D0%B5%D1%80%D0%B0%D1%86%D0%B8%D1%8F+%23%7Bi%7D%22%0A++++puts+%22%D0%A4%D0%B0%D0%B9%D0%B1%D0%B5%D1%80+B%3A+%D1%8F+%D1%82%D0%BE%D0%B6%D0%B5+%D1%83%D0%BC%D0%B5%D1%8E+%D0%B4%D0%B5%D0%BB%D0%B8%D1%82%D1%8C%D1%81%D1%8F%22%0A++++Fiber.yield%0A++end%0A++%22%D0%A4%D0%B0%D0%B9%D0%B1%D0%B5%D1%80+B+%D0%B7%D0%B0%D0%B2%D0%B5%D1%80%D1%88%D1%91%D0%BD%22%0Aend%0A%0A%23+%D0%94%D0%B8%D1%80%D0%B8%D0%B6%D1%91%D1%80%2C+%D1%83%D0%BF%D1%80%D0%B0%D0%B2%D0%BB%D1%8F%D1%8E%D1%89%D0%B8%D0%B9+%D0%BE%D1%80%D0%BA%D0%B5%D1%81%D1%82%D1%80%D0%BE%D0%BC%0A6.times+do%0A++fiber_a.resume+if+fiber_a.alive%3F%0A++fiber_b.resume+if+fiber_b.alive%3F%0Aend&engine=cruby-4.0.0
[2] попробовать вживую: https://go.dev/play/p/zKGDbm9MjQr
[3] попробовать вживую: https://www.onlineide.pro/playground/share/8ce4733b-90cd-486f-af7c-620ca8d721f3
[4] посмотреть вживую: https://go.dev/play/p/nUqI49X7CyB
[5] посмотреть вживую: https://go.dev/play/p/vwBTo-RKx1d
[6] Project Loom: https://habr.com/ru/articles/990356/
[7] посмотреть вживую: https://go.dev/play/p/qPDExnDh7bi
[8] посмотреть вживую: https://go.dev/play/p/JSry07Bag0P
[9] Источник: https://habr.com/ru/articles/1000924/?utm_source=habrahabr&utm_medium=rss&utm_campaign=1000924
Нажмите здесь для печати.