Что такое многопоточные приложения?
Если объяснить простыми словами, то это программы, в которых несколько «рабочих» потоков одновременно выполняют разные или похожие задачи. Зачем это нужно? Ресурсы компьютера не всегда используются максимально эффективно.
Представьте: ваша программа скачивает страницу из интернета, затем анализирует её и только после этого начинает загружать следующую. В момент анализа соединение простаивает, а когда идёт закачка – процессор «застревает» в ожидании. Можно обойти эту проблему, одновременно работая над разными этапами: пока анализируем текущую страницу, сразу качаем следующую.
В этой статье я покажу, как можно оптимизировать программы с помощью многопоточности и приведу несколько примеров кода.
Когда стоит распараллеливать?
Если у вас один поток данных, который нужно обрабатывать в строгом порядке без использования результатов предыдущего шага, то добавлять новые потоки смысла не будет. Но если этапы независимы, можно создать дополнительные потоки.
Пример из практики: два рабочих копают яму глубиной 2 метра за час. Вместе они сделают это за полчаса – звучит логично. Если взять 3600 таких рабочих, теоретически яма будет выкопана за 1 секунду. На деле же они будут мешать друг другу, топтаться и нервничать.
Таким образом, многопоточность полезна не всегда, и «чем больше потоков – тем лучше» – это миф. Потоки должны работать без конфликтов и эффективно использовать ресурсы системы.
Начало работы с потоками
Для начала подключаем пространство имён System.Threading:
using System.Threading;
В C# каждый поток – это метод. Поэтому создаём класс с нужным методом. Самый простой вариант: void MyThreadFunction() { … }.
Запуск потока выглядит так:
Thread thr = new Thread(MyThreadFunction);thr.Start();
После вызова Start() управление возвращается сразу, но новый поток уже работает параллельно. Если метод не возвращает значение – значит, его нет куда отправить после запуска.
Поток с параметром
Можно определить метод, принимающий объект:
void ThreadFunction(Object input) { … }
Это удобно, когда при создании потока нужно передать данные. Любой класс наследует Object, поэтому можно передавать любой объект.
Простой пример с двумя потоками
Ниже показан код программы, где главный поток выводит трижды «Это главный поток программы!», а дочерний – аналогично «Это дочерний поток программы!»:
using System;using System.Threading;namespace ThreadsExample{ class Program { static void Main(string[] args) { Thread thread = new Thread(ThreadFunction); thread.Start(); int count = 3; while (count > 0) { Console.WriteLine("Это главный поток программы!"); --count; } Console.Read(); } //Метод потока static void ThreadFunction() { int count = 3; while (count > 0) { Console.WriteLine("Это дочерний поток программы!"); --count; } } }}
Обратите внимание: метод потока объявлен static, чтобы его можно было вызвать из главного потока. Запускайте программу несколько раз – порядок вывода меняется, ведь планировщик ОС распределяет время процессора случайным образом.
Создание множества одинаковых потоков
Если все потоки выполняют одни и те же действия, можно использовать одну функцию. Пример создания 10 потоков в цикле:
using System;using System.Threading;namespace SomeThreadsExample{ class Program { static void Main(string[] args) { for (int i = 0; i < 10; ++i) { Thread thread = new Thread(ThreadFunction); thread.Start(); } Console.WriteLine("Создание потоков завершено!"); Console.Read(); } static void ThreadFunction() { Console.WriteLine("Я поток!"); } }}
Таким образом, 10 потоков используют одну и ту же функцию.
Потоки с разными действиями
Чтобы каждый поток делал что-то уникальное, можно либо создать разные функции, либо использовать один метод с параметром. Первый вариант:
using System;using System.Threading;namespace SomeThreadsExample{ class Program { static void Main(string[] args) { Thread thread1 = new Thread(ThreadFunction1); thread1.Start(); Thread thread2 = new Thread(ThreadFunction2); thread2.Start(); Console.Read(); } static void ThreadFunction1() { Console.WriteLine("Это первый поток!"); } static void ThreadFunction2() { Console.WriteLine("Это второй поток!"); } }}
Второй способ – один метод с булевым флагом:
using System;using System.Threading;namespace SomeThreadsExample{ class Program { static void Main(string[] args) { Thread thread1 = new Thread(ThreadFunction); thread1.Start(true); // первый поток Thread thread2 = new Thread(ThreadFunction); thread2.Start(false); // второй поток Console.Read(); } static void ThreadFunction(Object input) { bool flag = (bool)input; if (flag) Console.WriteLine("Это первый поток!"); else Console.WriteLine("Это второй поток!"); } }}
Синхронизация данных
В