单核处理器单靠提高频率来提升性能变得越来越困难,多核处理器已经是大势所趋。目前绝大部分新买的电脑都已经是双核的了,今后还会有更多的核心。
与硬件的多核形成对比的是,软件开发领域,还没有成熟的技术让我们方便的使用多核所带来的性能提升,大多数的执行过程仍然是单线程的,因为写多线程的应用很困难,除非涉及到大量的并发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
首先,我们设计一个计算:
我们使用Parallel.For<>,来改造这个计算。For<>方法有四个重载,最复杂的一个是:
toExclusive:结束值
step:步长
threadLocalSelector:线程初始化方法,
body:每一步要执行的任务,
threadLocalCleanup:线程结束后要执行的方法。
其他的重载可以看作是对某些参数使用默认值后对这个函数的调用,我们用的便是其默认步长为1的重载,改造后的计算:
来做个性能测试:(环境为:Vista™ Enterprise,Pentium D 双核 3.4G,1G 内存)
从测试结果可以看到。性能提升还是很明显的:
result=114917.508227442
00:00:06.1363260
result=114917.508226942
00:00:03.5691075
注意到,两次计算的结果并不完全一样,而是有一定的误差,考虑到计算两个double类型值在相加时,由于精度的限制,有可能对末位进行四舍五入,从而不同的计算顺序必然造成结果的不同,从这个角度上来说,上面的两个计算结果都正确,只是其误差已经使得他们的末尾几位数已经没有了意义。
我们来做个实验,首先抛出异常
运行结果为:
result=122427.621705062
computedCount=999992---99.9992%
虽然在运算进行到10%左右的时候就抛出了一个异常,但是最终结果是它几乎完成了所有的运算。因为抛出异常的那个线程(就是计算数字100000的那个线程)被分配到的任务只占总任务的很少一部分(每次运行结果可能会不同),抛出异常时,它立刻终止当前任务,执行threadLocalCleanup,并抛弃任务队列里的剩余任务(被抛弃的应该是7个数字的计算任务,加上抛出异常的一个,一共8个)。其他的未分配任务会被其他的线程执行完毕。
上例中如果把 throw new InvalidOperationException() 换成 ps.Stop() 那么所有线程都会立即终止,运行结果为:
result=12594.4311232184
computedCount=100000---10%
这次我们尝试阻塞其中的一部分线程:
运算结果为:
result=114917.508241467
ThreadCount=15
computedCount=1000000---100%
由于某些线程被阻塞,生成了新的线程。
如果我们在其中抛出一个异常:
运行结果:
result=76017.275196288
ThreadCount=2
computedCount=999992---99.9992%
显然,这次因为之前曾有线程抛出异常,即使线程被阻塞了,也没有生成新的线程。
来个小结:
1。这个方法并不会对同步执行的线程,也不保证每个任务是按顺序执行的。
2。线程任务的分配并不是一次完成的,而是一次只分配很少的任务,任务完成后会再分配其他任务,所以每个线程都有一个任务序列,而且都很短。
3。线程数也是动态分配的,默认情况下,会生成与CPU核心相同数量的线程,但数如果某个线程被阻塞,就会产生新的线程,最终尽量保证正在执行的线程与CPU核心数量相同,每个核心都被充分利用。
4。如果某个线程抛出了异常,在这之后无论在何种情况下,都不会再生成新的线程,但不会终止已生成的线程。
5。抛出异常的线程仍然会执行threadLocalCleanup,但是会抛弃其任务队列里未被执行的任务序列。
5。它会保存任一个线程抛出的异常,并在所有的线程都终止的时候重新抛出,一个线程的异常并不会影响其他线程的执行。
6。如果要,终止所有的线程,不要使用异常,使用ParallelState的Stop()方法。
与硬件的多核形成对比的是,软件开发领域,还没有成熟的技术让我们方便的使用多核所带来的性能提升,大多数的执行过程仍然是单线程的,因为写多线程的应用很困难,除非涉及到大量的并发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);
}
{
//获取容量为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);
}
{
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);
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);
}
{
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);
}
{
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()方法。