Форум программистов, компьютерный форум, киберфорум
UnmanagedCoder
Войти
Регистрация
Восстановить пароль
Блоги Сообщество Поиск Заказать работу  

Многопоточность в C#: Класс Thread

Запись от UnmanagedCoder размещена 24.03.2025 в 08:20
Показов 6575 Комментарии 0

Нажмите на изображение для увеличения
Название: 26d86b62-836e-4c19-997e-c4a844f3ec14.jpg
Просмотров: 305
Размер:	203.8 Кб
ID:	10486
Когда запускается приложение на компьютере, операционная система создаёт для него процесс - виртуальное адресное пространство. В C# этот процесс изначально получает один поток выполнения — главный поток, который начинает работу с метода Main(). Но что если нужно выполнять несколько задач одновременно?

В C# потоки представлены классом Thread, входящим в пространство имен System.Threading. Каждый поток имеет доступ ко всем данным процесса (точнее, к данным домена приложения этого процесса). При этом каждый поток выполняется независимо, со своим стеком вызовов и локальными переменными.

Когда использовать многопоточность? Есть несколько сценариев:
1. Масштабируемость (параллельное выполнение): когда у вас есть длительные операции, зависящие от процессора (CPU-bound), например, вычисление простых чисел или обработка большого массива данных. Для таких задач можно распараллелить вычисления на несколько потоков.
2. Отзывчивость UI: выполнение "тяжёлых" операций в отдельных потоках позволяет сохранить отзывчивость интерфейса, особенно в клиентских приложениях.
3. Асинхронная обработка: когда операции зависят от ввода-вывода (I/O-bound), например, чтение данных с диска или сети. Здесь многопоточность позволяет не блокировать выполнение программы, пока ожидается завершение операции.

В современном C# асинхронность часто обеспечивается не через прямое использование потоков, а через ключевые слова async/await, которые предоставляют более высокоуровневый подход.

Правило большого пальца: используйте потоки для операций, зависящих от CPU, и асинхронные методы для операций, зависящих от I/O. Для клиентских приложений подходят оба варианта, а для серверных рекомендуется асинхронный подход.

У многопоточного программирования есть и недостатки: код становится сложнее для понимания, отладки и тестирования, возникают проблемы с доступом к общим ресурсам, требуется синхронизация потоков. Кроме того, в однопроцессорных системах использование потоков может даже замедлить выполнение из-за накладных расходов на переключение контекста. Но с правильным подходом многопоточность может значительно улучшить производительность и отзывчивость приложений, особенно на современных многоядерных системах.

Класс Thread и его основы



Класс Thread — это фундамент многопоточного программирования в C#. Он находится в пространстве имен System.Threading и предоставляет все необходимые инструменты для создания и управления новыми потоками выполнения. Начнем с создания нового потока. Это делается в два этапа: сначала мы создаем экземпляр класса Thread, передавая ему делегат, указывающий на метод, который должен выполняться в новом потоке, а затем вызываем метод Start() для запуска потока:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
using System;
using System.Threading;
 
public class Program
{
    public static void Main()
    {
        // Создаем экземпляр Thread и передаем ему метод для выполнения
        Thread thread = new Thread(WorkerMethod);
        
        // Запускаем поток
        thread.Start();
        
        // Этот код выполняется в главном потоке параллельно с WorkerMethod
        Console.WriteLine("Главный поток продолжает работу!");
        Console.ReadLine();
    }
    
    private static void WorkerMethod()
    {
        // Код, который будет выполняться в отдельном потоке
        Console.WriteLine("Привет из отдельного потока!");
    }
}
В этом примере метод WorkerMethod будет выполняться в отдельном потоке, в то время как основной поток продолжит свое выполнение. Обратите внимание, что порядок вывода строк может меняться при каждом запуске программы, так как потоки выполняются независимо друг от друга. Конструктор класса Thread принимает два типа делегатов:

1. ThreadStart — делегат без параметров:
C#
1
   public delegate void ThreadStart();
2. ParameterizedThreadStart — делегат с одним параметром типа object:
C#
1
   public delegate void ParameterizedThreadStart(object obj);
Если ваш метод должен принимать аргументы, используйте второй вариант:

C#
1
2
3
4
5
6
7
8
9
10
Thread thread = new Thread(ParameterizedMethod);
thread.Start("Привет, поток!"); // Передаем строку в качестве параметра
 
// ...
 
private static void ParameterizedMethod(object message)
{
    string msg = message as string;
    Console.WriteLine(msg);
}
При передаче параметра важно помнить две вещи. Во-первых, вы можете передать только один параметр. Если вам нужно передать несколько значений, используйте специальный класс или структуру. Во-вторых, параметр должен быть типа object, поэтому вам придется выполнить приведение типа в вашем методе.

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

C#
1
bool isRunning = thread.IsAlive;
А также дождаться завершения потока с помощью метода Join():

C#
1
thread.Join(); // Блокирует вызывающий поток до завершения thread
Метод Join очень полезен, когда нам нужно дождаться результатов выполнения потока перед продолжением. Мы также можем указать максимальное время ожидания:

C#
1
bool completed = thread.Join(1000); // Ждем не более одной секунды
Потоки в C# бывают двух типов: обычные (foreground) и фоновые (background). По умолчанию создается обычный поток. Разница между ними заключается в их влиянии на жизненный цикл приложения:
  • Обычный поток предотвращает завершение приложения, даже если все остальные потоки (включая главный) завершили работу.
  • Фоновый поток автоматически завершается, когда все обычные потоки завершают работу.

Чтобы создать фоновый поток, установите свойство IsBackground в true:

C#
1
thread.IsBackground = true;
Это важно учитывать при разработке приложений, особенно если вы создаете потоки для длительных операций, которые не должны блокировать завершение приложения.
Еще одна важная возможность — именование потоков, что может значительно упростить отладку многопоточных приложений:

C#
1
thread.Name = "Рабочий поток №1";
При обработке исключений в многопоточной среде нужно помнить, что каждый поток имеет свой собственный стек вызовов. Это значит, что исключение, возникшее в одном потоке, не может быть перехвачено в другом. Чтобы обработать исключения, необходимо использовать блок try-catch внутри метода, выполняемого в потоке:

C#
1
2
3
4
5
6
7
8
9
10
11
12
private static void SafeMethod()
{
    try
    {
        // Опасный код, который может вызвать исключение
        int result = 10 / 0; // Вызовет DivideByZeroException
    }
    catch (Exception ex)
    {
        Console.WriteLine($"Перехвачено исключение: {ex.Message}");
    }
}
Неперехваченные исключения в потоках могут привести к непредсказуемому поведению приложения или даже его аварийному завершению, поэтому всегда обрабатывайте потенциальные исключения в каждом потоке.

При работе с классом Thread важно также учитывать культурный контекст, особенно если ваше приложение будет использоваться в международной среде. Дело в том, что каждый поток имеет свой набор культурных параметров, таких как формат даты, время, валюта и другие настройки локализации. Эти параметры определяются свойствами CurrentCulture и CurrentUICulture.

C#
1
2
Thread.CurrentThread.CurrentCulture = new CultureInfo("ru-RU");
Thread.CurrentThread.CurrentUICulture = new CultureInfo("ru-RU");
При создании нового потока он наследует культурные настройки из потока-родителя. Это может привести к неожиданным результатам, если вы не учитываете этот факт при разработке многопоточных приложений с интернационализацией.
Помимо именования потоков, которое мы обсудили ранее, существуют и другие способы идентификации потоков при отладке. Каждый поток имеет уникальный идентификатор, который можно получить через свойство ManagedThreadId:

