26计算限制的异步操作02-CLR
由CLR via C#(第三版) ,摘抄记录...
6 Parallel的静态For,ForEach和Invoke方法
在一些常见的编程情形中,使用任务也许会提升性能。为了简化编程,静态类System.Threading.Tasks.Paraller封装了这些常见的情形,它内部使用Task对象。例如,不要像下面一样处理一个集合中的所有项:
// 一个线程顺序执行这个工作(每次迭代调用一次DoWork) for (Int32 i = 0; i< 1000; i++ ) DoWork(i);
相反,可以使用Parallel类型的For方法,让多个线程池治线程帮助执行这个工作:
// 线程池的线程并行处理工作
Parallel.For(0,1000,i=>DoWork(i));
类似的,如果有一个集合,那么不要像下面这样写:
// 一个线程顺序执行这个工作(每次迭代调用一次DoWork) foreach ( var item in conllection) DoWork(item);
而是这样做:
// 线程池的线程并行处理工作
Parallel.ForEach(conllection,item=>DoWork(item));
如果代码中既可以用For,也可以用ForEach,那么建议使用For,因为它执行的快一点。最后,如果要执行几个方法,那么可以顺序执行它们,如下所示:
// 一个线程顺序执行所有方法 Method1(); Method2(); Method3();
也可以并行执行它们:
// 线程池的线程并行执行
parallel.Invoke(
() => Method1(),
() => Method2(),
() => Method3());
Parallel的所有方法都让调用线程参与处理。从资源利用的角度说,这是一件好事,因为我们不希望调用线程停下来,等待线程池做完所有工作后才继续。然而,如果调用线程在线程池完成自己的那一部分工作之前完成工作,调用程序就会将自己挂起,知道所有工作完成。这也是一件好事,因为这个提供了和普通for和foreach循环时相同的语义:线程要在所有工作后才继续运行。还要注意,如果任何操作抛出一个未处理的异常,你调用的paraller方法最后会抛出一个AggregateException。
// 存储用于配置 Parallel 类的方法的操作的选项。 public class ParallelOptions { // 初始化 ParallelOptions 类的新实例 public ParallelOptions(); // 获取或设置与此 ParallelOptions 实例关联的 CancellationToken,运行取消操作 public CancellationToken CancellationToken { get; set; } // 获取或设置此 ParallelOptions 实例所允许的最大并行度,默认为-1(可用CPU数) public int MaxDegreeOfParallelism { get; set; } // 获取或设置与此 ParallelOptions 实例关联的 TaskScheduler。默认为TaskScheduler.Default public TaskScheduler TaskScheduler { get; set; } }
除此之外,For和ForEach方法有一些重载版本允许传递3个委托:
private static Int64 DirectoryBytes(String path, String searchPattern, SearchOption searchOption) { var files = Directory.EnumerateFiles(path, searchPattern, searchOption); Int64 masterTotal = 0; ParallelLoopResult result = Parallel.ForEach<String, Int64>(files, () => { // localInit: 每个任务开始之前调用一次 // 每个任务开始之前,总计值都初始化为0 return 0; }, (file, parallelLoopState, index, taskLocalTotal) => { // body: 每个任务调用一次 // 获得这个文件的大小,把它添加到这个任务的累加值上 Int64 fileLength = 0; FileStream fs = null; try { fs = File.OpenRead(file); fileLength = fs.Length; } catch (IOException) { /* 忽略拒绝访问的文件 */ } finally { if (fs != null) fs.Dispose(); } return taskLocalTotal + fileLength; }, taskLocalTotal => { // localFinally: 每个任务完成后调用一次 // 将这个任务的总计值(taskLocalTotal)加到中的总计值(masterTotal)上去 Interlocked.Add(ref masterTotal, taskLocalTotal); }); return masterTotal; }
每个任务都通过taskLocalTotal变量为分配给它的文件维护自己的总计值。每个任务完成工作之后,都调用Interlocked.Add方法[对两个 32 位整数进行求和并用和替换第一个整数],以一种线程安全的方式更新总的总计值。由于每个任务都有自己的总计值,可以在一个工作项处理期间,无需进行线程同步。由于线程同步会造成性能的损失,所以不需要线程同步是一件好事。只有在每个任务返回之后,masterTotal才需要以一种线程安全的方式更新materTotal变量。所以,因为调用Interlocked.Add方法而造成的性能损失每个任务只发生一次,而不会每个工作项都发生。
// 可用来使 Parallel 循环的迭代与其他迭代交互 public class ParallelLoopState { // 获取循环的任何迭代是否已引发相应迭代未处理的异常 public bool IsExceptional { get; } // 获取循环的任何迭代是否已调用 Stop public bool IsStopped { get; } // 获取从中调用 Break 的最低循环迭代。 public long? LowestBreakIteration { get; } // 获取循环的当前迭代是否应基于此迭代或其他迭代发出的请求退出。 public bool ShouldExitCurrentIteration { get; } // 告知 Parallel 循环应在系统方便的时候尽早停止执行当前迭代之外的迭代。 public void Break(); // 告知 Parallel 循环应在系统方便的时候尽早停止执行。 public void Stop(); }
参与工作的每一个任务都会获得它自己的ParallelState对象,并可通过这个对象和参与工作的其他任务进行交互。Stop方法告诉循环停止处理任何更多的工作,未来对IsStopped属性的查询会返回true。Break方法告诉循环不再继续处理当前项之后的项。例如,假如ForEach被告知要处理100项,并在第5项时调用了Break,那么循环会确保前5项处理好之后,ForEach才返回。但注意,这并不是说在这100项中,只有前5项被处理,也许第5项之后可能在以前已经处理过了。LowestBreakIteration属性返回在处理过程中调用过Break方法的最低的项。从来没有调用过Break,LowestBreakIteration会返回null。
// 提供执行 System.Threading.Tasks.Parallel 循环的完成状态。 public struct ParallelLoopResult { // 获取该循环是否已运行完成(即该循环的所有迭代均已执行,并且该循环没有收到提前结束的请求)。 public bool IsCompleted { get; } // 获取从中调用 System.Threading.Tasks.ParallelLoopState.Break() 的最低迭代的索引。 public long? LowestBreakIteration { get; } }
可通过检查属性来了解循环的结果,如果IsCompleted返回true。表明循环运行完成,所有项都得到了处理。如果IsCompleted为false,而且LowestBreakIteration为null,表明参与工作的某个线程调用了Stop方法。如果LowestBreakIteration返回false,而且LowestBreakIteration不为null,表名参与工作的某个线程调用的Break方法,LowestBreakIteration返回的Int64值指明了保证已得到处理的最低一项的索引。
7、并行语言查询(PLINQ)
public static ParallelQuery<TSource> AsParallel<TSource>(this IEnumerable<TSource> source)
下面是将一个顺序查询转换成并行查询的例子。查询返回的是一个程序集中定义的所有过时(obsolete)方法。
private static void ObsoleteMethods(Assembly assembly) { var query = from type in assembly.GetExportedTypes().AsParallel() from method in type.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static) let obsoleteAttrType = typeof(ObsoleteAttribute) where Attribute.IsDefined(method, obsoleteAttrType) orderby type.FullName let obsoleteAttrObj = (ObsoleteAttribute) Attribute.GetCustomAttribute(method, obsoleteAttrType) select String.Format("Type={0}\nMethod={1}\nMessage={2}\n", type.FullName, method.ToString(), obsoleteAttrObj.Message); // 显示结果 foreach (var result in query) Console.WriteLine(result); }
在一个查询中,可以从执行并行操作换回执行顺序操作,这是通过调用ParallelEnumerable的AsSequential方法做到的:
public static IEnumerable <TSource> AsSequential<TSource>(this ParallelQuery<TSource> source)
static void ForAll<TSource>(this ParallelQuery<TSource> source,Action<TSource> action)
这个方法允许多个线程同时 处理结果,可以修改前面的代码来使用该方法:
//显示结果 query.ForAll(Console.WriteLine);
然而,让多个线程同时调用Console.WriteLine反而会损害性能,因为Console类内部会对线程进行同步,确保每次只有一个线程能访问控制台程序窗口,避免来自多个线程的文本最后显示成一团乱麻。希望为每个结果都执行计算时,才使用ForAll方法。
// 设置要与查询关联的 CancellationToken public static ParallelQuery<TSource> WithCancellation<TSource>(this ParallelQuery<TSource> source, CancellationToken cancellationToken); // 设置要在查询中使用的并行度。 并行度是将用于处理查询的同时执行的任务的最大数目。 public static ParallelQuery<TSource> WithDegreeOfParallelism<TSource>(this ParallelQuery<TSource> source, int degreeOfParallelism); // 设置查询的执行模式。 public static ParallelQuery<TSource> WithExecutionMode<TSource>(this ParallelQuery<TSource> source, ParallelExecutionMode executionMode); //设置此查询的合并选项,它指定查询对输出进行缓冲处理的方式。 public static ParallelQuery<TSource> WithMergeOptions<TSource>(this ParallelQuery<TSource> source, ParallelMergeOptions mergeOptions);
public enum ParallelExecutionMode { Default = 0, // 让并行LINQ决定处理查询的最佳方式 ForceParallelism = 1 // 强迫查询以其并行方式处理 }
如前所述,并行LINQ让多个线程处理数据项,结果必须再合并回去。可调用WithMergeOptions向它传递以下某个ParallelMargeOptions标志,从而控制这些结果的缓冲和合并方式:
// 指定查询中要使用的输出合并的首选类型。 换言之,它指示 PLINQ 如何将多个分区的结果合并回单个结果序列。 这仅是一个提示,系统在并行处理所有查询时可能不会考虑这一点。 public enum ParallelMergeOptions { // 使用默认合并类型,即 AutoBuffered。 Default = 0, // 不利用输出缓冲区进行合并。 一旦计算出结果元素,就向查询使用者提供这些元素。 NotBuffered = 1, // 利用系统选定大小的输出缓冲区进行合并。 在向查询使用者提供结果之前,会先将结果累计到输出缓冲区中。 AutoBuffered = 2, // 利用整个输出缓冲区进行合并。 在向查询使用者提供任何结果之前,系统会先累计所有结果。 FullyBuffered = 3, }
这些选项使你能在某种程度上控制速度和内存消耗的对应关系。NotBuffered 最省内存,但处理速度慢一些。FullyBuffered 消耗较多内存,但运行得最快。NotBuffered 介于NotBuffered 和FullyBuffered 之间,最好亲自试验所有选项,并对比其性能,来选择那种方式。
8、执行定时计算限制操作
public sealed class Timer : MarshalByRefObject, IDisposable { public Timer(TimerCallback callback, object state, int dueTime, int period); public Timer(TimerCallback callback, object state, long dueTime, long period); public Timer(TimerCallback callback, object state, TimeSpan dueTime, TimeSpan period); public Timer(TimerCallback callback, object state, uint dueTime, uint period); }
4个构造器以完全一样的方式构造Timer对象。callback参数标识希望由一个线程池线程回调的方法。当然,你写的对调方法必须和System.Threading.TimerCallback委托类型匹配,如下所示:
delegate void TimerCallback(Object state);
构造器的state参数允许在每次调用回调方法时都像它传递状态数据;如果没有需要传递的状态数据,可以传递null。dueTime参数告诉CLR在首次调用回调方法之前要等待多少毫秒。可以使用一个有符号或无符号的32位值,一个有符号的64位值或者一个TimeSpan值指定毫秒数。如果希望回调方法立即调用,为dueTime参数指定0即可。最后一个参数(period)指定了以后每次调用回调方法需要等待的时间(毫秒)。如果为这个参数传递Timeout.Infinite(-1),线程池线程值调用回调方法一次。
public sealed class Timer : MarshalByRefObject, IDisposable { public bool Change(int dueTime, int period); public bool Change(long dueTime, long period); public bool Change(TimeSpan dueTime, TimeSpan period); public bool Change(uint dueTime, uint period); }
Timer类还提供了Dispose方法,运行完全取消计时器,并可再当时处于pending状态的所有回调完成之后,向notifyObject参数标识的内核对象发送信号。以下是Dispose方法的各个重载版本:
public sealed class Timer : MarshalByRefObject, IDisposable { public void Dispose(); public bool Dispose(WaitHandle notifyObject); }
提示:一个Timer对象被垃圾回收时,它的终结代码告诉线程池取消计时器,使它不再触发。所以,使用一个Timer对象时,要确定有一个变量在保持Timer对象的存货,否则对你的回调方法调用就会停止。
internal static class TimerDemo { private static Timer s_timer; public static void Go() { Console.WriteLine("Main thread: starting a timer"); using (s_timer = new Timer(ComputeBoundOp, 5, 0, Timeout.Infinite)) { Console.WriteLine("Main thread: Doing other work here..."); Thread.Sleep(10000); } // 现在调用Dispose取消计时器 } // 一个方法的签名必须符合 TimerCallback 委托 private static void ComputeBoundOp(Object state) { // 这个方法由一个线程池线程执行 Console.WriteLine("In ComputeBoundOp: state={0}", state); Thread.Sleep(1000); // 让 Timer 在2秒钟之后再调用这个方法 s_timer.Change(2000, Timeout.Infinite); // 这个方法返回时,线程回归池中,等待下一个工作项 } }
FCL事实上提供了几个计时器,大多是开发人员都不清楚每个计时器到底有什么独到之处,在这里试着解释一下:
internal static class FalseSharing { private class Data { // 这两个字段是相邻的,并(极有可能)在相同的缓冲行中 public Int32 field1; public Int32 field2; } private const Int32 iterations = 100000000; private static Int32 s_operations = 2; private static Int64 s_startTime; public static void Go() { // 分配一个对象,并记录开始时间 Data data = new Data(); s_startTime = Stopwatch.GetTimestamp(); // 让零个线程访问在对象中它们自己的字段 ThreadPool.QueueUserWorkItem(o => AccessData(data, 0)); ThreadPool.QueueUserWorkItem(o => AccessData(data, 1)); //处于测试目的,阻塞Go线程 Console.ReadLine(); } private static void AccessData(Data data, Int32 field) { // 这里的线程各自访问它们在Data对象中自己的字段 for (Int32 x = 0; x < iterations; x++) { if (field == 0) { data.field1++; } else { data.field2++; } } // 不管哪个线程最后结束,都显示它花的时间 if (Interlocked.Decrement(ref s_operations) == 0) { Console.WriteLine("Access time: {0:N0}", Stopwatch.GetTimestamp() - s_startTime); } } }
[StructLayout(LayoutKind.Explicit)] private class Data { // 这两个字段分开了,不再相同的缓冲行中 [FieldOffset(0)] public Int32 field1; [FieldOffset(64)] public Int32 field2; }
在上述代码中,现在用一个缓存线(64字节)分隔两个字段。再次运行,结果变成了201毫秒,比第一个版本快了一些。从程序角度看,两个线程处理的是不同的数据。但从CPU缓存线来看,CPU处理的是相同的数据。这称为伪共享(false sharing)。在第二个版本中,字段在不同的缓存线上,所以CPU可以真正做到独立,不必共享什么。
~~~~~~待续。。。。
结语:我只想把我所知道的,尽量简洁清楚地表达出来。