Многопоточные приложения в C# для чайников

Потоки(treads - нити) в C#Что такое многопоточные приложения? Грубо говоря, это приложения с несколькими «рабочими», которые одновременно выполняют разные или однотипные задачи. Зачем это нужно? Ресурсы компьютера используются не всегда эффективно. Например, ваша программа скачивает страницу из интернета, потом анализирует ее, затем — качает следующую. Во время анализа простаивает интернет соединение, а во время закачки — скучает процессор. Это можно исправить. Уже во время анализа текущей страницы параллельно качать следующую.

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

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

Допустим, у нас есть 2 рабочих, которые хорошо копают ямы. Предположим, что один выкопает яму глубиной 2 метра, за 1 час. Тогда два рабочих, выкопают эту яму за полчаса. Это похоже на правду. Давайте возьмем 3600 таких рабочих. Теоретически, они выкопают яму глубиной 2 метра за 1 секунду. Но на практике они будут друг другу мешать, топтаться на одном месте и нервничать.

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

Далее немного практики. Что бы начать работать с потоками, необходимо подключить пространство имен System.Threading, добавив в начало файла с кодом следующую директиву:

Любой поток в C# это функция. Функции не могут быть сами по себе, они обязательно являются методами класса. Поэтому, что бы создать отдельный поток, нам понадобится класс с необходимым методом. Самый простой вариант метода возвращает void и не принимает аргументов:

Пример запуска такого потока:

После вызова метода Start() у объекта потока, управление вернется сразу, но в этот момент уже начнет работать ваш новый поток. Новый поток выполнит тело функции MyThreadFunction и завершится. Мой друг спросил меня, а почему функция не возвращает значение? А потому, что его некуда вернуть. После вызова Start(), управление передается дальше, при этом созданный поток может работать еще длительное время. Что бы обмениваться данными между потоками, можно пользоваться переменными класса. Об этом позже.

Кроме того, существует еще один вариант метода, из которого можно сделать поток. Выглядит он вот так:

Его назначение — применение в тех случаях, когда нужно при создании потоков, передавать им какие-то данные. Любой класс в C# наследуется от Object, поэтому можно передавать внутрь потока любой объект. Подробнее позже.

Давайте напишем простой пример, что бы проиллюстрировать работу потоков. Реализуем следующее. Наша программа запустит дополнительный поток, после чего выведет три раза на экран «Это главный поток программы!», в это время созданный поток выведет три раза на экран «Это дочерний поток программы!». Вот, что получилось:

Ничего сложно. Хочу обратить ваше внимание, что у метода потока присутствует модификатор static, это нужно для того, что бы к ней можно было напрямую обратиться из главного потока приложения. Теперь запустите программу несколько раз и сравните результаты вывода в консоль. Обычно, порядок вывода сообщений в консоль при каждом запуске разный. Планировщик задач операционной системы по-разному распределяет процессорное время, поэтому порядок вывода разный. Еще одно полезное свойство потоков заключается в том, что вам не нужно беспокоиться о правильности распараллеливания их на процессоре, планировщик задач все сделает сам.

Идем дальше. Что бы создать несколько потоков, необязательно использовать несколько функций, если потоки одинаковые. Вот пример:

Как видите, для создания 10 потоков нам понадобилась всего 1 функция.

Каждый поток имеет свой стек, поэтому локальные переменные метода для каждого потока свои. Что бы это продемонстрировать, мы  создадим поток для метода класса, а потом вызовем этот же метод из главного потока. Вот код:

После завершения выполнения потоков, в консоле будет выведено 10 чисел.

Если же мы хотим, что бы каждый поток вел себя по-разному, то есть два решения. Первое — создание дополнительной функции для потока. Например, вот так:

Второе решение более запутанное. Смысл в том, что бы используя один метод выполнять разные действия в разных потоках. Для этого, внутри метода потока нужно определить «кто я?». Как вариант, используем метод потока, который может принимать значения. В поток будем передавать булевый флаг. Если он равен истине — значит это первый поток, если ложь — значит второй. Метод потока сам будет определять «кто я?» и в зависимости от этого, выполнять разные действия. Вот код:

Как работает данная программа? Каждый поток использует свой экземпляр метода. Если потоку выдали флаг true, значит он выполняет один код, если false — другой. Этот пример так же наглядно демонстрирует как работать с методами потоков, который принимают параметры.

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

Теперь давайте пример. Создадим отдельный класс, который будет создавать дополнительные потоки. Каждый поток будет работать с одной переменной (свойство класса). Для обеспечения сохранности данных мы будем использовать оператор lock. Его синтаксис очень простой:

Как он работает? Когда один из потоков, доходит до оператора lock, он проверяет, не заблокирован ли object1. Если нет — выполняет указанные в скобках операторы. Если заблокирован — ждет, когда object1 будет разблокирован. Таким образом, оператор lock предотвращает одновременное обращение нескольких потоков к одним и тем же данным.

А вот и пример программы:

Я использовал отдельную переменную, для оператора lock, поскольку он не может заблокировать доступ к int переменной. А вообще, мне как то на хабре посоветовали всегда использовать «локеры» для блокировки других данных.

