单核处理器单靠提高频率来提升性能变得越来越困难,多核处理器已经是大势所趋。目前绝大部分新买的电脑都已经是双核的了,今后还会有更多的核心。
    与硬件的多核形成对比的是,软件开发领域,还没有成熟的技术让我们方便的使用多核所带来的性能提升,大多数的执行过程仍然是单线程的,因为写多线程的应用很困难,除非涉及到大量的并发IO,否则一般不会使用多线程(至少我是这样,因为我..懒)。
    值得欣喜的是,微软的一批牛人也在为此努力的工作,旨在开发一个并行库,使得更加容易的编写能够自动使用多核处理器的托管代码,这就是TPL(The Task Parallel Library)。微软于12月5日发布了其CTP版本,可以从这里下面的链接下载,里面还有文档和示例。
http://www.microsoft.com/downloads/details.aspx?FamilyID=e848dc1d-5be3-4941-8705-024bc7f180ba&displaylang=en
    迫不及待的把它Down了下来,装上,然后开始试用。
    默认安装目录为C:\Program Files\Microsoft Parallel Extensions Dec07 CTP,在目录下有一个DLL文件System.Threading.dll,这就我们期待的TPL,还有两个文件夹,一个是文档,一个是示例。
    System.Threading.dll中有一个System.Threading.Parallel类,有四个用于并行计算的方法:For,For<>,ForEach<>,Do
    首先,我们设计一个计算:
        private static void Normal()
        
{
            
//获取容量为1,000,000的整数列表
            List<int> list = GetIntList();
            
double result = 0;
            
for (int i = 0; i < list.Count; i++)
            
{
                
for (int n = 0; n < 100; n++)
                
{
                    result 
+= Math.Sqrt(list[i]) * Math.Sin(list[i]);
                }

            }

            Console.WriteLine(
"result=" + result);
        }

    我们使用Parallel.For<>,来改造这个计算。For<>方法有四个重载,最复杂的一个是:
        public static void For<TLocal>(int fromInclusive, int toExclusive, int step, Func<TLocal> threadLocalSelector, Action<int, ParallelState<TLocal>> body, Action<TLocal> threadLocalCleanup);
    fromInclusive:起始值
    toExclusive:结束值
    step:步长 
    threadLocalSelector:线程初始化方法, 
    body:每一步要执行的任务,
    threadLocalCleanup:线程结束后要执行的方法。

    其他的重载可以看作是对某些参数使用默认值后对这个函数的调用,我们用的便是其默认步长为1的重载,改造后的计算:
        private static void ParallelCalculate()
        
{
            
double result = 0;
            
object syncObj = new object();
            
//获取容量为1,000,000的整数列表
            List<int> list = GetIntList();

            
//运算从0到list.Count
            
//() => 0:线程状态初值为零,这个线程状态会在执行体中作为参数传入
            
//(index, ps) =>:index是 从0到list.Count的值,ps是线程状态,在线程结束前不会重置
            
//ps.ThreadLocalState:用我们的传入第三个函数参数初始化的值,可以用来保存线程的中间计算结果
            
//threadResult:线程结束时传入的参数,和ps.ThreadLocalState为同一个值
            Parallel.For<double>(0, list.Count, () => 0, (index, ps) =>
            
{
                
for (int n = 0; n < 100; n++)
                
{
                    
//把结果保存在线程状态里
                    ps.ThreadLocalState += Math.Sqrt(list[index]) * Math.Sin(list[index]);
                }

            }
, threadResult =>
            
{
                
//线程结束时,将线程计算的结果保存到最终结果中
                lock (syncObj)
                
{
                    result 
+= threadResult;
                }

            }
);
            Console.WriteLine(
"result=" + result);
        }

来做个性能测试:(环境为:Vista™ Enterprise,Pentium D 双核 3.4G,1G 内存)
            DateTime startTime = DateTime.Now;
            Normal();
            Console.WriteLine(DateTime.Now 
- startTime);

            startTime 
= DateTime.Now;
            ParallelCalculate();
            Console.WriteLine(DateTime.Now 
- startTime);

从测试结果可以看到。性能提升还是很明显的:

    result=114917.508227442
    00:00:06.1363260
    result=114917.508226942
    00:00:03.5691075

    注意到,两次计算的结果并不完全一样,而是有一定的误差,考虑到计算两个double类型值在相加时,由于精度的限制,有可能对末位进行四舍五入,从而不同的计算顺序必然造成结果的不同,从这个角度上来说,上面的两个计算结果都正确,只是其误差已经使得他们的末尾几位数已经没有了意义。
    

    我们来做个实验,首先抛出异常
        private static void ExceptionParallelCalculate()
        
