ANTICHAT.XYZ    VIDEO.ANTICHAT.XYZ    НОВЫЕ СООБЩЕНИЯ    ФОРУМ  
Баннер 1   Баннер 2
Antichat снова доступен.
Форум Antichat (Античат) возвращается и снова открыт для пользователей. Здесь обсуждаются безопасность, программирование, технологии и многое другое. Сообщество снова собирается вместе.
Новый адрес: forum.antichat.xyz
Вернуться   Форум АНТИЧАТ > Программирование > С/С++, C#, Delphi, .NET, Asm
   
 
 
Опции темы Поиск в этой теме Опции просмотра

[C#] FAQ Многопоточные приложения
  #1  
Старый 09.01.2010, 16:01
Аватар для Algol
Algol
Регистрация: 29.05.2002
Сообщений: 1,793
Провел на форуме:
2050916

Репутация: 0


По умолчанию [C#] FAQ Многопоточные приложения

Эта заметка скорее является не статьей, а небольшим FAQ по многопоточным приложениям в .NET.

Начнем с «детских» вопросов, затем рассмотрим более сложные:
1) Что такое поток?
2) Когда имеет смысл использовать потоки? Увеличится ли быстродействие многопоточной программы на одноядерном процессоре?
3) Зачем нужна синхронизация данных в потоках?
4) Как сделать синхронный доступ к данным.
5) Нужна ли синхронизация в однопроцессорных системах?
6) Зачем нужен Invoke в WinForms?
7) Ожидание завершения множества потоков
8) Как принудительно завершить поток?
9) Обработка ошибок в потоке
10) Накладные расходы на поток
11) Особенности пула потоков

FAQ
Q. Что такое потоки?
A. Что бы ответить на этот вопрос, представим себе работающую программу в виде строителя, строящего дом. Строитель будет аналогом процессора. У строителя есть чертеж, в котором описано как строить дом (это программный код), и есть набор строительных материалов, из чего строить дом (это исходные данные). Задача строителя – преобразовать исходные материалы в готовый дом, согласно чертежу. Такой пример – аналог простого однопоточного приложения. Все выглядит достаточно просто, пока у нас один строитель, один чертеж и один дом. А теперь представим, что у нас не один дом, а два, и оба их нужно строить по одному чертежу. Или же у нас два строителя, а нужно строить один дом. Или у нас два чертежа, два строителя и четыре разных дома. И так далее. Все это варианты многопоточных приложений. В общем случае, когда мы говорим о потоках, мы имеем ввиду, что некоторые действия нужно выполнять параллельно. Например, строить два дома. При этом строитель может быть один (однопроцессорные системы) – а может и много (многоядерные процессоры).
Итак, поток – это последовательность команд программы, которая выполняется параллельно с другими потоками. Следует отметить две особенности потоков – во-первых они могут использовать один и тот же программный код (один и тот же чертеж), и во-вторых они имеют доступ к одним и тем же данным (один и тот же склад кирпичей).