C#
1
2
int threadId = Thread.CurrentThread.ManagedThreadId;
Console.WriteLine($"Этот код выполняется в потоке с ID: {threadId}");
Это особенно полезно при отладке сложных многопоточных приложений, когда нужно точно определить, в каком потоке происходит определенное действие.
Класс Thread позволяет устанавливать приоритет выполнения потока с помощью свойства Priority:

C#
1
thread.Priority = ThreadPriority.AboveNormal;
Доступные значения: Lowest, BelowNormal, Normal (значение по умолчанию), AboveNormal и Highest. Приоритет влияет на то, как часто планировщик потоков операционной системы будет выделять процессорное время для выполнения потока. Потоки с более высоким приоритетом получают больше процессорного времени, но это не гарантирует определенный порядок выполнения. Стоит отметить некоторые ограничения класса Thread в современном C#. Хотя Thread предоставляет низкоуровневый контроль над потоками, его использование может быть излишне сложным для многих сценариев. В современном C# существуют более высокоуровневые абстракции, такие как Task и async/await, которые предлагают более простой и структурированный подход к асинхронному программированию.

Например, создание и выполнение задачи с помощью Task выглядит так:

C#
1
2
Task task = Task.Run(() => Console.WriteLine("Выполняется в отдельном потоке"));
await task; // Ждем завершения задачи
Этот код значительно проще и понятнее, чем ручное управление потоками. Кроме того, Task предоставляет больше возможностей для управления асинхронными операциями, таких как композиция задач, обработка исключений на более высоком уровне и возврат результатов. Еще одно ограничение класса Thread — это отсутствие встроенных механизмов для возврата результатов. Если метод, выполняемый в потоке, должен вернуть результат, вам придется использовать общие переменные или колбэки, что усложняет код и может привести к проблемам с синхронизацией. Task<T>, напротив, поддерживает возвращение результатов "из коробки".

Также стоит упомянуть о ThreadPool — пуле потоков, который представляет собой коллекцию потоков, управляемых CLR. ThreadPool автоматически регулирует количество потоков в зависимости от нагрузки и доступных ресурсов, что делает его более эффективным для выполнения большого количества коротких задач:

C#
1
2
3
ThreadPool.QueueUserWorkItem(state => {
    Console.WriteLine("Выполняется в потоке из пула");
});
Несмотря на эти ограничения, класс Thread остается важным инструментом в арсенале C# разработчика, особенно для сценариев, требующих точного контроля над жизненным циклом потоков или специфических настроек, таких как приоритет и фоновый режим.

При работе с потоками через класс Thread следует также помнить о возможных проблемах с управлением памятью. Каждый поток имеет собственный стек, который занимает память (по умолчанию 1 МБ). Создание большого количества потоков может быстро исчерпать доступную память. Поэтому для сценариев с большим количеством параллельных операций лучше использовать ThreadPool или Task, которые эффективнее управляют ресурсами.

STEAM VR , Liv, синхронизаци­­­­­­­я видео в реальности и Vr( tilt brush )
Здравствуйте, у меня задача настроить качественную запись видео художника рисующего в vr ( в программах tilt brush , adobe medium в очках oculus...

Как сделать аутентификац­ия по SMS без пароля с использовани­ем Xamarin
Здравствуйте подскажите пожалуйста, как можно сделать чтобы когда пользователь вводил номер телефона, ему отправлялось смс с кодом, который он бы...

Может ли EF Core актуализиров­ать информацию, посмотрев на ContextModel­Snapshot?
Доброго времени суток, дотнетчики! Возникла следующая проблема - зафакапил truncate'ом некоторые данные в своей тестовой бд (спасибо, что не...

Canvas.Rende­rTransform vs Canvas.Layou­tTransform
Доброго времени суток При использовании настройке Canvas'а у ItemsPanelTemplate и смены у него RenderTransform на LayoutTransform туда-сюда, встал...


Управление потоками



Класс Thread предлагает ряд методов и свойств, которые позволяют контролировать выполнение потоков и взаимодействие между ними. Давайте разберём основные механизмы управления.

Метод Sleep



Метод Thread.Sleep() приостанавливает выполнение текущего потока на указанное количество миллисекунд:

C#
1
2
// Приостановка текущего потока на 1 секунду
Thread.Sleep(1000);
Этот метод освобождает процессорное время, позволяя другим потокам выполняться. Существует и вариант с параметром TimeSpan:

C#
1
Thread.Sleep(TimeSpan.FromSeconds(1));
Стоит заметить, что Sleep(0) не означает "не спать вовсе". Этот вызов просит операционную систему передать оставшееся квантовое время другим потокам того же приоритета, готовым к выполнению.

C#
1
2
// Передать оставшееся время другим потокам
Thread.Sleep(0);
Впрочем, использование Sleep() для координации потоков — не лучшая практика. Для синхронизации работы потоков существуют специальные механизмы, которые мы рассмотрим позже.

Метод Join



Метод Join() блокирует вызывающий поток до тех пор, пока не завершится поток, для которого он вызван:

C#
1
2
3
4
5
6
Thread worker = new Thread(DoWork);
worker.Start();
 
// Блокировка текущего потока до завершения worker
worker.Join();
Console.WriteLine("Рабочий поток завершился");
Join() можно использовать с таймаутом, что позволяет избежать вечной блокировки в случае зависания потока:

C#
1
2
3
4
5
6
// Ждать завершения потока максимум 5 секунд
bool completed = worker.Join(5000);
if (completed)
    Console.WriteLine("Рабочий поток завершился в течение 5 секунд");
else
    Console.WriteLine("Истекло время ожидания");
Метод Join() возвращает true, если поток завершился до истечения таймаута, и false в противном случае.

Метод Interrupt



Метод Interrupt() используется для прерывания потока, который находится в состоянии ожидания (например, во время выполнения Sleep() или Join()):

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Thread worker = new Thread(() => 
{
    try
    {
        Thread.Sleep(Timeout.Infinite);  // Бесконечное ожидание
    }
    catch (ThreadInterruptedException ex)
    {
        Console.WriteLine("Поток был прерван: " + ex.Message);
    }
});
 
worker.Start();
Thread.Sleep(1000);  // Даём потоку время на запуск
worker.Interrupt();  // Прерываем поток
Когда поток прерывается, в нём возникает исключение ThreadInterruptedException, которое нужно обрабатывать. Если поток не находится в состоянии ожидания на момент вызова Interrupt(), то исключение возникнет при следующем входе потока в состояние ожидания.

Приоритеты потоков



Приоритет потока определяет, сколько процессорного времени ему будет выделено по сравнению с другими потоками. В C# приоритет устанавливается с помощью свойства Priority:

C#
1
thread.Priority = ThreadPriority.Highest;
Доступны следующие значения (от низшего к высшему): Lowest, BelowNormal, Normal (по умолчанию), AboveNormal и Highest. Вот пример, показывающий влияние приоритетов:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
void DemonstratePriorities()
{
    int[] counters = new int[3];
    bool running = true;
 
    // Создаём три потока с разными приоритетами
    Thread high = new Thread(() => { while(running) counters[0]++; });
    Thread normal = new Thread(() => { while(running) counters[1]++; });
    Thread low = new Thread(() => { while(running) counters[2]++; });
 
    high.Priority = ThreadPriority.Highest;
    normal.Priority = ThreadPriority.Normal;
    low.Priority = ThreadPriority.Lowest;
 
    high.Start(); normal.Start(); low.Start();
    
    Thread.Sleep(1000);  // Даём потокам поработать секунду
    running = false;     // Останавливаем все потоки
    
    // Ждём завершения всех потоков
    high.Join(); normal.Join(); low.Join();
    
    // Выводим результаты
    Console.WriteLine($"Высокий приоритет: {counters[0]:N0}");
    Console.WriteLine($"Нормальный приоритет: {counters[1]:N0}");
    Console.WriteLine($"Низкий приоритет: {counters[2]:N0}");
}
Обычно поток с более высоким приоритетом будет иметь большее значение счётчика, но результаты могут зависеть от загрузки системы, количества ядер процессора и других факторов.

Важно помнить: в большинстве случаев лучше оставить приоритет потока на уровне Normal. Повышение приоритета может привести к "голоданию" других потоков (они не получат достаточно процессорного времени), а понижение — к слишком медленной работе.

Фоновые и обычные потоки



Как уже упоминали, потоки в C# делятся на фоновые и обычные. Различие между ними становится существенным при завершении программы:
  • Обычные потоки предотвращают завершение процесса, пока они активны.
  • Фоновые потоки автоматически завершаются при закрытии всех обычных потоков.

По умолчанию создаётся обычный поток. Чтобы сделать поток фоновым, установите свойство IsBackground в true:

C#
1
2
3
Thread daemon = new Thread(DoBackgroundWork);
daemon.IsBackground = true;
daemon.Start();
Это свойство можно изменять даже когда поток уже запущен.
Вот пример, демонстрирующий разницу между фоновым и обычным потоком:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
static void Main()
{
    Thread foreground = new Thread(() => 
    {
        // Этот поток продолжит выполнение после завершения Main
        Thread.Sleep(2000);
        Console.WriteLine("Обычный поток завершился");
    });
    
    Thread background = new Thread(() => 
    {
        // Этот поток будет автоматически закрыт при завершении процесса
        Thread.Sleep(2000);
        Console.WriteLine("Фоновый поток завершился");
    });
    
    background.IsBackground = true;
    
    foreground.Start();
    background.Start();
    
    Console.WriteLine("Метод Main завершается");
    // Программа продолжит работу, пока foreground не завершится
    // background может быть закрыт раньше, если foreground завершится быстрее
}
В этом примере, если не дожидаться завершения потоков, сообщение от фонового потока может никогда не отобразиться, так как процесс завершится как только закончится обычный поток.

Методы Thread.Abort и Thread.Suspend



Класс Thread раньше предоставлял методы Abort() и Suspend() для принудительного завершения и приостановки потоков, но в современном C# эти методы не рекомендуются к использованию из-за их непредсказуемости и потенциальных проблем:
  • Thread.Abort() вызывает исключение ThreadAbortException в целевом потоке, что может оставить данные в неопределённом состоянии.
  • Thread.Suspend() приостанавливает поток, но может привести к взаимоблокировкам, если поток владеет ресурсами, необходимыми другим потокам.

В .NET Core и более новых версиях эти методы вообще удалены.
Вместо них рекомендуется использовать специальные флаги для сигнализации потокам о необходимости завершения:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class SafeThreadStop
{
    private volatile bool _shouldStop = false;
    
    public void DoWork()
    {
        Thread worker = new Thread(() => 
        {
            while (!_shouldStop)
            {
                // Выполнение работы
                Console.WriteLine("Поток выполняет работу...");
                Thread.Sleep(100);
            }
            Console.WriteLine("Поток корректно завершён");
        });
        
        worker.Start();
        
        // Даём потоку поработать 1 секунду
        Thread.Sleep(1000);
        
        // Сигнал потоку о завершении
        _shouldStop = true;
        
        // Ждём завершения
        worker.Join();
    }
}
Обратите внимание на модификатор volatile перед флагом _shouldStop. Он нужен для того, чтобы JIT-компилятор не кэшировал значение переменной внутри цикла, иначе поток может никогда не увидеть изменение флага.

Отладка потоков с помощью Visual Studio



При работе с многопоточными приложениями отладка становится более сложной задачей. К счастью, Visual Studio предоставляет инструменты, которые помогают отслеживать и анализировать поведение потоков.

В окне "Потоки" (Отладка → Окна → Потоки) вы можете видеть все активные потоки в вашем приложении, их идентификаторы, имена и текущее состояние. Это окно особенно полезно, когда вы пытаетесь выяснить, какие потоки блокируются или вызывают проблемы производительности.

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static void ThreadDebuggingExample()
{
    for (int i = 0; i < 3; i++)
    {
        int threadNumber = i; // Локальная копия для замыкания
        Thread t = new Thread(() => 
        {
            // Именование помогает при отладке
            Thread.CurrentThread.Name = $"Рабочий поток {threadNumber}";
            
            // Точка останова здесь позволит изучить все потоки в окне "Потоки"
            Console.WriteLine($"Поток {Thread.CurrentThread.Name} запущен");
            Thread.Sleep(1000 * threadNumber);
        });
        t.Start();
    }
}
Visual Studio также позволяет переключаться между контекстами выполнения различных потоков во время отладки. Когда программа приостановлена на точке останова, вы можете щёлкнуть правой кнопкой мыши на потоке в окне "Потоки" и выбрать "Переключиться на этот поток", чтобы увидеть его стек вызовов и локальные переменные.
Ещё одна полезная функция — установка точек останова для конкретных потоков. Для этого нужно щёлкнуть правой кнопкой мыши на точке останова, выбрать "Условие..." и указать выражение, проверяющее идентификатор или имя потока:

C#
1
2
// Условие точки останова для конкретного потока
Thread.CurrentThread.Name == "Рабочий поток 2"
При работе с сложными многопоточными приложениями можно использовать инструменты профилирования Visual Studio для обнаружения проблем с производительностью и конкуренцией. Анализатор параллельных стеков и параллельных задач помогает выявлять узкие места и проблемы синхронизации.

Влияние Thread.Abort и Thread.Suspend на стабильность приложения



Как мы уже упоминали, методы Thread.Abort() и Thread.Suspend() могут серьёзно повлиять на стабильность приложения. Рассмотрим их влияние подробнее.

Вызов Thread.Abort() выглядит заманчивым способом быстро остановить поток, но он порождает множество проблем:
1. Когда вызывается Abort(), в целевом потоке генерируется исключение ThreadAbortException. Проблема в том, что это исключение может возникнуть в любой точке выполнения потока, даже внутри блоков finally, что нарушает механизм безопасного освобождения ресурсов.
2. Объекты, с которыми работал поток, могут остаться в несогласованном состоянии. Например, если поток был прерван посреди обновления коллекции, эта коллекция может стать поврежденной.
3. Блокировки, захваченные потоком, не освобождаются автоматически, что может привести к взаимоблокировкам.

Вот пример, демонстрирующий проблемы с Abort():

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
void DemonstrateAbortProblems()
{
    object lockObj = new object();
    bool resourceAcquired = false;
    
    Thread problematicThread = new Thread(() => 
    {
        try
        {
            lock (lockObj)
            {
                resourceAcquired = true;
                Console.WriteLine("Ресурс захвачен");
                
                // Представим, что здесь выполняется длительная операция
                while (true)
                {
                    Thread.Sleep(100);
                }
                
                // До этой точки выполнение никогда не дойдёт при вызове Abort()
            } // Блокировка не будет освобождена при Abort()
        }
        catch (ThreadAbortException ex)
        {
            Console.WriteLine($"Поток прерван: {ex.Message}");
            // Попытка очистки, но ThreadAbortException может возникнуть снова здесь
        }
        finally
        {
            // ThreadAbortException может возникнуть и здесь,
            // нарушая логику очистки ресурсов
            Console.WriteLine("Finally выполняется, но может быть прерван");
        }
    });
    
    problematicThread.Start();
    Thread.Sleep(500); // Даём время захватить блокировку
    
    // ОПАСНО: может привести к повреждению данных и взаимоблокировкам
    problematicThread.Abort();
    
    Thread.Sleep(500);
    
    // Этот поток будет заблокирован навсегда, так как lockObj не был освобождён
    Thread deadlockedThread = new Thread(() => 
    {
        Console.WriteLine("Попытка захватить ресурс...");
        lock (lockObj)
        {
            Console.WriteLine("Ресурс захвачен (это сообщение никогда не появится)");
        }
    });
    
    deadlockedThread.Start();
}
Аналогичные проблемы возникают и с Thread.Suspend(). Этот метод может приостановить поток в момент, когда он владеет важными ресурсами, что приводит к взаимоблокировкам и другим проблемам синхронизации.
Вместо этих опасных методов всегда используйте кооперативный подход к остановке потоков, как было показано ранее, с помощью флагов или токенов отмены:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
using System.Threading;
 
class CooperativeCancellation
{
    private CancellationTokenSource _cts = new CancellationTokenSource();
    
    public void StartLongOperation()
    {
        Thread t = new Thread(() => DoWork(_cts.Token));
        t.Start();
    }
    
    public void StopOperation()
    {
        _cts.Cancel();
    }
    
    private void DoWork(CancellationToken token)
    {
        try
        {
            while (!token.IsCancellationRequested)
            {
                // Выполнение работы
                Console.WriteLine("Работа выполняется...");
                Thread.Sleep(100);
                
                // Периодически проверяем токен отмены
                token.ThrowIfCancellationRequested();
            }
        }
        catch (OperationCanceledException)
        {
            // Корректная обработка отмены
            Console.WriteLine("Операция отменена корректно");
        }
        finally
        {
            // Гарантированная очистка ресурсов
            Console.WriteLine("Ресурсы освобождены");
        }
    }
}
В этом примере используется CancellationToken из пространства имен System.Threading, который предоставляет структурированный механизм для отмены операций. Это гораздо безопаснее, чем принудительное прерывание потока с помощью Abort().

Синхронизация потоков



При работе с несколькими потоками, которые обращаются к общим ресурсам, возникает проблема конкуренции (race condition). Это ситуация, когда результат выполнения зависит от порядка доступа потоков к общим данным. Рассмотрим простой пример:

C#
1
2
3
4
5
6
7
8
9
10
11
public class Counter
{
    private int _count = 0;
    
    public void Increment()
    {
        _count++; // Не атомарная операция!
    }
    
    public int Value => _count;
}
Казалось бы, инкремент — это простая операция, но на самом деле _count++ разбивается на три действия: чтение текущего значения, увеличение его на единицу и запись обратно. Если два потока одновременно вызовут метод Increment(), возможна ситуация, когда оба прочитают одно и то же значение, увеличат его на единицу и запишут одинаковый результат. В итоге счётчик увеличится только на 1 вместо ожидаемых 2.

Для решения подобных проблем в C# существует несколько механизмов синхронизации. Рассмотрим наиболее важные из них.

Блокировки и критические секции



Ключевое слово lock — это самый распространённый механизм синхронизации в C#. Оно гарантирует, что участок кода будет выполняться только одним потоком в каждый момент времени:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class ThreadSafeCounter
{
    private int _count = 0;
    private readonly object _lockObject = new object();
    
    public void Increment()
    {
        lock (_lockObject)
        {
            _count++;
        }
    }
    
    public int Value
    {
        get
        {
            lock (_lockObject)
            {
                return _count;
            }
        }
    }
}
В этом примере объект _lockObject используется как маркер блокировки. Любой поток, выполняющий метод Increment(), сначала пытается "захватить" _lockObject. Если объект уже захвачен другим потоком, текущий поток блокируется до освобождения объекта. Важно не использовать в качестве объекта блокировки типы значений (int, bool и т.д.), строки или this. Лучше всего создать приватное поле типа object специально для этой цели.

Внутри блока lock вы должны выполнять только быстрые операции. Длительные операции (такие как I/O, сетевые запросы или ожидание пользовательского ввода) могут привести к снижению производительности, так как другие потоки будут заблокированы.

Кроме lock, .NET предоставляет класс Monitor, который предлагает более гибкий контроль над блокировками:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void ComplexOperation()
{
    bool lockTaken = false;
    try
    {
        Monitor.Enter(_lockObject, ref lockTaken);
        // Критическая секция
    }
    finally
    {
        if (lockTaken)
            Monitor.Exit(_lockObject);
    }
}
На самом деле, оператор lock является синтаксическим сахаром для Monitor.Enter и Monitor.Exit. Преимущество явного использования Monitor — возможность установить таймаут для захвата блокировки и использовать методы Monitor.Wait, Monitor.Pulse и Monitor.PulseAll для более сложных сценариев синхронизации.

Другие примитивы синхронизации



Помимо lock и Monitor, .NET предлагает ряд других примитивов синхронизации:

1. Mutex — примитив синхронизации, который может работать между процессами:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
using System.Threading;
 
class MutexExample
{
    static Mutex _mutex = new Mutex(false, "GlobalMutexName");
    
    public void SafeOperation()
    {
        try
        {
            _mutex.WaitOne();
            // Критическая секция
            Console.WriteLine("Критическая секция выполняется потоком " + 
                             Thread.CurrentThread.ManagedThreadId);
            Thread.Sleep(1000); // Имитация работы
        }
        finally
        {
            _mutex.ReleaseMutex();
        }
    }
}
2. Semaphore и SemaphoreSlim — позволяют ограничить доступ к ресурсу определённым количеством потоков:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private SemaphoreSlim _semaphore = new SemaphoreSlim(3); // Макс. 3 потока
 
public async Task LimitedAccessAsync()
{
    try
    {
        await _semaphore.WaitAsync();
        // До 3 потоков могут одновременно выполнять этот код
        await Task.Delay(1000); // Имитация асинхронной работы
    }
    finally
    {
        _semaphore.Release();
    }
}
3. ReaderWriterLockSlim — оптимизированная блокировка для сценариев, когда много потоков читают данные и редко записывают:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
private ReaderWriterLockSlim _rwLock = new ReaderWriterLockSlim();
private Dictionary<int, string> _cache = new Dictionary<int, string>();
 
public string GetItem(int id)
{
    try
    {
        _rwLock.EnterReadLock();
        if (_cache.TryGetValue(id, out string value))
            return value;
    }
    finally
    {
        _rwLock.ExitReadLock();
    }
    
    try
    {
        _rwLock.EnterWriteLock();
        // Проверяем ещё раз, так как другой поток мог добавить значение
        if (_cache.TryGetValue(id, out string value))
            return value;
        
        // Получаем данные (например, из БД)
        value = "Data for ID " + id;
        _cache[id] = value;
        return value;
    }
    finally
    {
        _rwLock.ExitWriteLock();
    }
}
4. ManualResetEvent и AutoResetEvent — примитивы, которые позволяют потоку сигнализировать другим потокам о наступлении события:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
private ManualResetEvent _dataReadyEvent = new ManualResetEvent(false);
private List<int> _data = new List<int>();
 
public void ProduceData()
{
    // Готовим данные
    for (int i = 0; i < 10; i++)
    {
        _data.Add(i);
        Thread.Sleep(100); // Имитация работы
    }
    
    // Сигнализируем, что данные готовы
    _dataReadyEvent.Set();
}
 
public void ConsumeData()
{
    // Ждём, пока данные будут готовы
    _dataReadyEvent.WaitOne();
    
    // Используем данные
    foreach (var item in _data)
    {
        Console.WriteLine(item);
    }
}

Межпоточное взаимодействие



При работе с потоками часто возникает необходимость передачи данных между ними. Для этого существует несколько подходов:

1. Общие переменные — самый простой, но и самый опасный метод. Необходимо обеспечить правильную синхронизацию:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class SharedState
{
    private readonly object _lock = new object();
    private Queue<string> _messageQueue = new Queue<string>();
    
    public void EnqueueMessage(string message)
    {
        lock (_lock)
        {
            _messageQueue.Enqueue(message);
            Monitor.Pulse(_lock); // Уведомляем ожидающий поток
        }
    }
    
    public string DequeueMessage()
    {
        lock (_lock)
        {
            while (_messageQueue.Count == 0)
                Monitor.Wait(_lock); // Ждём, пока появится сообщение
            
            return _messageQueue.Dequeue();
        }
    }
}
2. BlockingCollection<T> — потокобезопасная коллекция для реализации паттерна "производитель-потребитель":

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private BlockingCollection<int> _items = new BlockingCollection<int>(new ConcurrentQueue<int>());
 
public void Producer()
{
    for (int i = 0; i < 100; i++)
    {
        _items.Add(i);
        Thread.Sleep(10); // Имитация работы
    }
    _items.CompleteAdding(); // Сигнализируем, что добавление завершено
}
 
public void Consumer()
{
    foreach (var item in _items.GetConsumingEnumerable())
    {
        Console.WriteLine($"Обработан элемент: {item}");
        Thread.Sleep(50); // Обработка занимает больше времени
    }
}
3. Concurrent коллекции — набор потокобезопасных коллекций для различных сценариев:

C#
1
2
3
4
5
6
7
private ConcurrentDictionary<string, int> _stats = new ConcurrentDictionary<string, int>();
 
public void UpdateStats(string key)
{
    // Потокобезопасное увеличение счётчика или добавление нового ключа
    _stats.AddOrUpdate(key, 1, (k, v) => v + 1);
}

Избегание взаимоблокировок



Взаимоблокировка (deadlock) — ситуация, когда два или более потоков блокируются навсегда, ожидая освобождения ресурсов друг друга. Классический случай — поток A владеет ресурсом X и ожидает ресурс Y, а поток B владеет ресурсом Y и ожидает ресурс X.

Вот пример кода, который может привести к взаимоблокировке:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
private readonly object _lock1 = new object();
private readonly object _lock2 = new object();
 
public void MethodA()
{
    lock (_lock1)
    {
        Thread.Sleep(1000); // Имитируем работу
        lock (_lock2)
        {
            Console.WriteLine("MethodA выполнен");
        }
    }
}
 
public void MethodB()
{
    lock (_lock2)
    {
        Thread.Sleep(1000); // Имитируем работу
        lock (_lock1)
        {
            Console.WriteLine("MethodB выполнен");
        }
    }
}
Если MethodA и MethodB будут выполняться параллельно, возможна взаимоблокировка. Чтобы избежать этого, следуйте нескольким правилам:

1. Всегда захватывайте блокировки в одном и том же порядке:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public void SafeMethodA()
{
    lock (_lock1)
    {
        lock (_lock2)
        {
            Console.WriteLine("SafeMethodA выполнен");
        }
    }
}
 
public void SafeMethodB()
{
    lock (_lock1) // Используем тот же порядок блокировок, что и в SafeMethodA
    {
        lock (_lock2)
        {
            Console.WriteLine("SafeMethodB выполнен");
        }
    }
}
2. Избегайте вложенных блокировок — по возможности используйте одну блокировку за раз.

3. Используйте таймауты — они помогут выйти из потенциальной взаимоблокировки:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public void TryWithTimeout()
{
    bool lockTaken = false;
    try
    {
        Monitor.TryEnter(_lock1, 1000, ref lockTaken);
        if (lockTaken)
        {
            // Продолжаем работу
        }
        else
        {
            Console.WriteLine("Не удалось получить блокировку в течение таймаута");
        }
    }
    finally
    {
        if (lockTaken)
            Monitor.Exit(_lock1);
    }
}
4. Используйте примитивы сигнализации — например, CountdownEvent, которое позволяет дождаться завершения нескольких операций:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public void CoordinateThreads(int threadCount)
{
    var countdown = new CountdownEvent(threadCount);
    
    for (int i = 0; i < threadCount; i++)
    {
        int threadId = i; // Локальная копия для замыкания
        new Thread(() => 
        {
            Console.WriteLine($"Поток {threadId} начал работу");
            Thread.Sleep(1000 * threadId); // Разное время работы
            Console.WriteLine($"Поток {threadId} завершил работу");
            countdown.Signal(); // Уменьшаем счётчик
        }).Start();
    }
    
    // Ждём, пока все потоки завершат работу
    countdown.Wait();
    Console.WriteLine("Все потоки завершены");
}
Синхронизация потоков — один из самых сложных аспектов многопоточного программирования, требующий тщательного проектирования и глубокого понимания того, как работают потоки и как они взаимодействуют друг с другом. Невнимательность может привести к труднообнаружимым ошибкам и падениям производительности. Поэтому важно хорошо освоить доступные примитивы синхронизации и применять их правильно.

Практические примеры



Теория многопоточности может показаться сложной, но конкретные примеры помогут лучше понять, как применять потоки на практике. Рассмотрим несколько реальных сценариев, где многопоточность действительно полезна.

Многопоточные расчёты



Когда у вас есть вычислительно-интенсивная задача, её можно разделить на части и распределить между несколькими потоками. Классический пример — вычисление числа π с высокой точностью методом Монте-Карло:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
public class PiCalculator
{
    private int _totalPoints;
    private int _pointsInCircle;
    private readonly object _lockObject = new object();
    private volatile bool _shouldStop;
 
    public double CalculatePi(int iterations, int threadCount)
    {
        _totalPoints = 0;
        _pointsInCircle = 0;
        _shouldStop = false;
        
        // Создаём и запускаем рабочие потоки
        Thread[] threads = new Thread[threadCount];
        
        for (int i = 0; i < threadCount; i++)
        {
            threads[i] = new Thread(() => WorkerMethod(iterations / threadCount));
            threads[i].Start();
        }
        
        // Ожидаем завершения всех потоков
        foreach (var thread in threads)
            thread.Join();
        
        // Вычисляем приближенное значение π
        return 4.0 * _pointsInCircle / _totalPoints;
    }
    
    private void WorkerMethod(int iterations)
    {
        Random rand = new Random();
        int localPointsInCircle = 0;
        int localTotalPoints = 0;
        
        for (int i = 0; i < iterations && !_shouldStop; i++)
        {
            double x = rand.NextDouble();
            double y = rand.NextDouble();
            
            if (x * x + y * y <= 1)
                localPointsInCircle++;
                
            localTotalPoints++;
        }
        
        // Обновляем общие счётчики под блокировкой
        lock (_lockObject)
        {
            _pointsInCircle += localPointsInCircle;
            _totalPoints += localTotalPoints;
        }
    }
    
    public void Stop()
    {
        _shouldStop = true;
    }
}
В этом примере мы используем метод Монте-Карло: генерируем случайные точки в квадрате [0,1]×[0,1] и проверяем, сколько из них попало в четверть круга радиусом 1. Отношение этих чисел, умноженное на 4, даёт приближённое значение π.

Обратите внимание на несколько моментов:
  • Мы создаём отдельный экземпляр Random для каждого потока, чтобы избежать конкуренции.
  • Локальные счётчики собирают данные в каждом потоке, и только в конце мы обновляем общие счётчики под блокировкой.
  • Флаг _shouldStop позволяет корректно остановить вычисления.

Асинхронная обработка данных



Другой частый сценарий — параллельная обработка набора данных, например, файлов:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
public class FileProcessor
{
    private ConcurrentDictionary<string, long> _fileSizes = new ConcurrentDictionary<string, long>();
    private int _processedFiles;
    private readonly object _lockObject = new object();
    
    public void ProcessDirectory(string path, int maxThreads)
    {
        string[] files = Directory.GetFiles(path, "*", SearchOption.AllDirectories);
        
        _processedFiles = 0;
        
        // Контролируем количество одновременно работающих потоков
        using (SemaphoreSlim semaphore = new SemaphoreSlim(maxThreads))
        {
            List<Thread> threads = new List<Thread>();
            
            foreach (string file in files)
            {
                semaphore.Wait(); // Ждём, пока освободится слот
                
                Thread t = new Thread(() =>
                {
                    try
                    {
                        ProcessFile(file);
                    }
                    finally
                    {
                        semaphore.Release(); // Освобождаем слот
                    }
                });
                
                threads.Add(t);
                t.Start();
            }
            
            // Ждём завершения всех потоков
            foreach (var thread in threads)
                thread.Join();
        }
        
        // Выводим результаты
        Console.WriteLine($"Обработано файлов: {_processedFiles}");
        Console.WriteLine($"Топ-5 самых больших файлов:");
        
        foreach (var pair in _fileSizes.OrderByDescending(p => p.Value).Take(5))
        {
            Console.WriteLine($"{pair.Key}: {pair.Value / 1024} KB");
        }
    }
    
    private void ProcessFile(string filePath)
    {
        try
        {
            // Имитация обработки файла
            Thread.Sleep(10);
            
            FileInfo info = new FileInfo(filePath);
            _fileSizes[filePath] = info.Length;
            
            lock (_lockObject)
            {
                _processedFiles++;
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Ошибка при обработке {filePath}: {ex.Message}");
        }
    }
}
В этом примере мы используем семафор для ограничения количества одновременно работающих потоков. Это важно при обработке большого количества файлов, чтобы не исчерпать ресурсы системы и не создать излишнюю нагрузку на диск.

Распространенные ошибки и их устранение



При работе с многопоточным кодом легко допустить ошибки, которые могут вызвать странное поведение программы. Рассмотрим наиболее распространенные.

Состояние гонки (Race Condition)



Одна из самых частых ошибок — это когда несколько потоков одновременно изменяют общие данные без синхронизации:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class BankAccount
{
    private int _balance;
    
    public void Deposit(int amount)
    {
        _balance += amount; // Небезопасная операция
    }
    
    public void Withdraw(int amount)
    {
        if (_balance >= amount) // Проверка и изменение не атомарны
            _balance -= amount;
    }
    
    public int Balance => _balance;
}
Решение: используйте lock для синхронизации доступа:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class ThreadSafeBankAccount
{
    private int _balance;
    private readonly object _lockObject = new object();
    
    public void Deposit(int amount)
    {
        lock (_lockObject)
        {
            _balance += amount;
        }
    }
    
    public bool Withdraw(int amount)
    {
        lock (_lockObject)
        {
            if (_balance >= amount)
            {
                _balance -= amount;
                return true;
            }
            return false;
        }
    }
    
    public int Balance
    {
        get
        {
            lock (_lockObject)
            {
                return _balance;
            }
        }
    }
}

Проблема проглатывания исключений



Исключения в потоках не распространяются в вызывающий поток и могут быть "проглочены", если их не обработать:

C#
1
2
3
4
5
6
7
8
9
10
11
void TroubleWithExceptions()
{
    Thread t = new Thread(() =>
    {
        throw new InvalidOperationException("Ошибка в потоке");
    });
    
    t.Start();
    t.Join(); // Ждём завершения потока, но исключение не будет перехвачено
    Console.WriteLine("Готово"); // Этот код выполнится, несмотря на исключение
}
Решение: создайте механизм передачи исключений между потоками:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void HandlingThreadExceptions()
{
    Exception threadException = null;
    
    Thread t = new Thread(() =>
    {
        try
        {
            throw new InvalidOperationException("Ошибка в потоке");
        }
        catch (Exception ex)
        {
            threadException = ex;
        }
    });
    
    t.Start();
    t.Join();
    
    if (threadException != null)
        throw new Exception("Ошибка в рабочем потоке", threadException);
    
    Console.WriteLine("Успешное завершение");
}
В более сложных сценариях можно использовать TaskCompletionSource<T> для передачи результатов и исключений между потоками.

Взаимоблокировка (Deadlock)



Мы уже рассматривали проблемы взаимоблокировки ранее. Вот еще один пример, который показывает, как легко создать взаимоблокировку с помощью вложенных блокировок:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class DeadlockDemo
{
    private readonly object _resourceA = new object();
    private readonly object _resourceB = new object();
    
    public void OperationA()
    {
        lock (_resourceA)
        {
            Console.WriteLine("Поток A захватил ресурс A");
            Thread.Sleep(1000); // Имитация работы
            
            lock (_resourceB)
            {
                Console.WriteLine("Поток A захватил ресурс B");
            }
        }
    }
    
    public void OperationB()
    {
        lock (_resourceB)
        {
            Console.WriteLine("Поток B захватил ресурс B");
            Thread.Sleep(1000); // Имитация работы
            
            lock (_resourceA)
            {
                Console.WriteLine("Поток B захватил ресурс A");
            }
        }
    }
}
Решение зависит от конкретной ситуации:
  • Приобретайте блокировки всегда в одном порядке.
  • Используйте Monitor.TryEnter с таймаутом.
  • Перепроектируйте код, чтобы уменьшить взаимозависимости.

Проблема "слишком много потоков"



Создание большого количества потоков может привести к проблемам производительности:

C#
1
2
3
4
5
6
7
8
9
10
11
void TooManyThreads(int taskCount)
{
    for (int i = 0; i < taskCount; i++)
    {
        new Thread(() => {
            // Что-то делаем
            Thread.Sleep(1000);
        }).Start();
    }
    // Если taskCount очень большой, производительность может упасть
}
Решение: используйте пул потоков или Task с ограничением параллелизма:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
void BetterApproach(int taskCount)
{
    // Настраиваем ParallelOptions для ограничения параллелизма
    var options = new ParallelOptions
    {
        MaxDegreeOfParallelism = Environment.ProcessorCount
    };
    
    Parallel.For(0, taskCount, options, i =>
    {
        // Что-то делаем
        Thread.Sleep(1000);
    });
}

Тестирование многопоточного кода



Тестирование многопоточного кода — сложная задача, поскольку проблемы могут возникать недетерминированно и зависеть от очень тонких моментов во времени выполнения. Вот несколько методов для тестирования:

Стресс-тестирование



Запуск многих экземпляров кода одновременно может выявить проблемы синхронизации:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
[TestMethod]
public void StressTestThreadSafety()
{
    ThreadSafeBankAccount account = new ThreadSafeBankAccount();
    const int threadCount = 10;
    const int operationsPerThread = 1000;
    bool failed = false;
    
    Thread[] threads = new Thread[threadCount];
    for (int i = 0; i < threadCount; i++)
    {
        threads[i] = new Thread(() =>
        {
            for (int j = 0; j < operationsPerThread; j++)
            {
                account.Deposit(1);
                bool success = account.Withdraw(1);
                if (!success)
                    failed = true;
            }
        });
        threads[i].Start();
    }
    
    foreach (var thread in threads)
        thread.Join();
    
    Assert.IsFalse(failed, "Некоторые операции снятия не удались");
    Assert.AreEqual(0, account.Balance, "Баланс должен быть 0");
}

Инструментированное тестирование



Для более сложных сценариев можно добавить инструментацию для отслеживания порядка выполнения:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
class InstrumentedAccount
{
    private int _balance;
    private readonly object _lock = new object();
    private List<string> _operationLog = new List<string>();
    
    public void Deposit(int amount, int threadId)
    {
        lock (_lock)
        {
            _operationLog.Add($"T{threadId}: Депозит {amount} начат. Баланс={_balance}");
            _balance += amount;
            _operationLog.Add($"T{threadId}: Депозит {amount} завершен. Баланс={_balance}");
        }
    }
    
    public bool Withdraw(int amount, int threadId)
    {
        lock (_lock)
        {
            _operationLog.Add($"T{threadId}: Снятие {amount} начато. Баланс={_balance}");
            if (_balance >= amount)
            {
                _balance -= amount;
                _operationLog.Add($"T{threadId}: Снятие {amount} успешно. Баланс={_balance}");
                return true;
            }
            _operationLog.Add($"T{threadId}: Снятие {amount} неудачно. Баланс={_balance}");
            return false;
        }
    }
    
    public List<string> GetOperationLog()
    {
        lock (_lock)
        {
            return new List<string>(_operationLog);
        }
    }
    
    public int Balance
    {
        get
        {
            lock (_lock)
            {
                return _balance;
            }
        }
    }
}

Мониторинг производительности многопоточных приложений



Для оценки эффективности многопоточного кода можно использовать профилировщики:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
void MeasureThreadPerformance(int iterations, int threadCount)
{
    Stopwatch stopwatch = new Stopwatch();
    stopwatch.Start();
    
    // Однопоточное выполнение
    for (int i = 0; i < iterations; i++)
    {
        // Выполняем работу
        DoWork(i);
    }
    
    stopwatch.Stop();
    long singleThreadTime = stopwatch.ElapsedMilliseconds;
    Console.WriteLine($"Однопоточное время: {singleThreadTime} мс");
    
    // Многопоточное выполнение
    stopwatch.Restart();
    
    int itemsPerThread = iterations / threadCount;
    Thread[] threads = new Thread[threadCount];
    
    for (int t = 0; t < threadCount; t++)
    {
        int start = t * itemsPerThread;
        int end = (t == threadCount - 1) ? iterations : (t + 1) * itemsPerThread;
        
        threads[t] = new Thread(() =>
        {
            for (int i = start; i < end; i++)
            {
                DoWork(i);
            }
        });
        
        threads[t].Start();
    }
    
    foreach (var thread in threads)
        thread.Join();
    
    stopwatch.Stop();
    long multiThreadTime = stopwatch.ElapsedMilliseconds;
    Console.WriteLine($"Многопоточное время ({threadCount} потоков): {multiThreadTime} мс");
    
    double speedup = (double)singleThreadTime / multiThreadTime;
    Console.WriteLine($"Ускорение: {speedup:F2}x");
}
 
void DoWork(int value)
{
    // Имитация CPU-интенсивной работы
    double result = 0;
    for (int i = 0; i < 10000; i++)
    {
        result += Math.Sin(value * 0.01) * Math.Cos(value * 0.01);
    }
}
Также полезно использовать инструменты встроенного мониторинга .NET:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void MonitorThreadPool()
{
    // Получаем информацию о пуле потоков
    ThreadPool.GetMaxThreads(out int maxWorkerThreads, out int maxCompletionPortThreads);
    ThreadPool.GetAvailableThreads(out int availableWorkerThreads, out int availableCompletionPortThreads);
    
    int busyWorkerThreads = maxWorkerThreads - availableWorkerThreads;
    int busyCompletionPortThreads = maxCompletionPortThreads - availableCompletionPortThreads;
    
    Console.WriteLine($"Рабочие потоки: {busyWorkerThreads}/{maxWorkerThreads}");
    Console.WriteLine($"Потоки порта завершения: {busyCompletionPortThreads}/{maxCompletionPortThreads}");
    
    // Мониторинг производительности
    PerformanceCounter cpuCounter = new PerformanceCounter("Processor", "% Processor Time", "_Total");
    PerformanceCounter ramCounter = new PerformanceCounter("Memory", "Available MBytes");
    
    Console.WriteLine($"Загрузка CPU: {cpuCounter.NextValue()}%");
    Console.WriteLine($"Доступная память: {ramCounter.NextValue()} МБ");
}

Реальный пример: параллельная загрузка и обработка изображений



Рассмотрим практический пример приложения, которое загружает и обрабатывает несколько изображений одновременно:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
public class ImageProcessor
{
    private readonly SemaphoreSlim _throttler;
    private readonly ConcurrentDictionary<string, byte[]> _processedImages = new ConcurrentDictionary<string, byte[]>();
    
    public ImageProcessor(int maxConcurrentOperations)
    {
        _throttler = new SemaphoreSlim(maxConcurrentOperations);
    }
    
    public async Task ProcessImagesAsync(List<string> imageUrls)
    {
        List<Task> tasks = new List<Task>();
        
        foreach (var url in imageUrls)
        {
            tasks.Add(ProcessSingleImageAsync(url));
        }
        
        await Task.WhenAll(tasks);
    }
    
    private async Task ProcessSingleImageAsync(string imageUrl)
    {
        try
        {
            await _throttler.WaitAsync();
            
            // Здесь мы используем Task, а не Thread напрямую,
            // так как это более подходит для I/O операций
            using (HttpClient client = new HttpClient())
            {
                // Загружаем изображение
                byte[] imageData = await client.GetByteArrayAsync(imageUrl);
                
                // Запускаем CPU-интенсивную обработку в отдельном потоке
                byte[] processedData = await Task.Run(() => ApplyImageFilter(imageData));
                
                // Сохраняем результат
                _processedImages[imageUrl] = processedData;
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Ошибка при обработке {imageUrl}: {ex.Message}");
        }
        finally
        {
            _throttler.Release();
        }
    }
    
    private byte[] ApplyImageFilter(byte[] imageData)
    {
        // Имитация обработки изображения (в реальном приложении здесь был бы код фильтрации)
        Thread.Sleep(500); // Представим, что обработка занимает время
        
        // Для демонстрации просто возвращаем исходные данные
        return imageData;
    }
    
    public IReadOnlyDictionary<string, byte[]> GetProcessedImages()
    {
        return _processedImages;
    }
}
 
// Использование:
async Task Main()
{
    List<string> imageUrls = new List<string>
    {
        "https://example.com/image1.jpg",
        "https://example.com/image2.jpg",
        "https://example.com/image3.jpg",
        // ... больше URL-адресов
    };
    
    ImageProcessor processor = new ImageProcessor(maxConcurrentOperations: 3);
    await processor.ProcessImagesAsync(imageUrls);
    
    var results = processor.GetProcessedImages();
    Console.WriteLine($"Обработано {results.Count} изображений");
}
В этом примере мы комбинируем асинхронное программирование (для I/O операций) с параллельными вычислениями (для CPU-интенсивной работы). Также используем SemaphoreSlim для ограничения одновременных запросов, чтобы не перегружать сеть или систему.

Тонкая настройка многопоточного приложения



Для максимальной производительности многопоточных приложений важно учитывать особенности аппаратного обеспечения:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
public class ThreadOptimizer
{
    private readonly int _processorCount;
    private readonly int _optimalThreadCount;
    
    public ThreadOptimizer()
    {
        _processorCount = Environment.ProcessorCount;
        
        // Для CPU-bound задач оптимальное число потоков обычно равно числу ядер
        _optimalThreadCount = _processorCount;
        
        // Для I/O-bound задач можно использовать больше потоков
        int ioThreadCount = _processorCount * 2;
        
        Console.WriteLine($"Количество процессоров: {_processorCount}");
        Console.WriteLine($"Оптимальное количество потоков для CPU-bound задач: {_optimalThreadCount}");
        Console.WriteLine($"Рекомендуемое количество потоков для I/O-bound задач: {ioThreadCount}");
    }
    
    public ParallelOptions GetCpuBoundParallelOptions()
    {
        return new ParallelOptions
        {
            MaxDegreeOfParallelism = _optimalThreadCount
        };
    }
    
    public void RunCpuBoundTask(Action<int> action, int iterations)
    {
        Parallel.For(0, iterations, GetCpuBoundParallelOptions(), action);
    }
    
    // Метод для тестирования разных степеней параллелизма
    public Dictionary<int, long> BenchmarkThreadCounts(Action<int> work, int iterations, int maxThreadsToTest)
    {
        Dictionary<int, long> results = new Dictionary<int, long>();
        
        for (int threads = 1; threads <= Math.Min(maxThreadsToTest, _processorCount * 4); threads++)
        {
            Stopwatch sw = Stopwatch.StartNew();
            
            int itemsPerThread = iterations / threads;
            CountdownEvent countdown = new CountdownEvent(threads);
            
            for (int t = 0; t < threads; t++)
            {
                int start = t * itemsPerThread;
                int end = (t == threads - 1) ? iterations : (t + 1) * itemsPerThread;
                
                new Thread(() => {
                    for (int i = start; i < end; i++)
                    {
                        work(i);
                    }
                    countdown.Signal();
                }).Start();
            }
            
            countdown.Wait();
            sw.Stop();
            
            results[threads] = sw.ElapsedMilliseconds;
            Console.WriteLine($"Потоков: {threads}, Время: {sw.ElapsedMilliseconds} мс");
        }
        
        int bestThreadCount = results.OrderBy(pair => pair.Value).First().Key;
        Console.WriteLine($"Лучший результат с {bestThreadCount} потоками");
        
        return results;
    }
}
Этот класс помогает определить оптимальное количество потоков для конкретной задачи и системы. В некоторых случаях оптимальное число потоков может быть не равно количеству ядер процессора.

Заключение



Многопоточное программирование — мощная техника, которая может значительно повысить производительность и отзывчивость приложений. Класс Thread в C# предоставляет низкоуровневый контроль над потоками, хотя в современной разработке часто предпочтительнее использовать более высокоуровневые абстракции, такие как Task, async/await или Parallel. Однако понимание основ работы потоков критически важно даже при использовании этих абстракций, поскольку многие проблемы (особенно связанные с синхронизацией и взаимоблокировками) остаются актуальными независимо от уровня абстракции.

При разработке многопоточных приложений придерживайтесь следующих рекомендаций:
1. Чётко определите, какие задачи выигрывают от параллельного выполнения (CPU-bound vs. I/O-bound).
2. Выбирайте подходящие механизмы синхронизации.
3. Избегайте общего изменяемого состояния, когда это возможно.
4. Тщательно тестируйте на различных нагрузках и конфигурациях.
5. Используйте инструменты профилирования для оптимизации.

Многопоточное программирование требует особой осторожности и внимания к деталям, но при правильном использовании позволяет создавать высокопроизводительные и отзывчивые приложения, полностью использующие возможности современного оборудования.

Не удалось привести тип объекта "<>f__AnonymousType0`6[System.Int32,System.String,System.String,System.St­­­ring,Stri
Cам listbox: &lt;ListBox x:Name=&quot;ActualList&quot; Background=&quot;Transparent&quot; BorderBrush=&quot;Transparent&quot; VerticalAlignment=&quot;Center&quot;...

Многопоточное умножение матриц не работает без Thread.Sleep
Задача состоит в том, чтобы перемножить две матрицы n*m и m*k вычисляя произведение векторов в n*k процесах и продемонстрировать непоследовательный...

Как использовать Thread.Sleep(5000), ошибка: "Элемент "Thread" не существует в текущем контексте"
я так понимаю, что Thread.Sleep(5000); это &quot;тормоз&quot; процесса выполенения программы на 5 сек? А как его правильно инициализировать и запустить?...

Компромис скорости и нагрузки на процессор - Thread.Sleep(0) и Thread.Sleep(1)
Всем привет! Есть бесконечный цикл. while(true) { ... } Внутри выполняются важные операции)

Метод Thread.Suspend(),Thread.Resume()
Здравствуйте,пытаюсь сделать игру простенькую в Windows Form. И хочу,чтобы в ней было включено нажатие на паузу и возобновление, для этого видел...

Неоднозначный вызов следующих методов или свойств - Thread.Thread()
Вот кусок кода, по которому у меня вопрос: this.dataGridView1.Rows.Insert(this.dataGridView1.Rows.Count, new object); this.dataGridView1.Value...

Ошибки при использовани­­и примера кода из MSDN
При использовании примера кода из MSDN возникло множество ошибок. В VS 2022 cоздал проект Windows Forms (Microsoft) C# и вставил код в Form1.cs ...

SendAudioAsy­­­­nc Telegram.Bot Api
У меня есть ссылка на аудио источник. Пример ссылки:...

Аутентификац­ия и авторизация SPA
Добрый день! Необходимо реализовать простенькую SPA и ASP.NET Core на сервере. Как правильно реализовывать аутентификацию? Ведь я могу просто вручную...

Ошибка "NullReferen­ce­Exception: Object reference not set to an instance of an object"
Привет, я делал игру и у меня вылезла эта ошибка: NullReferenceException: Object reference not set to an instance of an object UnitScript.Update...

Ошибка System.Data.SqlClient.SqlExcep­­­­­tion: 'Incorrect syntax near 'nvarchar'
у меня выдаёт такую ошибку - ошибка System.Data.SqlClient.SqlException: 'Incorrect syntax near 'nvarchar' This exception was originally thrown at...

Исключение [System.Linq.Enumerable+WhereSelectEnumerableIterat­or`2[System.Text.RegularExpressions.Match,System.String]
Здравствуйте ! Нужно объединить textlogin и Result в одну часть, Но появляется ошибка , не могу понять почему , можете помочь:drink: По...

Надоела реклама? Зарегистрируйтесь и она исчезнет полностью.
Всего комментариев 0
Комментарии
 
Новые блоги и статьи
Debian 13: Установка Lazarus QT5
ВитГо 09.05.2026
Эта инструкция моя компиляция инструкций volvo https:/ / www. cyberforum. ru/ blogs/ 203668/ 10753. html и его же старой инструкции по установке Lazarus с gtk2. . .
Нейросеть на алгоритме "эстафета хвоста" как перспектива.
Hrethgir 06.05.2026
На десерт, когда запущу сервер. Статья тут https:/ / habr. com/ ru/ articles/ 1030914/ . Автор я сам, нейросеть только помогает в вопросах которые мне не известны - не знаю людей которые знали-бы. . .
Асинхронный приём данных из COM-порта
Argus19 01.05.2026
Асинхронный приём данных из COM-порта Купил на aliexpress термопринтер QR701. Он оказался странным. Поключил к Arduino Nano. Был очень удивлён. Наотрез отказывается печатать русские буквы. Чтобы. . .
попытка написать игровой сервер на C++
pyirrlicht 29.04.2026
попытка написать игровой сервер на плюсах с открытым бесконечным миром. возможно получится прикрутить интерпретатор питон для кастомизации игровой логики. что есть на текущий момент:. . .
Контроль уникальности выбранного документа-основания при изменении реквизита
Maks 28.04.2026
Алгоритм из решения ниже разработан на примере нетипового документа "ЗаявкаНаРемонтСпецтехники", разработанного в КА2. Задача: уведомлять пользователя, если указанная заявка (документ-основание). . .
Благородство как наказание
Maks 24.04.2026
У хорошего человека отношения с женщинами всегда складываются трудно. А я человек хороший. Заявляю без тени смущения, потому что гордиться тут нечем. От хорошего человека ждут соответствующего. . .
Валидация и контроль данных табличной части документа перед записью
Maks 22.04.2026
Алгоритм из решения ниже реализован на примере нетипового документа, разработанного в КА2. Задача: контроль и валидация данных табличной части документа перед записью с учетом регламента компании. . .
Отчёт о затраченных материалах за определенный период с макетом печатной формы
Maks 21.04.2026
Отчёт из решения ниже размещён в конфигурации КА2. Задача: разработка отчёта по затраченным материалам за определённый период, с возможностью вывода печатной формы отчёта с шапкой и подвалом. В. . .
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin
Copyright ©2000 - 2026, CyberForum.ru