суббота, 12 мая 2012 г.

Часть 2. Процессы и потоки

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

Собственно программа, которая будет заниматься решением задачи, будет представлять из себя консольное приложение. Она будет получать в виде параметров командной строки:
  • строку, которую необходимо найти,
  • путь до файла, который необходимо анализировать,
  • путь до файла, в который необходимо записать результат.
Вот код консольного приложения Worker:
namespace Worker
{
    class Program
    {
        static void Main(string[] args)
        {
            if (args != null && args.Length == 3)
            {
                string target = args[0];
                string sourcePath = args[1];
                string distPath = args[2];
                if (File.Exists(sourcePath))
                {
                    StreamReader reader = new StreamReader(sourcePath);
                    int position = -1;
                    while (!reader.EndOfStream && reader.ReadLine() != target)
                    {
                        position++;
                    }
                    reader.Close();
                    StreamWriter writer = new StreamWriter(distPath, true);
                    writer.WriteLine(position);
                    writer.Close();                   
                }
            }
        }
    }
}
Как видим ничего сложного. Теперь давайте обсудим, как нам программно запускать такие консольные приложения, передавать им имена файлов, и анализировать результаты.
Для работы с процессами в C# есть класс Process. Этот класс позволяет получать доступ к локальным и удаленным процессам, создавать новые процессы, ну и управлять всем этим.
В рамках поставленной задачи, нам понадобится свойство класса Process: StartInfo. Оно позволяет задать параметры для старта нового процесса. Тип этого свойства – ProcessStartInfo. А у класса ProcessStartInfo, в свою очередь, есть два свойства: FileName и Arguments, как раз то, что нам нужно. Еще нам понадобиться методами класса Process:  Start (с ним я думаю все понятно) и WaitForExit (вызвав этот метод, мы приостановим главную программу, до завершения дочернего процесса). Для проверки времени работы в режиме, когда процесс для обработки файлов запускаются последовательно, и в режиме, когда запускаются параллельно, воспользуемся двумя консольными приложениями.
Сначала разберем пример с последовательным запуском процессов. Для такого запуска воспользуемся вот таким приложением:

namespace ProcessRuner
{
    class Program
    {       
        static void Main(string[] args)
        {
            Process first, second;
            DateTime start, end;
            // Настройка процессов
            first = new Process();
            first.StartInfo.FileName = "worker.exe";
            first.StartInfo.Arguments = "Hello 1.inp 1.out";
            second = new Process();
            second.StartInfo.FileName = "worker.exe";
            second.StartInfo.Arguments = "Hello 2.inp 2.out";
            // Засекаем время
            start = DateTime.Now;
            // Запускаем первый процесс           
            first.Start();
            // Ждем завершения
            first.WaitForExit();           
            // Запускаем второй процесс
            second.Start();
            // Ждем завершения
            second.WaitForExit();
            end = DateTime.Now;
            Console.WriteLine("Время работы {0} с.", end.Subtract(start).TotalSeconds);
            Console.ReadKey();
        }       
    }
}
Чтобы все это запустить, нам в одной папке необходимо собрать: worker.exe, processruner.exe, 1.inp и 2.inp:
Запускаем processruner.exe, видим сразу после запуска появление еще одного окна консоли, потом, после его исчезновения второго окна консоли, ну и результат:
В папке, если все пошло нормально должны появиться два файла 1.out и 2.out.
Для запуска в параллельном режиме, создадим еще один проект, в котором код запуска процессов будет немного отличаться, от предыдущего примера. Мы после запуска первого процесса на обработку не ждем завершения его работы, а сразу запускаем второй процесс и только затем ждем окончания работы обоих процессов:

namespace ParallelProcessRuner
{
    class Program
    {
        static void Main(string[] args)
        {
            Process first, second;
            DateTime start, end;
            // Настройка процессов
            first = new Process();
            first.StartInfo.FileName = "worker.exe";
            first.StartInfo.Arguments = "Hello 1.inp 1.out";
            second = new Process();
            second.StartInfo.FileName = "worker.exe";
            second.StartInfo.Arguments = "Hello 2.inp 2.out";
            // Засекаем время
            start = DateTime.Now;
            // Запускаем первый процесс           
            first.Start();
            // Запускаем второй процесс
            second.Start();
            // Ждем завершения
            first.WaitForExit();
            second.WaitForExit();
            end = DateTime.Now;
            Console.WriteLine("Время работы {0} с.", end.Subtract(start).TotalSeconds);
            Console.ReadKey();
        }
    }
}
Собираем в одной папке worker.exe, parallelprocessruner.exe, 1.inp и 2.inp. Запускаем parallelprocessruner. Сразу после запуска появляются еще два окна консоли, которые практически одновременно закрываются. Результат работы основной программы:
Благодаря тому, что на компьютере, где запускалось это приложение, было несколько ядер, мы получили прирост производительности почти в два раза.
И хотя мы получили такой хороший рост производительности, у данного подхода есть ряд недостатков:
1. На запуск новых процессов и их управление ОС тратит достаточно много ресурсов.
2. Взаимодействие между процессами достаточно затруднено.
Из-за этих причин, в современных ОС поддерживается запуск нескольких потоков команд параллельно в рамках одного процесса. Такие потоки реализуются в языке C# классом Thread.
Работать с ним чуть проще, чем с Process. В качестве параметра конструктора класса Thread, мы должны передать экземпляр класса ThreadStart, конструктор которого, в свою очередь принимает делегат. Ну а дальше два метода нам в помощь: Start (запустить метод переданный в конструктор в отдельном потоке) и Join() (ожидание окончания работы метода).
Для демонстрации работы с потоками, мы создадим новое консольное приложение, в котором worker будет просто методом, а для упрощения его запуска мы создадим два метода без параметров, которые и будем передавать потом в ThreadStart:

static void Worker(string target, string sourcePath, string distPath)
{
    if (File.Exists(sourcePath))
    {
        StreamReader reader = new StreamReader(sourcePath);
        int position = -1;
        while (!reader.EndOfStream && reader.ReadLine() != target)
        {
            position++;
        }
        reader.Close();
        StreamWriter writer = new StreamWriter(distPath, true);
        writer.WriteLine(position);
        writer.Close();
    }
} 

static void DoFirst()
{
    Worker("Hello", "1.inp", "1.out");
} 

static void DoSecond()
{
    Worker("Hello", "2.inp", "2.out");
}

Ну и собственно метод для тестирования последовательного запуска и параллельного:

static void Main(string[] args)
{
    DateTime start, end;        

    // Запускаем последовательно
    start = DateTime.Now;
    DoFirst();
    DoSecond();
    end = DateTime.Now;
    Console.WriteLine("Последовательный запуск: {0} c.", end.Subtract(start).TotalSeconds);

    // Для параллельного запуска создаем два потока
    Thread first = new Thread(new ThreadStart(DoFirst));
    Thread second = new Thread(new ThreadStart(DoSecond));         

    // запускаем их параллельно
    start = DateTime.Now;
    first.Start();
    second.Start();
    // Ждем окончания
    first.Join();
    second.Join();
    end = DateTime.Now;
    Console.WriteLine("Последовательный запуск: {0} c.", end.Subtract(start).TotalSeconds);
    Console.ReadKey();
}

Собираем в одной папке threadruner.exe, 1.inp и 2.inp. Запускаем. Новых окон консоли не выскакивает, выходные файлы появляются, ну а время остается примерно таким же, как и для процессов:
Все, давайте на этом заканчивать наше знакомство с процессами и потоками. О процессах, мы больше говорить не будем, а вот в следующий раз поговорим о гонках и блокировках на примерах с потоками.

1 комментарий:

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

    ОтветитьУдалить