Q. Когда имеет смысл использовать потоки? Увеличится ли быстродействие многопоточной программы на одноядерном процессоре?
A. Обычно о потоках задумываются тогда, когда программа начинает «тормозить». Что бы разобраться поможет нам многопоточность или нет, нужно понять причину падения производительности. Причины бывают разные. Обычно причиной является то, что некоторый ресурс системы ограничен, либо мы используем не все его способности. Потоки нам помогут увеличить производительность, только тогда, когда некоторый ресурс системы используется не на 100% своих возможностей.
Например, у нас программа производит некоторые вычисления, основной ресурс, который она использует – процессор. Запускаем программу, смотрим в диспетчер задач – если он показывает 100% загрузку процессора, это значит что наша программа использует все возможности ресурса. В таком случае многопоточность не сможет увеличить производительность, ведь процессор и так работает на всю мощность (наш строитель бегает в поте лица, и сколько ему заданий больше не давай – он не сможет сделать больше). Если же диспетчер задач показывает 25% загрузки, и мы знаем что у нас четырехядерный процессор, то это означает, что используется только один из четырех процессоров, и мы можем повысить производительность программы, с помощью дополнительных потоков (раздав чертежи еще трем строителям - бездельникам).
Другой пример – браузер. Как правило, для браузера критическим ресурсом, ограничивающим быстродействие, является не процессор, а сеть. Смотрим на загрузку сети – если она меньше чем ширина канала, значит нам имеет смысл делать многопоточность. Если же канал занят на 100% - многопоточность нам не поможет.
А что делать если в однопоточном приложении – ресурс занят на 5 или 10%, а программа все равно тормозит? Это значит что вы смотрите не на тот ресурс.
Следует заметить, что здесь мы рассматриваем целесообразность применения потоков для повышения производительности. Однако потоки могут применяться и для других целей. Типичный пример – мы производим какую-то продолжительную операцию, и хотим что бы окно программы в это время не «висело». В этом случае имеет смысл разделить приложение на два потока – один будет реагировать на действия пользователя и отрисовывать окно, а другой – собственно будет производить вычисления. Общая производительность программы не увеличится, но интерфейс будет более приятен для пользователя.

Q. Зачем нужна синхронизация данных в потоках?
A. Очень просто – что бы наши строители не хватали один и тот же кирпич или не пытались вставить два окна в один проем. Что бы не случались такие неприятности, мы посадим на кирпичном заводе объект синхронизации – назовем его «Тетя Глаша». Суть работы тети Глаши прост – приходит строитель – она отдает ему кирпичи. Приходят два строителя – она одному отдает кирпичи, а другого просит подождать. В таком случае один кирпич достанется ровно одному строителю.
Однако доступ строителей к тете Глаше ограничен – ведь тетя одна, а строителей много. Пока тетя занята одним, другой – вынужден ждать. Отсюда мы видим, что синхронизация данных – снижает производительность приложения (а иногда, при неудачной реализации, она вырождается полностью).

Q. Как сделать синхронный доступ к данным.
A. В C# синхронизацию легче всего делать с помощью оператора lock:
Код:
Object locker = new Object();
….
void CriticalMethod()
{
	lock(locker)
	{
		//критическая секция
		//здесь мы работаем с общими для потоков данными
	}
}
Оператор lock принимает как аргумент объект синхронизации locker и допускает внутрь критической секции только один поток, остальные потоки подошедшие к lock – ожидают пока завершится поток, находящийся внутри критической секции. В качестве объекта синхронизации может выступать любой созданный (не null) объект ссылочного типа.
Следует отметить такие моменты:
1) Обратите внимание, что объект синхронизации создается вне критического метода, и его создает, как правило, главный поток. Смысл синхронизации в том, что все потоки используют один и тот же объект синхронизации. Если каждый поток создаст по своей «тете Глаше», то синхронизации никакой не произойдет.
2) Один и тот же объект синхронизации может использоваться в нескольких критических секциях. В таком случае во всех этих критических секциях одновременно может выполняться только один поток. Иными словами тетя Глаша может контролировать как кирпичный завод, так и цементный. Причем, если она пустила строителя на цементный завод, то она уже никого не пускает ни на цементный, ни на кирпичный, до тех пор, пока строитель не покинет завод.
3) Внутри критической секции нельзя снова делать lock для того же объекта синхронизации. Вот например такая рекурсивная конструкция недопустима:
Код:
void CriticalMethod()
{
	lock(locker)
	{
		//критическая секция
		…
		CriticalMethod()
	}
}
Строитель, войдя на кирпичный завод, обнаруживает, что он снова должен зайти на кирпичный завод. Но тетя Глаша его не пустит. Она женщина простая – раз строитель не вышел из критической секции она никого не пустит, в том числе и того же самого строителя. Такая ситуация называется Тупик, Взаимная блокировка, deadlock. Следствием тупика является зависание программы.

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

