.Net的多线程历经历代的演变,已经变得越来越易用简便了,我们可以从头回顾一下:
Thread & ThreadPool
static void LockCount() { LockCounter.count = 0; List<Thread> threadList = new List<Thread>(threadMax); var start = DateTime.Now; for (int i = 0; i < threadMax; i++) { var thread = new Thread(LockCounter.Increase);//tell thread what it need to do threadList.Add(thread); thread.Start();// an optional object parameter if LockCounter.Increase need } while (threadList.Any(p => p.IsAlive)) { }//block main thread by checking thread state Console.WriteLine((DateTime.Now - start).TotalMilliseconds + "ms"); Console.WriteLine("Lock New Thread Counter.count:" + LockCounter.count); }
通过Thread.IsAlive方法判断是否所有的子线程都执行完成。
这种多线程方式显然比同步方式要快多了,在我4核的机器上快了近4倍。但是带来的负面作用是CPU的压力会很大。原因是创建或使用线程需要如下开销:
线程创建及切换开销
线程创建之前
1.系统为线程分配并初始化一个线程内核对象;
2.系统为每个线程保留1MB的地址空间(按需提交)用于线程用户模式堆栈;
3.系统为线程分配12KB(左右)的地址空间用于线程的内核模式堆栈。
线程创建之后
4.Windows调用当前进程中的每个DLL都有的一个函数,用来通知进程中的所有DLL,操作系统创建了一个新的线程。
销毁一个线程时
5.当前进程中的所有DLL都要接收一个关于该线程即将"死亡"的通知;
6.线程的内核对象及创建时系统分配的堆栈需要释放。
调度
Windows必须决定CPU下一个次(每隔约20毫秒)调度那一个线程使其运行
上下文切换的开销
1.进入内核模式;
2.将CPU的寄存器保存到当前正在执行的线程的内核对象中。
注明:X86架构下CPU寄存器占了大约700字节(Byte)的空间,X64架构下CPU寄存器大约占了1024(Byte)的空间,IA64架构下CPU寄存器占了大约2500Byte的空间。
3.需要一个自旋锁(spin lock),确定下一次调度那一个线程,然后再释放该自旋锁。如果下一次调度的线程属于同一个进程,那么此处开销更大,因为OS必须先切换虚拟地址空间。
4.把即将要运行的线程的内核对象的地址加载到CPU寄存器中。
5.退出内核模式。
由此可见,创建和切换线程的代价可不小。一个有效的优化方案就是使用线程池:
static void LockThreadPoolCount() { LockCounter.count = 0; //WaitHandle[] handles = new WaitHandle[threadMax]; var start = DateTime.Now; ThreadPool.SetMaxThreads(4, 4); for (int i = 0; i < threadMax; i++) { //handles[i] = new AutoResetEvent(true); ThreadPool.QueueUserWorkItem(WaitCallBack); } //WaitHandle.WaitAll(handles); while (true) { int maxWorkerThreads, workerThreads, portThreads; ThreadPool.GetMaxThreads(out maxWorkerThreads, out portThreads); ThreadPool.GetAvailableThreads(out workerThreads, out portThreads); if (maxWorkerThreads == workerThreads)//通过线程池内的可用工作线程数来判断是否所有线程都已回归线程池 { break; } } Console.WriteLine((DateTime.Now - start).TotalMilliseconds + "ms"); Console.WriteLine("Lock Thread Pool Counter.count:" + LockCounter.count); } static void WaitCallBack(object paramter)//线程池只支持传入一个带object参数且空返回的方法 { LockCounter.Increase(); }
线程池相比线程有以下优缺点:
优点:重复利用已创建的CLR线程以减小开销
缺点:1.无法精确控制线程状态
2.针对长期运行的线程不合适
3.线程池内部创建启动线程有一点延时,且同时启动的速度受最小线程数影响,默认是4,单核最大250。CLR创建线程的速度是每秒不超过2个。
在本例中,首先线程池并不会真的循环1000次,而是会根据实际情况逐渐增加新线程。若线程释放的速度与任务增加速度达到某种平衡,线程池会选择使用已有的线程。正因为如此,其创建线程的大小是根据CPU核数而有上限的,且生成速度也有限制。
另一点,不像单个线程可以控制状态,线程池内线程被全权委托,无法控制。所以当我们需要判断子线程执行情况时,只能使用WaitHandler或者本例中的最大可用线程数来判断。然而WaitHandler中可处理的线程数最大只能是64个,超过将引发异常,所以本例中只能使用后一种方式相当不便。
Task/Task Factory & Parallel
选用Task和Parrallel才是真正的简洁美观。Task不但像单个Thread一样可用控制Task之间的状态,还可以有返回值。TaskFactory可以像使用ThreadPool一样自动优化线程数。对于不需要交互的行为之间还可以使用Parallel。最妙的是他们还不用手动让主线程等待,非常简洁。
static void LockParallelCount() { LockCounter.count = 0; var actions = new Action[threadMax]; var start = DateTime.Now; Parallel.Invoke(); for (int i = 0; i < threadMax; i++) { actions[i] = () => { LockCounter.Increase(); }; } Parallel.Invoke(actions); Console.WriteLine((DateTime.Now - start).TotalMilliseconds + "ms"); Console.WriteLine("Lock Parallel Counter.count:" + LockCounter.count); }
如果深入代码,可以发现Parallel针对传入的方法数组的数量是有额外优化的,且内部实现也是使用了TaskFactory。
值得注意的是Parallel中的action是互不干扰没有依赖的,即执行顺序是不固定的。
线程安全:Volatile关键字 与Memory Barrier(内存屏障)
以下两篇详细阐述了Volatile 与Memory Barrier的概念。两者都是为了解决多核下的线程安全问题的。是两个不同的解决方案。Volatile是每次强制都从内存读取而不是寄存器中。Memory Barrier则是要求读取或赋值前强行同步内存值到寄存器中。实际上Volatile也是借助了Memory Barrier来实现内存数据的同步。
http://www.tuicool.com/articles/A77BnqF
http://blog.chinaunix.net/uid-25739055-id-2973550.html