Фирма

«Инрэко ЛАН»

Пожалуй, сложно будет назвать хотя бы одно серьёзное приложение, которое бы не использовало многозадачность. И это понятно − очень неудобно работать с программой, когда она, время от времени, «залипает», выполняя свои внутренние задачи. До недавнего времени .NET Framework предоставлял разработчикам низкоуровневые средства использования многопоточности в приложениях, т.е. затраты на создание, инициализацию, запуск, сопровождение и завершение потока целиком и полностью ложились на плечи программиста (за небольшими исключениями: BackgroundWorker, разнообразные Timer'ы). В последней версии фреймворка Microsoft, осознав необходимость улучшений в сегменте параллельной обработки данных, сделала шаг навстречу разработчикам, дополнив существующие и создав новые библиотеки для более комфортной разработки многопоточных приложений.

Класс Thread

Yield() − метод позволяет передавать управление другому потоку, который готов к выполнению на том же процессоре, на котором выполняется текущий поток. Поток для передачи управления будет выбираться операционной системой. Возвращает значение типа bool, которое показывает, было ли передано управление другому потоку или нет.

Класс SemaphoreSlim

Семафор представляет собой конструкцию, которая позволяет ограничить количество потоков,  одновременно обращающихся к определённому ресурсу. Пример из жизни − лифт, вместимость которого лимитирована заводом-изготовителем. И если кабина лифта будет заполнена до предела, то единственный способ попасть в лифт − это дождаться, пока оттуда кто-нибудь выйдет. Класс SemaphoreSlim является более легковесной версией класса SemaphoreSemaphoreSlim работает в рамках одного процесса и не использует функционал своего старшего брата Semaphore, за счёт этого работает он быстрее. Простой пример использования. Допустим, необходимо ограничить количество потоков, которое может использоваться для выполнения какой-либо операции. Это, например, может потребоваться для более рационального использования ресурсов компьютера. Ниже приведён исходный код и комментарии к примеру.

class Program
{
static readonly SemaphoreSlim s = new SemaphoreSlim(2);
static void Main(string[] args)
{
new List<string>()
{
"http://inrecolan.ru",
"http://inrecolan.com",
"http://google.ru",
"http://yandex.ru",
"http://rambler.ru"
}.ForEach(url => new Thread(() => DownloadPage(url)).Start());
}

static void DownloadPage(string url)
{
Console.WriteLine("Request to download {0}.", url);
try
{
s.Wait();
Console.WriteLine("Downloading "{0}"...", url);
using (var wc = new WebClient())
{
var content = wc.DownloadString(url);
Console.WriteLine(""{0}" was downloaded. Size: {1}.", url, content.Length);
}
}
finally
{
s.Release();
}
}
}

В примере загружаются пять страниц из Интернета и выводится на экран размер каждой из них. Для загрузки страниц используется не более двух потоков одновременно. Реализация следующая: создаем семафор с указанием максимально возможного количества потоков, между вызовами методов Wait() и Release() располагаем исполняемый код, для которого нами вводились ограничения (в данном случае это получение страницы и отображение информации на экране по завершении загрузки), в основном потоке сразу запускаем все вспомогательные.

Класс Barrier

Ещё одна конструкция синхронизации работы потоков, позволяющая реализовать алгоритм, в котором несколько задач параллельно могут выполнять какие-либо действия, после чего идёт обобщение результатов, полученных в результате выполнения каждой из задач. Как видно из названия, образуется некая преграда, на которую, в конце концов, и натыкаются все потоки. И так может повторяться несколько раз, в конце каждой итерации есть возможность выполнить какие-либо действия, например, обработать промежуточные результаты, полученные от всех потоков. Для демонстрации возможностей класса Barrier воспользуемся примером выше, немного изменив его: загрузим страницы не более, чем в двух потоках, но в конце выведем на экран информацию о том, какая из них окажется самой большой по размеру.

class Program
{
    static readonly SemaphoreSlim s = new SemaphoreSlim(2);
    static Barrier barrier;
    static readonly List<Tuple<string, int>> resultList = new List<Tuple<string, int>>();
    static void Main(string[] args)
    {
        var urlList = new List<string>()
        {
            "http://inrecolan.ru",
            "http://inrecolan.com",
            "http://google.ru",
            "http://yandex.ru",
            "http://rambler.ru"
        };
        barrier = new Barrier(urlList.Count, b =>
            {
                int maxSize = resultList.Max(pageInfo => pageInfo.Item2);
                Console.WriteLine("'{0}' has maximum page size {1}.",
                    resultList.First(pageInfo => pageInfo.Item2 == maxSize).Item1,
                    maxSize);
            });
        urlList.ForEach(url => new Thread(() => DownloadPage(url)).Start());
        Console.ReadLine();
    }

    static void DownloadPage(string url)
    {
        Console.WriteLine("Request to download {0}.", url);
        try
        {
            s.Wait();
            Console.WriteLine("Downloading '{0}'...", url);
            using (var wc = new WebClient())
            {
                var content = wc.DownloadString(url);
                lock (resultList)
                {
                    resultList.Add(Tuple.Create(url, content.Length));
                }
                Console.WriteLine("'{0}' was downloaded. Size: {1}.", url, content.Length);
            }
        }
        finally
        {
            s.Release();
            barrier.SignalAndWait();
        }
    }
}

В данном примере барьер создается с использованием конструктора Barrier(int participantCount, Action<Barrier> postPhaseAction), который позволяет задать количество потоков, принимающих участие в процессе и действие, которое будет выполняться после достижения цели заданным количеством потоков (в нашем случае всеми потоками). Задействовать барьер очень просто − вызвать метод SignalAndWait() в соответствующем потоке после выполнения действий. При этом важно, чтобы поток просигнализировал в любом случае, независимо от результата загрузки страницы (может быть ошибка при скачивании), иначе мы не получим желаемого результата (не узнаем максимальный размер страницы). Видно, насколько лаконичным получился наш пример, несмотря на довольно продвинутый функционал. И можно представить, чего бы стоило реализовать подобную логику без использования специальных средств.

Класс CancellationTokenSource

Как правило, при работе с потоками рано или поздно встаёт вопрос о том, как же его остановить. Безусловно, есть методы Interrupt() и Abort(), которыми можно воспользоваться для остановки  потока, но их использование, зачастую, может оказаться небезопасным. Например, если завершение будет осуществляться в момент записи данных в файл. Класс CancellationTokenSource позволяет решить проблему безопасного завершения потока (конечно, подобную конструкцию можно реализовать самостоятельно, но тут это сделано в рамках фреймворка и по определению должно быть более грамотным и правильным; в итоге, мы имеем некий универсальный интерфейс, избавляющий нас от необходимости писать дополнительные классы и который, плюс ко всему, используется некоторыми встроенными классами). Чтобы понять, как на практике использовать это нововведение, давайте рассмотрим пример с записью в файл и воспроизведём ситуацию, описанную несколькими строками выше, т. е. завершим поток во время записи данных в файл. После исходного кода примера даны некоторые разъяснения.

class Program
{
static void Main(string[] args)
{
var cts = new CancellationTokenSource();
new Thread(() => WriteFile(cts.Token)).Start();
Console.WriteLine("Press any key to cancel...");
Console.ReadKey(true);
cts.Cancel();
Console.ReadLine();
}

static void WriteFile(CancellationToken token)
{
var fileName = "test.txt";
try
{
using (var sw = new StreamWriter(fileName))
{
for (int i = 0; i < 100; i++)
{
token.ThrowIfCancellationRequested();
sw.WriteLine(i);
Thread.Sleep(TimeSpan.FromSeconds(1));
Console.Write('.');
}
}
}
catch (OperationCanceledException)
{
Console.WriteLine("Safe cancellation...");
if (File.Exists(fileName))
{
Console.WriteLine("File is incomplete and will be deleted...");
File.Delete(fileName);
Console.WriteLine("File was deleted.");
}
}
}
}

Создаём экземпляр класса CancellationTokenSource, запускаем поток, в который через параметр  передаётся свойство Token типа CancellationToken. Через этот параметр поток будет информирован о том, что произошла отмена. Запрос на отмену выполнения потока осуществляется вызовом метода cts.Cancel(). Поток должен проверять состояние токена и, если получен сигнал об отмене, − соответствующим образом отреагировать. В примере это вывод сообщений на экран и удаление файла, поскольку он не успел сформироваться до конца. Наряду с использованием метода ThrowIfCancellationRequested() может использоваться свойство IsCancellationRequested, применение которого не вызывает генерацию ошибки и использование свойства предпочтительнее, если нет разницы при использовании метода или свойства (просто в нашем примере конструкция с методом ThrowIfCancellationRequested() подходит больше).

Метки: .NET Framework 4 | Barrier | C# | CancellationToken | Microsoft | semaphore | threading

Комментарии  

#1 Дима 25.06.2012 04:56
Парни, подскажите в каком руководстве подробнее это все почитать? А за статью респект
Цитировать

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