Многопоточность, Thread

Иногда в реализации нашего кода может появиться момент, когда мы хотим, чтобы два действия выполнялись одновременно. Однако с самого начала обучения мы с вами знаем, что код идет сверху вниз и все. Как же нам разделить этот код на две отдельные последовательности, которые будут идти одновременно?

Для этого существуют потоки. В C# поток – Thread. Когда потоков много, у нас реализовывается многопоточность


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


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

Изначально, в нашем коде есть один поток – основной. Он запускается вместе с программой, и именно в этом потоке у нас воспроизводится весь наш код. Но что, если у меня есть что-то, что блокирует выполнение этого кода, например, бесконечный цикл?

Например, я хочу, чтобы у меня каждую секунду мелькала строчка то на нулевой строке, то на второй строке. Я напишу вот такой код

while (true) {     Console.SetCursorPosition(0, 2);     Console.WriteLine(" ");     Console.SetCursorPosition(0, 0);     Console.WriteLine("---");         Thread.Sleep(1000);         Console.SetCursorPosition(0, 2);     Console.WriteLine("---");     Console. SetCursorPosition(0, 0);     Console.WriteLine(" ");     Thread.Sleep(1000); }

Уже здесь мы видим работу с потоками. Мы останавливаем основной поток с помощью команды Thread.Sleep(); на 1000 милисекунд, т.е. на 1 секунду.

Параллельно с этим я хочу, чтобы между этими строчками у меня было постоянное чтение символа с консоли и его вывод. Для этого, чуть ниже, я напишу код для постоянного чтения символа с консоли. Итоговый код будет выглядеть вот так

while (true) {     Console.SetCursorPosition(0, 2);     Console.WriteLine(" ");     Console.SetCursorPosition(0, 0);     Console.WriteLine("---");         Thread.Sleep(1000);         Console.SetCursorPosition(0, 2);     Console.WriteLine("---");     Console.SetCursorPosition(0, 0);     Console.WriteLine(" ");     Thread.Sleep(1000); } while (true) {     ConsoleKeyInfo key = Console.ReadKey();     Console.SetCursorPosition(0, 1);     Console.Write(" ");     Console.SetCursorPosition(0, 1);     Console.Write(key.Key); }

Однако здесь мы видим, что у нас два бесконечных цикла. Код и из одного не выйдет, а разговора о втором даже не заходит. Однако я очень хочу, чтобы эти два кода работали одновременно. Тогда, один из этих циклов, мне нужно вынести в отдельный поток – Thread

Выносить я буду, например, отрисовку строчки. Чтобы понять, что нужно выносить - лучше выносите что-то фоновое, постоянное. В моем случае, отрисовка полоски - постоянное действие, и выношу я ее.

Thread – сложный тип данных, так что, чтобы создать поток, нам нужно создать переменную типа данных Thread

Thread thread = new Thread();

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

  1. Вынести код с моей задачей – отрисовку строчки – в отдельный метод. Внутри Thread написать new ThreadStart(НазваниеМетода). ThreadStart говорит нам о том, какой метод надо запустить при старте этого потока
    Обратите внимание, что название метода в этом случае будет без круглых скобок! Еще обратите внимание, что если у нас внутри метода есть параметры, то такой способ использовать нельз

    Thread thread = new Thread(new ThreadStart(Method)); void Method() {     while (true)     {         Console.SetCursorPosition(0, 2);         Console.WriteLine(" ");         Console.SetCursorPosition(0, 0);         Console.WriteLine("---");                 Thread.Sleep(1000);                 Console.SetCursorPosition(0, 2);         Console.WriteLine("---");         Console. SetCursorPosition(0, 0);         Console.WriteLine(" ");         Thread.Sleep(1000);     } }

  2. С помощью лямбда выражения (вспоминаем сложные linq запросы, лямбда выражение это где =>), сразу вписать код, который мы хотим выполнить при старте. Наша лямбда – выражение будет выгляжить как _ => {}, где _ - переменная заглушка, а внутри фигурных скобок будет код, который должен выполняться в потоке. Внутри этих фигурных скобок можно также вызывать методы и передавать туда значения в параметры

    Thread thread = new Thread(_ => {     while (true)     {         Console.SetCursorPosition(0, 2);         Console.WriteLine(" ");         Console.SetCursorPosition(0, 0);         Console.WriteLine("---");                 Thread.Sleep(1000);                 Console.SetCursorPosition(0, 2);         Console.WriteLine("---");         Console. SetCursorPosition(0, 0);         Console.WriteLine(" ");         Thread.Sleep(1000);     } });

После того, как мы определили, какой код будет выполняться в другом потоке, мы должны его запустить. Чтобы запустить поток, надо написать названиепеременной.Start();. И тогда, наш код будет параллельно выполняться в двух потоках

Суммарно, код, с вторым вариантом потока, выглядит вот так

Thread thread = new Thread(_ => {     while (true)     {         Console.SetCursorPosition(0, 2);         Console.WriteLine(" ");         Console.SetCursorPosition(0, 0);         Console.WriteLine("---");                 Thread.Sleep(1000);                 Console.SetCursorPosition(0, 2);         Console.WriteLine("---");         Console.SetCursorPosition(0, 0);         Console.WriteLine(" ");         Thread.Sleep(1000);     } }); thread.Start(); while (true) {     ConsoleKeyInfo key = Console.ReadKey();     Console.SetCursorPosition(0, 1);     Console.Write(" ");     Console.SetCursorPosition(0, 1);     Console.Write(key.Key); }

А при запуске, мы увидим, что мы можем и символы вводить, и полосочки отрисовываются. Причем одновременно!

Результат работы

Дополнительно, мы можем не только запускать код, но и узнать информацию о нашем потоке. Например:

  • Узнать текущий поток – Thread.CurrentThread
  • Выставить приоритет выполнения текущего потока - Thread.CurrentThread.Priority
  • Остановить выполнение текущего потока - Thread.CurrentThread.Abort
  • Узнать, запущен ли поток - Thread.CurrentThread.IsAlive
  • Узнать или изменить состояние текущего потока - Thread.CurrentThread.CurrentState
  • Остановить поток на какой-то промежуток времени – Thread.Sleep(100). 100 здесь – 100 милисекунд