Самый эффективный способ выполнить несколько потоков - PullRequest
1 голос
/ 31 марта 2012

Вы можете пропустить эту часть:

Я создаю приложение, в котором клиент должен найти сервер в той же сети.

Сервер:

public static void StartListening (порт Int32) {

 TcpListener server = new TcpListener(IP.GetCurrentIP(), port);
 server.Start();

 Thread t = new Thread(new ThreadStart(() =>
   {
     while (true)
       {
         // wait for connection
         TcpClient client = server.AcceptTcpClient();
         if (stopListening)
           {                        
             break;
           }
       }
   }));
 t.IsBackground = true;
 t.Start();

}

Допустим, серверпрослушивает порт 12345

, затем клиент:

  • получает текущий IP-адрес клиента, скажем, 192.168.5.88
  • создать список всех возможных IP-адресов.IP-адрес сервера, вероятно, будет связан с IP-адресом клиента, если они находятся в одной локальной сети, поэтому я создаю список следующим образом:

       192.168.5.0
       192.168.5.1
       192.168.5.2
       192.168.5.3
       .....etc
       .....
       192.168.0.88
       192.168.1.88
       192.168.2.88
       192.168.3.88
       ...etc
       192.0.5.88 
       192.1.5.88
       192.2.5.88
       192.3.5.88
       192.4.5.88
       ..... etc
       0.168.5.88
       1.168.5.88
       2.168.5.88
       3.168.5.88
       4.168.5.88
       .... etc       
    

Затем я пытаюсь соединиться со всеми возможнымиip и порт 12345. Если одно соединение успешно, то это означает, что я нашел адрес сервера.


Теперь мой вопрос:

Теперь я сделал это двумя способами,Я знаю только основы о потоках, и я не знаю, опасно ли это, но это работает очень быстро.

        // first way
        foreach (var ip in ListOfIps)
        {               
            new Thread(new ThreadStart(() =>
            {
                TryConnect(ip);
            })).Start();
        }

второй способ, который я считаю, это более безопасно, но это занимает гораздо больше времени:

        // second way
        foreach (var ip in ListOfIps)
        {               
            ThreadPool.QueueUserWorkItem(new WaitCallback(TryConnect), ip);
        }

Мне нужно вызывать метод TryConnect около 1000 раз, и каждый раз это занимает около 2 секунд (я установил тайм-аут соединения на 2 секунды).Какой будет самый эффективный и безопасный способ назвать его 1000 раз?


РЕДАКТИРОВАТЬ 2

Вот результаты с использованием различных методов:

1) Использование пула потоков

        ..
        ..
        var now = DateTime.Now;
        foreach (var item in allIps)
        {
            ThreadPool.QueueUserWorkItem(new WaitCallback(DoWork), item);
        }

        ThreadPool.QueueUserWorkItem(new WaitCallback(PrintTimeDifference), now);
    }

    static void PrintTimeDifference(object startTime)
    {
        Console.WriteLine("------------------Done!----------------------");
        var s = (DateTime)startTime;
        Console.WriteLine((DateTime.Now-s).Seconds);
    }

enter image description here

Потребовалось 37 секунд для завершения

2) Использование потоков:

        ..
        ..
        var now = DateTime.Now;
        foreach (var item in allIps)
        {
            new Thread(new ThreadStart(() =>
            {
                DoWork(item);

            })).Start();

        }

        ThreadPool.QueueUserWorkItem(new WaitCallback(PrintTimeDifference), now);

enter image description here

Потребовалось 12 секунд, чтобы завершить

3) Использование задач:

        ..
        ..
        var now = DateTime.Now;
        foreach (var item in allIps)
        {

            var t = Task.Factory.StartNew(() =>

                DoWork(item)
            );                                
        }

        ThreadPool.QueueUserWorkItem(new WaitCallback(PrintTimeDifference), now);
    }

    static void PrintTimeDifference(object startTime)
    {
        Console.WriteLine("------------------Done!----------------------");
        var s = (DateTime)startTime;
        Console.WriteLine((DateTime.Now-s).Seconds);
    }

enter image description here

Это заняло 8 секунд !!

Ответы [ 4 ]

3 голосов
/ 31 марта 2012

В этом случае я бы предпочел решение с ThreadPool-Threads, потому что создание 1000 потоков - это тяжелая операция (когда вы думаете о памяти, которую получает каждый поток).Но начиная с .NET 4 есть еще одно решение с классом Task.Задачи - это рабочие нагрузки, которые могут выполняться параллельно.Вы можете определить и запустить их следующим образом:

var t = Task.Factory.StartNew(() => DoAction());

Вам не нужно заботиться о количестве используемых потоков, потому что среда выполнения обрабатывает это.Поэтому, если у вас есть возможность разделить рабочую нагрузку на более мелкие пакеты, которые могут выполняться параллельно, я бы использовал Tasks для выполнения этой работы.

2 голосов
/ 31 марта 2012

Оба метода рискуют создать слишком много потоков.

Поток стоит много времени, которое требуется для создания и потребления памяти.

Похоже, ваш второй подход, использующий ThreadPool, должен работать лучше.Из-за длительного тайм-аута (2 секунды) он все равно создаст много потоков, но гораздо меньше 1000.

Лучшим подходом (требуется Fx 4) будет использование Parallel.ForEach(...).Но это тоже может потребовать некоторой настройки.

И действительно хорошее решение - использовать широковещательный (UDP) протокол для обнаружения сервисов.

1 голос
/ 01 апреля 2012

Теперь я сделал свой собственный тест.

Вот код:

class Program {
    private static long parallelIterations = 100;
    private static long taskIterations = 100000000;

    static void Main(string[] args) {
        Console.WriteLine("Parallel Iterations: {0:n0}", parallelIterations);
        Console.WriteLine("Task Iterations: {0:n0}", taskIterations);
        Analyse("Simple Threads", ExecuteWorkWithSimpleThreads);
        Analyse("ThreadPool Threads", ExecuteWorkWithThreadPoolThreads);
        Analyse("Tasks", ExecuteWorkWithTasks);
        Analyse("Parallel For", ExecuteWorkWithParallelFor);
        Analyse("Async Delegates", ExecuteWorkWithAsyncDelegates);
    }

    private static void Analyse(string name, Action action) {
        Stopwatch watch = new Stopwatch();
        watch.Start();

        action();

        watch.Stop();
        Console.WriteLine("{0}: {1} seconds", name.PadRight(20), watch.Elapsed.TotalSeconds);
    }

    private static void ExecuteWorkWithSimpleThreads() {
        Thread[] threads = new Thread[parallelIterations];
        for (long i = 0; i < parallelIterations; i++) {
            threads[i] = new Thread(DoWork);
            threads[i].Start();
        }
        for (long i = 0; i < parallelIterations; i++) {
            threads[i].Join();
        }
    }

    private static void ExecuteWorkWithThreadPoolThreads() {
        object locker = new object();
        EventWaitHandle waitHandle = new ManualResetEvent(false);

        int finished = 0;
        for (long i = 0; i < parallelIterations; i++) {
            ThreadPool.QueueUserWorkItem((threadContext) => {
                DoWork();
                lock (locker) {
                    finished++;
                    if (finished == parallelIterations)
                        waitHandle.Set();
                }
            });
        }
        waitHandle.WaitOne();
    }

    private static void ExecuteWorkWithTasks() {
        Task[] tasks = new Task[parallelIterations];
        for (long i = 0; i < parallelIterations; i++) {
            tasks[i] = Task.Factory.StartNew(DoWork);
        }
        Task.WaitAll(tasks);
    }

    private static void ExecuteWorkWithParallelFor() {
        Parallel.For(0, parallelIterations, (n) => DoWork());
    }

    private static void ExecuteWorkWithAsyncDelegates() {
        Action[] actions = new Action[parallelIterations];
        IAsyncResult[] results = new IAsyncResult[parallelIterations];

        for (long i = 0; i < parallelIterations; i++) {
            actions[i] = DoWork;
            results[i] = actions[i].BeginInvoke((result) => { }, null);
        }
        for (long i = 0; i < parallelIterations; i++) {
            results[i].AsyncWaitHandle.WaitOne();
            results[i].AsyncWaitHandle.Close();
        }
    }

    private static void DoWork() {
        //Thread.Sleep(TimeSpan.FromMilliseconds(taskDuration));
        for (long i = 0; i < taskIterations; i++ ) { }
    }
}

Вот результат с различными настройками:

Параллельные итерации: 100.000Итерации задачи: 100Простые темы: 13,4589412 секундThreadPool Темы: 0,0682997 секундЗадачи: 0,1327014 секундыПараллельно для: 0,0066053 секундАсинхронные делегаты: 2,3844015 секунд

Параллельные итерации: 100Итерации задачи: 100.000.000Простые темы: 5,6415113 секундThreadPool Тем: 5,5798242 секундЗадачи: 56261562 секунд.Параллельно для: 5,8721274 секундAsync Delegates: 5,6041608 секунд

Как видите, простые потоки неэффективны, когда их слишком много.Но при использовании некоторых из них они очень эффективны, так как накладных расходов мало (например, синхронизация).

1 голос
/ 31 марта 2012

Ну, у этого подхода есть плюсы и минусы:

  1. Использование отдельного потока для каждого соединения (теоретически) позволит вам выполнять все соединения параллельно, поскольку это блокирующая операция ввода-вывода, все потоки будут приостановлены до тех пор, пока не будет установлено соответствующее соединение. Однако создание 1000 потоков - это немного излишне для системы.

  2. Использование пула потоков дает вам преимущество повторного использования потоков, но одновременно может быть активным только ограниченное число задач подключения. Например, если пул потоков имеет 4 потока, то будет предпринято 4 подключения, затем еще 4 и так далее. Это ограниченный ресурс, но может занять слишком много времени, потому что, как вы сказали, на одно соединение требуется около 2 секунд.

Так что я бы посоветовал компромисс: создайте пул потоков с примерно 50 потоками (используя метод SetMaxThreads) и поставьте в очередь все соединения. Таким образом, он будет легче для ресурсов, чем 1000 потоков, и при этом обрабатывать соединения достаточно быстро.

...