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可以真正做到独立,不必共享什么。
~~~~~~待续。。。。
结语:我只想把我所知道的,尽量简洁清楚地表达出来。


浙公网安备 33010602011771号