Теперь о программе. Каждый поток сначала выводит значение свойства value, а потом увеличивает его на единицу. Зачем же тут нужен оператор lock? Запустите программу. Она выведет по порядку «0 1 2 3 4». Потоки по очереди выводят значение value, и все хорошо. Предположим, что оператора lock нету. Тогда может получиться следующее:

  1. Первый поток выведет 0 на экран;
  2. Второй поток выведет 0 на экран и увеличит значение на единицу;
  3. Третий поток выведет 1 на экран;
  4. Первый поток увеличит значение на единицу;
  5. Третий поток увеличит значение на единицу;
  6. Второй поток выведет 3 на экран;

… и так далее. В результате мы получим, что-то вроде «0 0 1 3 4». Это связано с тем, что процессор и планировщик задач не знают в каком порядке выполняться операции, поэтому они делают это оптимально с точки зрения выполнения программы, при этом логика нарушается.

Думаю, на сегодня хватит, и так много получилось. Основные примеры можно скачать здесь. Пишите комментарии, отзывы и пожелания. Задавайте свои ответы. Удачной компиляции.

Комментарии 35

  • Премного благодарен! Когда читал о потоках на MSDN, ни черта не понял, а это, оказывается,настолько легко…

  • спасибо за статью ) для старта именно то ))))

  • Хорошо для новичка расписано, спасибо! Только по больше бы вариантов того, как вернуть что либо из результата работы потока.

  • весьма доступно и понятно , огромное спасибо ! :3

  • Расскажите, пожалуйста, как распараллелить цикл.
    Например перемножение матриц.

  • Или, например, рекурсивные функции)

  • Очень полезная статья, особенно для новичков. Подскажите, как можно сделать многопоточный калькулятор, чтобы каждое вычисление происходило в отдельном потоке в порядке приоритетности операций ( * -> / -> — -> +)и т.д, и выводился общий вычислений.

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

    Смотрите, выражение А * B + C = X. Две операции: умножения и сложения. Вы не можете одновременно умножить и сложить, так как для сложения требуется результат операции умножения.

    Другое дело, выражения с равноправными частями. Например, A * B + C * D = X. В данном случае, можно одновременно умножить A на B и C на D, а уже после сложить результаты.

    Задавай-те вопросы, может получится ответить.

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

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

  • Или для распределённых вычислении потоки не предназначены?

  • Как запустить поток из другого класса?

    • Зависит от того, что вы конкретно имеете ввиду. У вас функция потока в одном классе, а запустить на выполнение вы хотите из другого?

      • Есть desktop-е приложение:

        класс
        public partial class Form1 : Form

        событие
        private void buttonStart_Click(object sender, EventArgs e)

        есть класс
        public class MyThread : Thread
        {
        bool started = false;

        public void run()
        {
        while (true)
        {
        mainLoopStep();
        Thread.Sleep(1000);
        }
        }

        public void startT()
        {
        started = true;
        }

        public void stopT()
        {
        started = false;
        }
        }

        и вот теперь необходимо каким-то образом из класса Form1 запустить поток из класса MyThread, но при этом необходимо что бы логи программы была реализована в классе Form1 что то типа:
        th = new Thread()
        {
        public void mainLoopStep()
        {
        count++;
        }
        }

        Подскажите пожалуйста как это можно реализовать.

        • Буду честен. Нет желания разбираться в вашем коде. По поводу логов. Почитайте по делегаты в интернете.

  • Спасибо!

  • Автору респект. Долго не мог понять кто эти потоки такие, что из себя представляют. Но автор разложил по полочкам))

  • а можно ли запустить например 1 поток для каждого процессора.
    и вообще где именно они работают в Вашем примере? — на первом ядре или как….

  • Почему у вас «&lt» вместо знаков равенства?

  • Спасибо большое, очень помогло на экзамене!))

  • Спасибо большое, я новичек в c#, 2 дня не мог понять синтаксис и работу потоков, пока не нашел вашу статью, все разжовано и предельно ясно, спасибо вам огромное!

  • Добрый день. Так кака же все таки передавать и получать данные из потоков. Опишу ситуацию. Есть программа windows form где пользователь в текстовом поле вводит url сайта. Мне нужно передать этот URL в фоновый поток, который получит исходный код, распарсит необходимые теги, и вернет эти данные в галвный поток дя вывода в пользовательский интерфейс… Честное слово уже месяца два не могу этот момент раздуплить.

  • Классно!!!

  • так а как обмениваться данными между потоками то? здесь просто по порядку выводятся данные. Как обмениваться то? в Java есть специальный класс для этого: Exchange()

    • Смотри.

      Зависит от того какие данные ты хочешь гонять туда сюда и от контекста задачи.

      К сожалению я статью писал давно, и сейчас в голове у меня уже нет свежей мысли. Кроме того, давно не писал на C# и могу забыв или не зная нюанс — посоветовать что-то не то.

      Но, всеже приведу пример.

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

      Еще один пример.

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

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

  • Отличная статья. Жду продолжения

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *