执行多个线程的最有效方法

时间:2012-03-31 07:10:36

标签: c# multithreading performance threadpool

您可以跳过此部分:

我正在创建一个应用程序,客户端需要在同一网络上找到服务器。

服务器:

public static void StartListening(Int32 port)  {

 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)使用threadpool

        ..
        ..
        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 个答案:

答案 0 :(得分:3)

在这种情况下,我更喜欢使用ThreadPool-Threads的解决方案,因为创建1000个线程是一个繁重的操作(当你想到每个线程得到的内存时)。 但是从.NET 4开始,还有另一个类Task的解决方案。 任务是可以并行执行的工作负载。您可以像这样定义和运行它们:

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

您不必关心使用的线程数,因为运行时环境会处理这些线程。因此,如果您有可能将工作负载拆分为可以并行执行的较小包,我将使用Tasks来完成工作。

答案 1 :(得分:2)

这两种方法都存在创建太多线程的风险。

线程在创建和内存消耗方面的代价很高。

看起来你的第二种方法,使用ThreadPool,应该更好。由于长时间超时(2秒),它仍会创建许多线程,但远低于1000.

更好的方法(需要Fx 4)是使用Parallel.ForEach(...)。但这也可能需要一些调整。

一个非常好的解决方案是使用广播(UDP)协议来发现服务。

答案 2 :(得分:1)

这种方法有利有弊:

  1. 每个连接使用一个单独的线程(理论上)允许您并行进行所有连接,因为这是阻塞I / O操作,所有线程将被挂起,直到相应的连接成功。但是,创建1000个线程对系统来说有点过分。

  2. 使用线程池为您提供了重用线程的好处,但一次只能激活有限数量的连接任务。例如,如果线程池有4个线程,那么将尝试4个连接,然后是另外4个连接,依此类推。资源很少但可能需要很长时间,因为正如您所说,单个连接需要大约2秒钟。

  3. 所以我建议权衡:创建一个包含大约50个线程的线程池(使用SetMaxThreads方法)并排队所有连接。这样,它将比1000个线程更轻的资源,并且仍然可以合理地快速处理连接。

答案 3 :(得分:1)

现在我做了自己的基准测试。

以下是代码:

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秒
任务:5,6261562秒
平行于:5,8721274秒
异步代表:5,6041608秒

正如你所看到的那样,简单的线程在它们太多时效率不高。 但是当使用它们中的一些时,它们非常有效,因为几乎没有开销(例如同步)。