JavaRush /Курсы /C# SELF /Введение в многопоточность

Введение в многопоточность

C# SELF
55 уровень , 0 лекция
Открыта

1. Введение

Многопоточность — это как параллельная работа нескольких работников в офисе: один печатает документы, другой звонит клиенту, третий варит кофе (и, конечно, все они — программисты). Если бы у нас был только один работник, он делал бы всё по очереди, и офис бы захлебнулся от скуки и очереди за кофе. В программировании ситуация аналогична: однопоточная программа может выполнять только одну задачу за раз.

Представим, что наше приложение выполняет длительную операцию, например, скачивает файл из интернета или рассчитывает огромную таблицу. Всё остальное в этот момент "замораживается" — кнопки не нажимаются, анимация не двигается, слова "не отвечает" выводятся во всплывающем окне.

Многопоточность позволяет приложению делать несколько вещей одновременно: интерфейс остается отзывчивым, операции выполняются параллельно, и мы не срываемся на монолог в стиле "Компьютер, ты опять завис?!".

Основные понятия и терминология

Прежде чем окунуться с головой, разберёмся, что такое поток (thread) и чем он отличается от процесса.

  • Процесс (Process): Самостоятельная программа с собственным адресным пространством, переменными, ресурсами. Например, каждое запущенное приложение в Windows — отдельный процесс.
  • Поток (Thread): Единица выполнения внутри процесса. Процесс может содержать один или несколько потоков, которые работают с одними и теми же ресурсами (памятью, переменными).

Почему многопоточность вызывает столько вопросов?

Потому что потоки — это азартные ребята: они могут начать выполнение в любой момент, перемешать данные, прервать друг друга и вообще устроить беспредел в памяти, если не следить за порядком. Если кажется, что это похоже на детсад без воспитателя — совершенно верно! Дисциплина и забота о потоках — основа написания надёжных многопоточных программ.

2. История и роль многопоточности в C# и .NET

В далёкие времена C# был однопоточным, а программы — простыми. С ростом требований к производительности, появлением многоядерных процессоров и необходимостью создавать отзывчивые приложения без зависаний интерфейса, в .NET появились средства для многопоточности. Сначала это был классический System.Threading.Thread, позже появились задачи (Task), асинхронные методы (async/await), параллельная обработка данных (PLINQ) и высокоуровневые примитивы синхронизации.

C# вырос в мощную платформу, где многопоточность — не диковинка, а повседневность.

Визуально: процесс и потоки

Вот простая схема:


+--------------------------------------------------+
|                 Процесс (ваша программа)         |
|      +-------------+   +-------------+           |
|      |    Поток 1  |   |   Поток 2   |           |
|      +-------------+   +-------------+           |
|                ...                                 |
|      +-------------+                              |
|      |    Поток N  |                              |
|      +-------------+                              |
+--------------------------------------------------+

Все потоки внутри процесса видят общие переменные и ресурсы.

3. Как создать поток в C#?

Начнем с самого базового: класса Thread из пространства имен System.Threading.

Пример: Запускаем второй поток

Пусть у нас есть какая-то длительная задача — например, подсчёт суммы чисел от 1 до 10_000_000. Пока идет подсчёт, основной поток пусть пишет приветствие пользователю.


using System;
using System.Threading;

class Program
{
    // Метод для второй задачи
    static void CalculateSum()
    {
        long sum = 0;
        for (int i = 1; i <= 10_000_000; i++)
            sum += i;
        Console.WriteLine($"[Поток 2] Сумма: {sum}");
    }

    static void Main()
    {
        // Создаём поток, указываем делегат на метод
        Thread thread = new Thread(CalculateSum);
        
        thread.Start(); // Запускаем второй поток

        // Основной поток продолжает работать
        Console.WriteLine("[Поток 1] Привет! Работаем параллельно...");

        // Ждём завершения второго потока перед выходом
        thread.Join();

        Console.WriteLine("[Поток 1] Всё завершено!");
    }
}

Что произойдёт?
На экране появится строка "[Поток 1] Привет! Работаем параллельно...", а затем, когда поток с подсчётом закончит работу, он выведет сумму.

Типичная проблема: Кто первый, тот и вывел