Как пример, рассмотрим многопоточное приложение, которое занимается скачиванием сайтов. В качестве исходных данных – у нас будет выступать очередь из URL сайтов, а на выходе мы хотим получить список скачанных HTML страничек. Мы будем работать в 3 потока.
Код:
/*Внимание, этот демонстрационный пример не обрабатывает ошибки, и не следит за завершением потоков*/
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using System.Net;
using System.IO;

namespace Downloader
{
    class Program
    {
        //очередь адресов для закачки
        static Queue<string> URLs = new Queue<string>();
        //список скачанных страниц
        static List<string> HTMLs = new List<string>();
        //локер для очереди адресов
        static object URLlocker = new object();
        //локер для списка скачанных страниц
        static object HTMLlocker = new object();

        static void Main(string[] args)
        {
            URLs.Enqueue("http://microsoft.com");
            URLs.Enqueue("http://google.com");
            URLs.Enqueue("http://ya.ru");
            URLs.Enqueue("http://forum.antichat.ru");
            //создаем и запускаем 3 потока
            for (int i = 0; i < 3;i++)
                (new Thread(new ThreadStart(Download))).Start();
            //ожидаем нажатия Enter
            Console.ReadLine();
        }

        public static void Download()
        {
            //будем крутить цикл, пока не закончатся ULR в очереди
            while (true)
            {
                string URL;
                //блокируем очередь URL и достаем оттуда один адрес
                lock (URLlocker)
                {
                    if (URLs.Count == 0)
                        break;//адресов больше нет, выходим из метода, завершаем поток
                    else
                        URL = URLs.Dequeue();
                }
                Console.WriteLine(URL + " - start downloading ...");
                //скачиваем страницу
                WebRequest request = WebRequest.Create(URL);
                HttpWebResponse response = (HttpWebResponse)request.GetResponse();
                string HTML = (new StreamReader(response.GetResponseStream())).ReadToEnd();
                //блокируем список скачанных страниц, и заносим туда свою страницу
                lock (HTMLlocker)
                    HTMLs.Add(HTML);
                //
                Console.WriteLine(URL + " - downloaded (" + HTML.Length+" bytes)");
            }
        }
    }
}
(Внимание! Этот пример – рабочий, но в нем нет обработки ошибок и контроля завершения потоков, эти вещи мы рассмотрим позже.)

В приведенном примере мы создали три потока. Стартовым методом для каждого потока был метод Download(). Поток существует до тех пор, пока не выполнится метод Download(). Как только этот метод завершается, завершается и поток. В этом примере главный поток (тот, который создает остальные потоки) не ожидает завершения дочерних потоков и не отслеживает их завершения. Как только пользователь нажмет Enter( в точке Console.ReadLine();), главный поток завершится, а дочерние будут продолжать работать, пока не будет исчерпана очередь адресов. Приложение завершится только тогда, когда завершатся все его потоки.

Обратите внимание – в этом примере у нас две критические секции – в одной из них мы берем URL из очереди адресов. Здесь синхронизация нужна для того, что бы потоки не хватали одни и те же URL из очереди. Вторая критическая секция нужна для занесения результатов в выходной список – что бы результаты не занеслись в один и тот же элемент списка. Весь длительный фрагмент кода (скачивание страниц) вынесен из критических секций. Если бы скачивание было непосредственно внутри критической секции lock (URLlocker){…} то многопоточность потеряла бы смысл, так как все потоки ждали бы пока один из них скачает страницу (это антипример, демонстрирующий вырождение многопоточного приложения в однопоточное).

Обратим внимание еще на то, что в качестве локера можно было бы использовать сами объекты URLs и HTMLs, но обычно так делать не рекомендуют, по ряду соображений из области ООП.

