[C#] FAQ Многопточные приложения

Discussion in 'С/С++, C#, Rust, Swift, Go, Java, Perl, Ruby' started by Algol, 9 Jan 2010.

  1. Algol

    Algol New Member

    Joined:
    29 May 2002
    Messages:
    1,759
    Likes Received:
    4
    Reputations:
    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:
    Code:
    Object locker = new Object();
    ….
    void CriticalMethod()
    {
    	lock(locker)
    	{
    		//критическая секция
    		//здесь мы работаем с общими для потоков данными
    	}
    }
    Оператор lock принимает как аргумент объект синхронизации locker и допускает внутрь критической секции только один поток, остальные потоки подошедшие к lock – ожидают пока завершится поток, находящийся внутри критической секции. В качестве объекта синхронизации может выступать любой созданный (не null) объект ссылочного типа.
    Следует отметить такие моменты:
    1) Обратите внимание, что объект синхронизации создается вне критического метода, и его создает, как правило, главный поток. Смысл синхронизации в том, что все потоки используют один и тот же объект синхронизации. Если каждый поток создаст по своей «тете Глаше», то синхронизации никакой не произойдет.
    2) Один и тот же объект синхронизации может использоваться в нескольких критических секциях. В таком случае во всех этих критических секциях одновременно может выполняться только один поток. Иными словами тетя Глаша может контролировать как кирпичный завод, так и цементный. Причем, если она пустила строителя на цементный завод, то она уже никого не пускает ни на цементный, ни на кирпичный, до тех пор, пока строитель не покинет завод.
    3) Внутри критической секции нельзя снова делать lock для того же объекта синхронизации. Вот например такая рекурсивная конструкция недопустима:
    Code:
    void CriticalMethod()
    {
    	lock(locker)
    	{
    		//критическая секция
    		…
    		CriticalMethod()
    	}
    }
    Строитель, войдя на кирпичный завод, обнаруживает, что он снова должен зайти на кирпичный завод. Но тетя Глаша его не пустит. Она женщина простая – раз строитель не вышел из критической секции она никого не пустит, в том числе и того же самого строителя. Такая ситуация называется Тупик, Взаимная блокировка, deadlock. Следствием тупика является зависание программы.

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

    Как пример, рассмотрим многопоточное приложение, которое занимается скачиванием сайтов. В качестве исходных данных – у нас будет выступать очередь из URL сайтов, а на выходе мы хотим получить список скачанных HTML страничек. Мы будем работать в 3 потока.
    Code:
    /*Внимание, этот демонстрационный пример не обрабатывает ошибки, и не следит за завершением потоков*/
    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. Казалось бы, если процессор один, то физически невозможна ситуация когда происходит одновременный доступ к данным, даже если потоков – несколько. Однако синхронизация все равно нужна. Причиной этому является эффект под странным названием Гонки. Дело в том, что для выполнения нескольких потоков процессор применяет квантование времени: на поток отводится некий промежуток времени, в течении которого он занимает процессор, когда квант времени заканчивается, поток
    приостанавливается и квант времени выделяется другому потоку и т.д. А это значит, что наш поток может быть прерван в любой точке программы. Теперь рассмотрим простую операцию внесения элемента в массив:
    Code:
    a[i]=value;
    i++;
    Здесь значение заносится в массив, а индекс увеличивается на единицу. Теперь представим, что этот фрагмент будет выполняться в нескольких потоках. Один из потоков занес число 12 в массив, и тут у него заканчивается квант времени. При этом, i++ выполнится не успеет. Далее, второй поток заносит значение 36 в массив, и заносит он его в ту же позицию, где первый поток сохранил значение 12, ведь i по прежнему тоже смое! Далее второй поток делает i++, затем управление возвращается к первому потоку, он делает снова i++. В итоге индекс i увеличился на 2, но оба значения были занесены в одну и ту же ячейку массива!
    Для устранения гонок – используйте синхронизацию, как было показано выше.


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

    В примере поток заносит значение 12 в textBox формы:
    Code:
            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, если оно вызвано не из главного потока. Тогда пример можно переписать так:
    Code:
            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
     
    #1 Algol, 9 Jan 2010
    Last edited: 9 Jan 2010
  2. Algol

    Algol New Member

    Joined:
    29 May 2002
    Messages:
    1,759
    Likes Received:
    4
    Reputations:
    0
    Q. Как сделать ожидание завершения множества потоков из главного потока?
    A. Основная идея проста – создаем массив флажков, по одному флажку на поток. Когда поток отработал – он устанавливает свой флажок. Главный поток в это время ожидает, пока все флажки будут установлены - и когда это происходит – продолжает свое выполнение. Для нашего примера программы скачивания сайтов это будет выглядеть так:
    Code:
    …
            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");
                //создаем массив хендлеров, для контроля завершения потоков
                ManualResetEvent[] handles = new ManualResetEvent[3];
                //создаем и запускаем 3 потока
                for (int i = 0; i < 3; i++)
                {
                    handles[i] = new ManualResetEvent(false);
                    (new Thread(new ParameterizedThreadStart(Download))).Start(handles[i]);
                }
                //ожидаем, пока все потоки отработают
                WaitHandle.WaitAll(handles);
                //
                Console.WriteLine("Download completed");
                Console.ReadLine();
         }
    
            public static void Download(object handle)
            {
                //будем крутить цикл, пока не закончатся ULR в очереди
                while (true)
                {
                    …
                }
                //устанавливаем флажок хендла, что бы сообщить главному потоку о том, что мы отработали
                ((ManualResetEvent)handle).Set();
            }
    
    Здесь главный поток запускает три дочерних потока, которые начинают скачивать сайты. Пока они работают – главный поток ожидает в точке WaitHandle.WaitAll(handles);. Как только все три потока завершат работу – главный поток продолжит свое выполнение.

    Для ожидания завершения потоков в .Net Framework есть набор специальных объектов. Здесь мы воспользовались двумя из них: ManualResetEvent и WaitHandle.

    В приведенном примере есть одна загвоздка. Приложение может работать в двух режимах MTA и STA, здесь мы не будем рассматривать что это значит (welcome to Google). Так вот, в режиме STA метод WaitHandle.WaitAll(handles); не работает. Что бы выкрутиться из такой ситуации, можно использовать свой метод WaitAll, вместо приведенного:
    Code:
            /// Ожидаем завершения всех потоков
            static void WaitAll(WaitHandle[] waitHandles)
            {
                if (Thread.CurrentThread.ApartmentState == ApartmentState.STA)
                    // WaitAll для STA не поддерживается, поэтому делаем это вручную
                    foreach (WaitHandle myWaitHandle in waitHandles)
                        WaitHandle.WaitAny(new WaitHandle[] { myWaitHandle });
                else
                    //Вызываем стандартный метод
                    WaitHandle.WaitAll(waitHandles);
            } 
    Q. Как принудительно завершить поток?
    A. Вообще говоря – никак :). Поток – это отдельный и самостоятельный кусок кода, который продолжает работать, пока не выполнится целиком. Собственно любая программа (однопоточная) работает также. Гарантированно завершить поток можно только сняв процесс в диспетчере задач. Но здесь мы все же рассмотрим случаи негарантированного завершения потоков.
    Случай первый – хороший. Это случай корректного завершения потока. Под корректностью понимается то, что поток завершит какой-то участок своей работы и затем просто завершится, не выполняя оставшуюся работу, но и не теряя сделанную. Автоматических средств такого процесса быть не может потому что понятие “корректности” завершения зависит от вашего алгоритма, который вы реализуете в потоке. В приведенном примере выше, мы бы могли реаилзовать следующий простой алгоритм: создать глобальный флажок bool stopThreads, а в методе Download() регулярно проверять его перед тем, как начать скачивать новую страницу. Вот так
    Code:
    bool stopThreads = false;
    …
    void Download()
    {
    	while(true)
    	{
    		if(stopThreads)
    		break;
    		...
    	}
    }
    (здесь по-хорошему нужно было бы использовать ManualResetEvent вместо bool, но не будем усложнять пример)

    Далее, когда пользователь хочет прервать скачивание сайтов, главный поток устанавливает stopThreads = true, и потоки завершают выполнение, после скачивания текущего сайта.

    Случай второй – плохой. Все бы хорошо, но нужно понимать, что описанный вариант в первом случае работает только если отдельная задача, которую выполняет поток в цикле – выполняется довольно быстро. А если поток производит вычисления, которые выполняются часами? Если, например, программа скачивает гигабайтный файл? Хотелось бы иметь механизм прерывания потока не только в точке проверки флажка, но и в любой точке. Для этого у объекта Thread есть метод Abort(). Он прерывает поток, вызывая в нем исключение типа ThreadAbortException. Таким образом, если главный поток вызовет метод Abort для каждого из потоков, они все сгенерируют исключение и прервутся.
    К сожалению, применение Abort – не очень хорошее решение. Во-первых это не является корректным завершением потока, поскольку исключение может возникнуть в любой точке кода потока, и поток не успеет сохранить ту работу, которую он уже сделал. Во-вторых злостный поток может обработать ThreadAbortException простым try{}catch{}, и свободно продолжать выполнение. И в-третьих – самое неприятное то, что Abort работает только внутри управляемого кода. Вызов Abort в то время как поток выполняет неуправляемый код (например ожидание сокета) – не приведет ни к какому результату. Исключение будет сгенерировано только в тот момент, когда начнет выполняться управляемый код. Таким образом Abort делает некорректное завершение потока, да и то – не гарантированное и не сиюминутное.

    Случай третий – совсем плохой. Если уж нам приспичило стопроцентно завершить приложение, вместе со всеми потоками, и нам начхать на несохраненные данные и вообще на все – мы просто хотим выйти – можно применить следующий код:
    Code:
                Process currentProcess = Process.GetCurrentProcess();
                int pid = currentProcess.Id;
                Process.Start(Application.StartupPath + "tskill.exe", pid.ToString()); 
    (предварительно нужно скопировать файл tskill.exe из C:\WINDOWS\system32\tskill.exe в папку с программой)

    Здесь мы просто получаем идентификатор своего процесса, и просим ОС завершить данный процесс, вместе со всеми потоками, запустив программку tskill.exe, которая входит в стандартный набор утилит Windows.
    Другой вариант (правда это не всегда возможно) – установить свойство IsBackground для дочерних потоков. Background потоки не отличаются о обычных, за исключением того, что они автоматически завершаются, если завершается процесс в которым они работают.

    A. Обработка ошибок в потоке
    Q. Обработка ошибок – еще одна из проблем, возникающих в многопоточных приложениях. Решение этой пролблемы специфично для каждой задачи и я приведу лишь одно из всевозможных решений.
    Во-первых, определим для себя, что именно нужно делать, если в процессе работы возникает исключение (политика обработки исключений). Допустим, у нас есть все та же программа – загрузчик сайтов. И я хочу, что бы при любой возникающей ошибке в потоках, потоки продолжали работу, но в конце работы программы, ошибки отобразились бы пользователю. Именно так и сделаем.
    Но перед этим отметим для себя следующую особенность потоков: поскольку поток выполняется не в главном потоке приложения (извините за каламбур), то блок try{}catch{} в методе который создает потоки – не сможет отловить ошибки самих потоков. Это значит, что отлавливать ошибки потоков нужно внутри того метода, где собственно поток работает.
    Итак, более совершенная версия нашего загрузчика, в котором реализована наша политика обработки исключений, ожидание завершения всех потоков и сохранение результатов в файлы:
    Code:
    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 Queue<Exception> exceptions = new Queue<Exception>();
    
            static void Main(string[] args)
            {
                URLs.Enqueue("http://microsoft.com");
                URLs.Enqueue("http://google.com");
                URLs.Enqueue("http://ya.ru");
                URLs.Enqueue("http://gfdgjfhjkgfjgfds.com");//запросим несуществующий сайт, что бы вызвать исключение
                URLs.Enqueue("http://forum.antichat.ru");
                //создаем массив хендлеров, для контроля завершения потоков
                ManualResetEvent[] handles = new ManualResetEvent[3];
                //создаем и запускаем 3 потока
                for (int i = 0; i < 3; i++)
                {
                    handles[i] = new ManualResetEvent(false);
                    (new Thread(new ParameterizedThreadStart(Download))).Start(handles[i]);
                }
                //ожидаем, пока все потоки отработают
                WaitHandle.WaitAll(handles);
                //проверяем ошибки, если были - выводим
                foreach (Exception ex in exceptions)
                    Console.WriteLine(ex.Message);
                //сохраняем закачанные страницы в файлы
                try
                {
                    for (int i = 0; i < HTMLs.Count; i++)
                        File.WriteAllText("c:\\" + i + ".html", HTMLs[i]);
                    Console.WriteLine(HTMLs.Count+" files saved");
                }
                catch(Exception ex) { Console.WriteLine(ex); }
                //
                Console.WriteLine("Download completed");
                Console.ReadLine();
            }
    
            public static void Download(object handle)
            {
                //будем крутить цикл, пока не закончатся ULR в очереди
                while (true)
                try
                {
                    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)");
                }
                catch (ThreadAbortException)
                {
                    //это исключение возникает если главный поток хочет завершить приложение
                    //просто выходим из цикла, и завершаем выполнение
                    break;
                }
                catch (Exception ex)
                {
                    //в процессе работы возникло исключение
                    //заносим ошибку в очередь ошибок, предварительно залочив ее
                    lock (exceptions)
                        exceptions.Enqueue(ex);
                    //берем следующий URL
                    continue;
                }
                //устанавливаем флажок хендла, что бы сообщить главному потоку о том, что мы отработали
                ((ManualResetEvent)handle).Set();
            }
        }
    } 
    Обратим внимание, что потоки сами отлавливают возникающие в них исключения, и складывают их в очередь исключений (разумеется, с синхронизацией), а затем продолжают работать. Главный поток, после завершения всех потоков, отображает пользователю возникшие исключения.
    В результате работы был получен такой результат:
    Code:
    http://microsoft.com - start downloading ...
    http://ya.ru - start downloading ...
    http://google.com - start downloading ...
    http://ya.ru - downloaded (4818 bytes)
    http://gfdgjfhjkgfjgfds.com - start downloading ...
    http://forum.antichat.ru - start downloading ...
    http://microsoft.com - downloaded (1020 bytes)
    http://forum.antichat.ru - downloaded (103709 bytes)
    http://google.com - downloaded (7720 bytes)
    The remote name could not be resolved: 'gfdgjfhjkgfjgfds.com'
    4 files saved
    Download completed
    A. Накладные расходы на поток.
    Q. Когда мы обсуждали применимость потоков для повышения производительности, мы не учитывали, что сам по себе поток также может занимать ресурсы системы. Настало время немного поговорить и об этом. Рассмотрим два заблуждения, относительно потоков:
    Заблуждение первое: сделав два потока вместо одного, мы повысим производительность в два раза. Это не верно. Во-первых потому что ресурс который мы делим между потоками может просто не дать нам повысить производительность. Например, если сеть имеет ширину канала 100mbs, и один поток использовал из них 60mbs, то понятно, что второй поток сможет использовать только оставшихся 40mbs, но никак не 60mbs. Во-вторых само по себе содержание потоков, довольно расточительно для системы. Само по себе переключение между потоками занимает процессорное время. Кроме того, потоки не работают с максимальной эффективностью, из-за необходимой синхронизации доступа к данным.
    Заблуждение второе: мы можем создать любое необходимое число потоков. Это не так.
    Нужно помнить, что создание потока – ресурсоемкая операция. Так, при создании потока за ним сразу закрепляется 1mb памяти под стек. Это значит, что если у вас памяти имеет размер 2ГБ, то вы не сможете создать более 2000 потоков. Размер стека определяется линковщиком, и для VS это значение по умолчанию равно 1 мегабайту. Однако, мы можем изменить размер стека при создании потока. Для этого нужно использовать конструктор Thread(ThreadStart start, int maxStackSize). Здесь второй параметр задает размер стека для потока. Однако даже в таком случае система выделяет минимум 64КБ под стек (или 256КБ для Vista и Win7), меньший стек сделать нельзя.

    Q. Особенности пула потоков.
    A. Для решения проблемы накладных расходов при создании потоков, в .NET Framework существует пул потоков ThreadPool. Суть его проста – создается массив, содержащий потоки Thread. Когда нужно выполнить какую-то задачу, пул не создает поток заново, а просто берет уже созданный поток и в нем запускает выполнение кода. Для этого он конечно выбирает не занятый поток. А если все потоки пула заняты? Тогда пул создает новый поток и заносит его в список своих потоков. По умолчанию, пул содержит 25 потоков на каждый системный процессор. То есть, если у вас двуядерный процессор, то пул будет содержать 50 потоков.
    Применение пула потоков оправдано, когда число потоков невелико и они часто создаются/уничтожаются. Если же вам нужно создать, например, 200 потоков – использовать пул – плохая идея. Это может привести к существенному падению производительности. Дело в том, что если у пула не хватает потоков для выполнения задач он создает новые потоки, но лишь по одному в секунду (приблизительно). Таким образом, на создание 200 потоков пулу потребуется более трех минут! Что бы убедится в этом, а заодно и привести пример использования пула потоков приведем код (под .NET Framework 3.5):
    Code:
     using System;
    using System.Collections.Generic;
    using System.Text;
    using System.Threading;
    
    namespace ConsoleApplication19
    {
        class Program
        {
            static void Main(string[] args)
            {
                for(int i=0;i<200;i++)
                {
                    ThreadPool.QueueUserWorkItem(
                        new WaitCallback(delegate(object s)
                            {
                                //внутри потока - выводим сообщение и спим минуту
                                Console.WriteLine("Hi from thread at "+DateTime.Now);
                                Thread.Sleep(60000);
                            }));
                }
                Console.ReadLine();
            }
        }
    }
    Казалось бы данный код должен мгновенно вывести 200 сообщений «Hi from thread», но этого не произойдет. Сначала будет быстро выведено несколько сообщений (это сработают те потоки, которые уже были в пуле), а затем сообщения будут выдаваться очень медленно – по одному в секунду. Пул создает потоки постепенно, поскольку бережет ресурсы системы (об этом можете почитать более подробно у Рихтера, например). Но факт тот, что если вам нужно быстро создать много потоков – пул не для вас.
    Кстати из-за этой особенности пула, наблюдаются и другие артефакты, в системах, которые используют пул. Например, .Net Remouting не позволяет быстро и одновременно обработать много запросов на сервер, поскольку берет потоки для коннектов – из пула.
     
    #2 Algol, 9 Jan 2010
    Last edited: 9 Jan 2010
  3. .ATK

    .ATK Active Member

    Joined:
    20 Feb 2009
    Messages:
    143
    Likes Received:
    111
    Reputations:
    9
    Спасибо за труды! Счас ознакомлюсь!
     
  4. [Life]

    [Life] Banned

    Joined:
    7 Jan 2010
    Messages:
    74
    Likes Received:
    3
    Reputations:
    0
    ждем продолжения...
    очень интересная статья...
     
  5. cel1697i845

    cel1697i845 Elder - Старейшина

    Joined:
    22 Nov 2008
    Messages:
    618
    Likes Received:
    396
    Reputations:
    80
    Благодарю за помощь в получение новых знаний.
     
  6. оlbaneс

    оlbaneс Moderator

    Joined:
    5 Nov 2007
    Messages:
    1,378
    Likes Received:
    1,097
    Reputations:
    356
    очень доходчиво. спасибо.
     
    _________________________
  7. Algol

    Algol New Member

    Joined:
    29 May 2002
    Messages:
    1,759
    Likes Received:
    4
    Reputations:
    0
    Запостил вторую часть статьи
     
  8. W!z@rD

    W!z@rD Борец за русский язык

    Joined:
    12 Feb 2006
    Messages:
    973
    Likes Received:
    290
    Reputations:
    43
    Оставшееся время обеда посвятил топику и хотелось бы кое что добавить. =)
    во-первых, что почитать:
    Само по себе классика Рихтер, но вот еще раз и два
    во-вторых...
    Бывают такие ситуации когда необходим куче потоков доступ на чтение и лишь один поток изменяет эти данные (либо наоборот)...
    Представим банальную ситуацию, несколько потоков ведут журналирование работы в единый объект из которого один читает.
    Для этого конечно же можно попыхтеть с секциями, следить за блокировками и тому подобное. Но можно поступить и проще, в .NET есть такой класс как - ReaderWriterLockSlim

    Ниже идет пример с использованием класса:
    PHP:
    using System;
    using System.Threading;

    namespace 
    SharedRes
    {
        class 
    ByClass
        
    {
            private 
    string param//переменная для свойства
            
    private readonly ReaderWriterLockSlim rwl = new ReaderWriterLockSlim();

            
    /// <summary>
            /// Свойство используемое потоками
            /// </summary>
            
    public string SharedRes
            
    {
                
    get
                
    {
                    var 
    "";
                    try
                    {
                        
    rwl.EnterReadLock();
                        
    param;
                    }
                    catch(
    Exception e)
                    {
                        
    Console.WriteLine(e.Message); //Ловим исключения и выводим сообщение об ошибке
                    
    }
                    finally
                    {
                        
    rwl.ExitReadLock();
                    }
                    return 
    s;
                }

                
    set
                
    {
                    try
                    {
                        
    rwl.EnterWriteLock();
                        
    param value;
                    }
                    finally
                    {
                        
    rwl.ExitWriteLock();
                    }
                }
            }
        }
    }
    а тут без использования класса =)
    PHP:
    using System;
    using System.Threading;

    namespace 
    SharedRes
    {
        class 
    ByLock
        
    {
            private 
    readonly ManualResetEvent mreGet = new ManualResetEvent(true);
            private 
    readonly ManualResetEvent mreSet = new ManualResetEvent(true);
            private 
    int readers;
            private 
    string param;
            private 
    readonly object locker = new object();

            
    /// <summary>
            /// Свойство используемое потоками
            /// </summary>
            
    public string SharedRes
            
    {
                
    get
                
    {
                    try
                    {
                        
    mreGet.WaitOne();
                        
    mreGet.Reset();
                        
    lock (locker)
                        {
                            
    Interlocked.Increment(ref readers); //Увеличивание счетчика на 1
                        
    }
                        var 
    param;
                        
    lock (locker)
                        {
                            
    Interlocked.Decrement(ref readers); //Уменьшение счетчика на 1
                        
    }
                        return 
    s;
                    }
                    finally
                    {
                        
    lock (locker)
                        {
                            if (
    readers == 0)
                            {
                                
    mreGet.Set();
                            }
                        }
                    }
                }

                
    set
                
    {
                    try
                    {
                        
    mreGet.Reset();
                        
    mreSet.WaitOne();
                        
    mreSet.Reset();
                        
    param value;
                        
    mreGet.Set();
                        
    mreSet.Set();
                    }
                    catch(
    Exception e)
                    {
                        
    Console.WriteLine(e.Message); //Ловим исключения и выводим сообщение об ошибке
                    
    }
                }
            }
        }
    }
    Сорцы (VS 2008)
     
  9. Fata1ex

    Fata1ex Elder - Старейшина

    Joined:
    12 Dec 2006
    Messages:
    703
    Likes Received:
    300
    Reputations:
    38
    Писал нечто подобное по С++. Может все-таки кому-нибудь пригодиться :(
    link
     
  10. cheater_man

    cheater_man Member

    Joined:
    13 Nov 2009
    Messages:
    651
    Likes Received:
    44
    Reputations:
    7
    Честно говоря статья не впечатлила :(
    Code:
    (new Thread(new ParameterizedThreadStart(Download))).Start
    
    Этот метод запуска потока не универсален, но применним в ЭТОМ случае.
    Не раскрыта тайна класса Monitor.
    Пример
    Ну и еще много другого. Для начинающего программиста на C# это материал будет бесполезен. А вот за BeginInvoke молодец, что включил в статью.
     
  11. Algol

    Algol New Member

    Joined:
    29 May 2002
    Messages:
    1,759
    Likes Received:
    4
    Reputations:
    0
    Не знал прот такой класс.
    Но лично я не сторонник использования классов типа СуперПуперГиперТамкакойТоРидер. Мне легче написать просто lock, который я точно знаю как будет работать, и с помощью которого я сделаю все что мне нужно. Использование же более сложных синхронизаторов треубет более высокого уровня входа, их нужно изучать и т.д., и при этом отдача от них - я думаю не больше чем от простого локера.
     
  12. nerezus

    nerezus Banned

    Joined:
    12 Aug 2004
    Messages:
    3,191
    Likes Received:
    729
    Reputations:
    266
    Не рассмотрено создание пула потоков под обработку задачи.
     
  13. Algol

    Algol New Member

    Joined:
    29 May 2002
    Messages:
    1,759
    Likes Received:
    4
    Reputations:
    0
    Ну всем не угодишь :)
    Какая такая там тайна ? Тот же лок использует Monitor. Я привел самые простые решения, годящиеся для начинающих. Зачем ломать мозг об Monitor, если есть lock?
    Нельзя объять необъятное. Вы хотите что б я тут целую книгу накатал?
    Уверен, тут еще будет 20 постов, и в каждом кто-то скажет, а вот в статье нет описания <AnyClass> :D
     
  14. NetSter

    NetSter Moderator

    Joined:
    30 Jul 2007
    Messages:
    810
    Likes Received:
    414
    Reputations:
    62
    Спасибо большое за отличную и доходчивую подборку инфо!
     
    _________________________
  15. NullByte

    NullByte Member

    Joined:
    8 Jun 2010
    Messages:
    25
    Likes Received:
    6
    Reputations:
    0
    Освоил потоки именно по этой статье, большое спасибо=)
     
  16. FEV

    FEV Member

    Joined:
    23 Sep 2009
    Messages:
    15
    Likes Received:
    7
    Reputations:
    0
    немного удивляет 3 случай завершения потоков, путем убийства приложения с использованием tskill.exe

    гораздо проще при создании потоков устанавливать св-во IsBackground в true и в главном потоке делать Environment.Exit(0);
     
  17. n0_lim1t

    n0_lim1t New Member

    Joined:
    25 Apr 2010
    Messages:
    0
    Likes Received:
    0
    Reputations:
    0
    Спасибо, хороший фак! :) Никто не знает как сделать ожидание завершения потоков в Windows Forms-приложении? Если делать как советует ТС, то UI зависает, может нужно как-то через бекграунд воркер?