Попробуйте несколько раз запустить этот код — порядок вывода строк может быть разным! Иногда сумма появляется первой, иногда приветствие. Это и есть настоящая многопоточность — программы становятся менее предсказуемыми, как настроение кота по понедельникам.

4. Зона памяти: что видят потоки?

Все потоки внутри процесса имеют доступ к одним и тем же переменным (если они не локальные для метода). Если изменить переменную в одном потоке — её увидят остальные!

Пример: Общая переменная


using System;
using System.Threading;

class Program
{
    static int counter = 0;

    static void Increment()
    {
        for (int i = 0; i < 1000; i++)
            counter++;
    }

    static void Main()
    {
        Thread t1 = new Thread(Increment);
        Thread t2 = new Thread(Increment);

        t1.Start();
        t2.Start();

        t1.Join();
        t2.Join();

        Console.WriteLine($"counter = {counter}");
    }
}

Сколько ожидаем увидеть в counter? Логика подсказывает 2000, ведь каждый поток увеличивает по 1000 раз.

А вот и нет! Запустите несколько раз — увидите разные значения: 1782, 1935, 1999…
Почему? Это классическая проблема Race Condition — потоки "перехватывают" друг у друга управление между чтением -> увеличением -> записью, и часть инкрементов теряется.

5. Полезные нюансы

Как потоки взаимодействуют с интерфейсом?

В современных desktop-приложениях (WinForms/WPF/MAUI) основной поток обслуживает графический интерфейс пользователя. Все действия пользователя (клики, ввод) — в этом потоке. Фоновые задачи должны выполняться в других потоках, но при этом, по правилам, нельзя напрямую "трогать" интерфейс из других потоков. Это сделано для предотвращения хаоса.

В консоли такого ограничения нет, можно писать в Console.WriteLine из любого потока. Однако в реальных приложениях без правильной синхронизации интерфейс может "поехать".

Последовательность и параллельность

Таблица — чтобы закрепить различия.

Однопоточный код Многопоточный код
Выполняет задачи по очереди Задачи могут выполняться одновременно
UI "висит" при долгих задачах UI остаётся отзывчивым
Просто читать и писать переменные Требует контроля доступа к данным
Отлаживается легко Может быть сложно отлаживать

Важные моменты при работе с потоками

  • Общие переменные — общий риск. Как уже показали выше, если несколько потоков используют общую переменную — без синхронизации возможны ошибки! (Подробнее в следующих лекциях.)
  • Поток можно "ждать" через Join(). Метод Join() позволяет "приостановить" основной поток до завершения фонового. Используйте его, если нужно дождаться результата.
  • Поток нельзя запустить дважды. После выполнения потока его нельзя снова стартовать — придётся создавать новый объект Thread.
  • Завершение потока. Поток завершается, когда заканчивается выполнение его метода. Принудительно "убивать" потоки — плохая идея (метод Abort() давно считается вредным и устаревшим).

Для чего нужна многопоточность на практике?

  • UI-приложения: не замораживать интерфейс, когда идет фоновая загрузка или вычисления.
  • Серверы и службы: одновременно обрабатывать запросы многих клиентов.
  • Высокопроизводительные вычисления: разбивать большую задачу (например, обработку миллионов записей) на части и выполнять их параллельно.
  • Игры, симуляции, обработка данных: моделировать сложные системы, не теряя в производительности.

Проблемы многопоточности

Многопоточность дает мощь, но и добавляет сложности:

  • Race Condition (состояние гонки): когда несколько потоков одновременно меняют одни и те же данные, результат зависит от порядка инструкций, который непредсказуем.
  • Deadlock (взаимная блокировка): потоки ждут друг друга, и никто не может продолжить работу.
  • Starvation (голодание): один из потоков постоянно "обделяют" доступом к ресурсу.

В этой лекции мы только упомянули основные проблемы многопоточности, а в следующих научимся их распознавать и избегать. Пока — просто запомните, что если всё "зависло", виноваты могут быть не только баги, но и потоки, которые "устроили вечеринку".

2
Задача
C# SELF, 55 уровень, 0 лекция
Недоступна
Создание и запуск потока
Создание и запуск потока
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