A. Нужна ли синхронизация в однопроцессорных системах?
Q. Казалось бы, если процессор один, то физически невозможна ситуация когда происходит одновременный доступ к данным, даже если потоков – несколько. Однако синхронизация все равно нужна. Причиной этому является эффект под странным названием Гонки. Дело в том, что для выполнения нескольких потоков процессор применяет квантование времени: на поток отводится некий промежуток времени, в течении которого он занимает процессор, когда квант времени заканчивается, поток
приостанавливается и квант времени выделяется другому потоку и т.д. А это значит, что наш поток может быть прерван в любой точке программы. Теперь рассмотрим простую операцию внесения элемента в массив:
Код:
a[i]=value;
i++;
Здесь значение заносится в массив, а индекс увеличивается на единицу. Теперь представим, что этот фрагмент будет выполняться в нескольких потоках. Один из потоков занес число 12 в массив, и тут у него заканчивается квант времени. При этом, i++ выполнится не успеет. Далее, второй поток заносит значение 36 в массив, и заносит он его в ту же позицию, где первый поток сохранил значение 12, ведь i по прежнему тоже смое! Далее второй поток делает i++, затем управление возвращается к первому потоку, он делает снова i++. В итоге индекс i увеличился на 2, но оба значения были занесены в одну и ту же ячейку массива!
Для устранения гонок – используйте синхронизацию, как было показано выше.


A. Зачем нужен Invoke в WinForms?
Q. Визуальные компоненты WinForms устроены таким образом, что доступ к ним разрешается только из главного потока. Не будем рассматривать почему так происходит, просто примем это как факт. А что же делать, если нужно обратится к контролу из дочернего потока? В таком случае, у каждого наследника Control есть метод Invoke, заставляющий главный поток выполнить метод, который указан в Invoke.

В примере поток заносит значение 12 в textBox формы:
Код:
        delegate void ParametrizedMethodInvoker(object arg);

	  ///этот метод формы выполняется в дочернем потоке
        private void SomeThreadMethod()
        {
            int n = 12;
		//здесь мы не можем напрямую занести значение в textBox1.Text
		//поэтому мы вопсользуемся Invoke, что бы значение присвоил
		//главный поток
            Invoke(new ParametrizedMethodInvoker(ShowNumberInTextBox), n);
        }

        //этот метод будет выполняться главным потоком
        void ShowNumberInTextBox(object arg)
        {
            textBox1.Text = arg.ToString();
        }
Ту же самую задачу можно решить другим способом – что бы не усложнять код вызывающих методов, можно сделать так – дочерний поток напрямую вызывает метод ShowNumberInTextBox, а этот метод уже сам делает Invoke самого себя, что бы выполнится в главном потоке. Для такой схемы есть свойство контрола InvokeRequired. Это свойство возвращает True, если оно вызвано не из главного потока. Тогда пример можно переписать так:
Код:
        delegate void ParametrizedMethodInvoker(object arg);

	    ///этот метод формы выполняется в дочернем потоке
        private void SomeThreadMethod()
        {
            int n = 12;
            ShowNumberInTextBox(n);
        }

        //этот метод будет выполняться главным потоком
        void ShowNumberInTextBox(object arg)
        {
            if(InvokeRequired)
            {
                //если мы не в главном потоке - то вызовем себя через инвокер
                Invoke(new ParametrizedMethodInvoker(ShowNumberInTextBox), arg);
                return;
            }
            //это присвоение всегда будет происходить только в главном потоке
            //независимо от того в каком потоке мы вызывает данный метод
            textBox1.Text = arg.ToString();
        }
Tags: multithreading, synchronize

Последний раз редактировалось Algol; 09.01.2010 в 20:11..
 
Ответить с цитированием
 



Похожие темы
Тема Автор Раздел Ответов Последнее сообщение
Продаю приложения для ВК ZnikiR Покупка, продажа, услуги в Соц. Сетях 11 01.10.2009 22:42
Sale of ICQ Hertz ICQ - Покупка, продажа 1 28.09.2009 04:27
Введение в Symfony Framework gibson Авторские статьи 1 30.04.2009 11:15
Ru.phreak Faq silveran Электроника и Фрикинг 0 21.07.2005 10:20



Здесь присутствуют: 1 (пользователей: 0 , гостей: 1)
 


Быстрый переход




ANTICHAT.XYZ