четверг, 22 марта 2012 г.

Task-и в C#

В C# 4.5 появились два ключевых слова async и await. Но о них, как нибудь в другой раз. Но вот чтобы они заработали в них применяются Task-и, которые доступны были и раньше. Сегодня я хочу показать простенький пример именно на класс Task.
Будем решать задачу поиска простых чисел до 10000000.Первое решение будет в лоб, но методы из него, мы поиспользуем и в многопоточной программе.
Мне лень возиться с синхронизацией, динамическими объектами и прочим, да и понятнее будет на статике. Поиск будет осуществлять перебором делителей. Для хранения делителей воспользуемся вот таким массивом:
static List<int> SimpleNumber = null;

Проверку числа на простоту сделаем вот так:
static void TestNumber(int p_number)
{
 int sqrt = (int)Math.Sqrt(p_number) + 1;
 int i = 0;
 while (SimpleNumber[i] < sqrt)
 {
  if (p_number % SimpleNumber[i] == 0)
  {
   return;
  }
  i++;
 }
 lock (SimpleNumber)
 {
  SimpleNumber.Add(p_number);
 }
  }
 }
}

Ну и собственно перебор:
private static void SimpleCalculator(int p_begin, int p_end, int p_steep)
{
 for (int i = p_begin; i < p_end; i += p_steep)
 {
  TestNumber(i);
 }
}

Метод Main будет иметь вот такой вид:

static void Main(string[] args)
{
 SimpleNumber = new List<int>(new int[] { 3 });
 int N = 10000000;
 DateTime begin, end;
 begin = DateTime.Now;
 SimpleCalculator(5, N, 2);
 end = DateTime.Now;
 Console.WriteLine("Один поток {0} c. Простых чисел: {1}", end.Subtract(begin).TotalSeconds, SimpleNumber.Count);
 Console.ReadKey();
}


Запускаем. Оказывается, что время работы (ну по крайней мере на моем ноуте) составляет 3,8 с.
Ок, пытаемся превратить нашу программу в многопоточную. На самом деле, нам понадобиться переписать только метод Main. Именно для реализации многопоточности мы и воспользуемся классом Task. Он в качестве параметра принимает Action и у него есть два замечательных метода Start и Wait. Первый запускает задачу на выполнение, а второй ожидает пока она закончится. Если метод Wait вызван после того, как задача завершилась, то управление сразу передается команде следующей за вызовом Wait. Собственно код:

static void Main(string[] args)
{
 SimpleNumber = new List<int>(new int[] { 3 });
 int N = 10000000;
 DateTime begin, end;
 begin = DateTime.Now;
 SimpleCalculator(5, 10000, 2);
 Task task1 = new Task(() => SimpleCalculator(10001, N, 8));
 task1.Start();
 Task task2 = new Task(() => SimpleCalculator(10003, N, 8));
 task2.Start();
 Task task3 = new Task(() => SimpleCalculator(10005, N, 8));
 task3.Start();
 Task task4 = new Task(() => SimpleCalculator(10007, N, 8));
 task4.Start();
 task1.Wait();
 task2.Wait();
 task3.Wait();
 task4.Wait();
 end = DateTime.Now;
 Console.WriteLine("4 потока {0} c. Простых чисел: {1}", end.Subtract(begin).TotalSeconds, SimpleNumber.Count);
 Console.ReadKey();
}

Запускаем. У меня время 1,3 с. Совсем неплохо. Выигрыш в 3 раза. Единственно, меня напрягают повторяющиеся фрагменты кода. Исправляем. Для этого воспользуемся фабрикой задач:

static void Main(string[] args)
{
 SimpleNumber = new List<int>(new int[] { 3 });
 int N = 10000000;
 DateTime begin, end;
 begin = DateTime.Now;
 SimpleCalculator(5, 10000, 2);
 Task[] tasks = new Task[4];
 for (int i = 0; i < 4; i++)
 {
  tasks[i] = Task.Factory.StartNew(() => SimpleCalculator(10001 + i * 2, N, 8));
 }
 Task.WaitAll(tasks);
 end = DateTime.Now;
 Console.WriteLine("4 потока {0} c. Простых чисел: {1}", end.Subtract(begin).TotalSeconds, SimpleNumber.Count);
 Console.ReadKey();
}

Время тоже, но смотрится чуть более здорово.

3 комментария:

  1. У меня есть предположение ,что алгоритм работает неправильно:
    в списке SimpleNumber, я так понимаю должны жить простые числа упорядоченные по возрастанию при этом поток SimpleCalculator(5, 10000, 2); заполняет его простыми числами до 10000 потом для каждого потока из этого списка ищутся делители из простых чисел , квадрат которых , меньше проверяемого на простоту, если делитель не найден то число добавляется в список простых чисел, короче если случайно остановится первый поток, то он не найдёт например простое число Na0, которое являлось делителем для второго потока, всё же потоки функционально зависимы от списка простых чисел (хотя если потоки 1 2 3 4 работают более менее равномерно , то этого не произойдёт )

    ОтветитьУдалить
    Ответы
    1. Именно для того, чтобы нужные числа уже были в массиве, анализ и начинается с такого большого числа.
      Но и это все лирика, просто нужен был алгоритм, который минимально бы отвлекал от параллельности, ну вот я такой и придумал...

      Удалить
  2. и вообще простые числа после 10000 в списке SimpleNumber не будут упорядочены по возрастанию

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