{
            
double result = 0;
            
object syncObj = new object();
            List
<int> list = GetIntList();
            
int computedCount = 0;
            
try
            
{
                Parallel.For
<double>(0, list.Count, () => 0, (index, ps) =>
                
{
                    
if (index == 100000)
                        
throw new InvalidOperationException();
                    
for (int n = 0; n < 100; n++)
                        ps.ThreadLocalState 
+= Math.Sqrt(list[index]) * Math.Sin(list[index]);
                    computedCount
++;
                }
,
                threadResult 
=>
                
{
                    
lock (syncObj)
                    
{
                        result 
+= threadResult;
                    }

                }
);
            }

          
catch (Exception) { };
            Console.WriteLine(
"result=" + result);
            Console.WriteLine(
"computedCount={0}---{1}%", computedCount, computedCount / 10000d);
        }

    运行结果为:

    result=122427.621705062
    computedCount=999992---99.9992%

    
    虽然在运算进行到10%左右的时候就抛出了一个异常,但是最终结果是它几乎完成了所有的运算。因为抛出异常的那个线程(就是计算数字100000的那个线程)被分配到的任务只占总任务的很少一部分(每次运行结果可能会不同),抛出异常时,它立刻终止当前任务,执行threadLocalCleanup,并抛弃任务队列里的剩余任务(被抛弃的应该是7个数字的计算任务,加上抛出异常的一个,一共8个)。其他的未分配任务会被其他的线程执行完毕。
    上例中如果把 throw new InvalidOperationException() 换成 ps.Stop() 那么所有线程都会立即终止,运行结果为:
    
    result=12594.4311232184
    computedCount=100000---10%

    
    这次我们尝试阻塞其中的一部分线程:
Code

    运算结果为:
    result=114917.508241467
    ThreadCount=15
    computedCount=1000000---100%
    
    由于某些线程被阻塞,生成了新的线程。

    如果我们在其中抛出一个异常:
private static void ExceptionJoinParallelCalculate()
        
{
            
double result = 0;
            
object syncObj = new object();
            
object syncObj2 = new object();
            
object syncObj3 = new object();
            List
<int> list = GetIntList();
            List
<int> threadIDList = new List<int>();
            
int computedCount = 0;
            
try
            
{
                Parallel.For
<double>(0, list.Count, () => 0, (index, ps) =>
                
{
                    
if (index == 50000)
                        
throw new InvalidOperationException();
                    
if (index > 100000 && index < 100100)
                    
{
                        Thread.CurrentThread.Join(
500);
                    }

                    
lock (syncObj)
                    
{
                        
if (!threadIDList.Contains(Thread.CurrentThread.ManagedThreadId))
                            threadIDList.Add(Thread.CurrentThread.ManagedThreadId);
                    }

                    
for (int n = 0; n < 100; n++)
                        ps.ThreadLocalState 
+= Math.Sqrt(list[index]) * Math.Sin(list[index]);
                    
lock (syncObj2)
                    
{
                        computedCount
++;
                    }

                }
,
                threadResult 
=>
                
{
                    
lock (syncObj3)
                    
{
                        result 
+= threadResult;
                    }

                }
);
            }

            
catch (Exception) { };
            Console.WriteLine(
"result=" + result);
            Console.WriteLine(
"ThreadCount=" + threadIDList.Count);
            Console.WriteLine(
"computedCount={0}---{1}%", computedCount, computedCount / 10000d);
        }

    运行结果:
    result=76017.275196288
    ThreadCount=2
    computedCount=999992---99.9992%

    显然,这次因为之前曾有线程抛出异常,即使线程被阻塞了,也没有生成新的线程。

    来个小结:
    1。这个方法并不会对同步执行的线程,也不保证每个任务是按顺序执行的。
    2。线程任务的分配并不是一次完成的,而是一次只分配很少的任务,任务完成后会再分配其他任务,所以每个线程都有一个任务序列,而且都很短。
    3。线程数也是动态分配的,默认情况下,会生成与CPU核心相同数量的线程,但数如果某个线程被阻塞,就会产生新的线程,最终尽量保证正在执行的线程与CPU核心数量相同,每个核心都被充分利用。
    4。如果某个线程抛出了异常,在这之后无论在何种情况下,都不会再生成新的线程,但不会终止已生成的线程。
    5。抛出异常的线程仍然会执行threadLocalCleanup,但是会抛弃其任务队列里未被执行的任务序列。
    5。它会保存任一个线程抛出的异常,并在所有的线程都终止的时候重新抛出,一个线程的异常并不会影响其他线程的执行。
    6。如果要,终止所有的线程,不要使用异常,使用ParallelState的Stop()方法。

posted on 2007-12-20 13:09  yujiasw  阅读(1012)  评论(0编辑  收藏  举报