由CLR via C#(第三版) ,摘抄记录...

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

    当然,这并不是说需要检查自己的所有源代码,将for循环替换成Parallel.For的调用。调用Parallel的方法时,有一个前提条件务必记住:工作项要能并行执行。因此,如果工作项必须顺序执行,就不要调用Parallel的方法。另外,要避免会修改任何共享数据的工作项,因为多个线程同时处理的数据可能损坏。为了解决这个问题,一般的方法就是围绕数据访问添加线程同步锁。但是这样一来,一次就只能有一个线程访问数据,无法享受并行处理多个想带来的好处。
    除此之外,Parallel的方法本身也有开销:委托对象必须分配,而针对每一个工作项,都要调用一次这些委托。如果有大量可由多个线程处理的工作项,那么也许会获得性能的提升。但是,如果只为区区几个工作项使用Parallel的方法,或者为处理得非常快的工作项使用Parallel就会得不偿失了。
    Parallel的For,ForEach和Invoke方法都能接受一个ParallelOptions对象的重载版本。这个对象的定义如下:

 // 存储用于配置 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个委托:

    任务局部初始化委托(localInit),为参与工作的每一个任务都调用一次委托。这个委托是在任务被要求处理一个工作项之前调用。
    主体委托(body),为参与工作的各个线程所处理的每一项都调用一次委托。
    任务局部终结委托(localFinally),为参与工作的每一个任务都调用一次委托。这个委托是在任务处理好派遣给它的所有工作之后调用。即使主体委托引发一个未处理的异常,也会调用它。
    以下示例代码演示了如何利用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方法而造成的性能损失每个任务只发生一次,而不会每个工作项都发生。

    注意,我们向主题委托传递一个ParallelLoopState对象,它的定义如下:

// 可用来使 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。

    处理任何一项时,如果造成一个未处理的异常,IsExceptional属性会返回true。如果处理一项时会花费大量的时间,代码可查询ShouldExitCurrentIteration属性看它是否应该提前退出。如果调用过Stop,调用过Break,取消过CancellationTokenSource,或者处理一项时造成了未处理的异常,这个属性就会返回true。
    Parallel的For和ForEach方法都返回一个ParallelLoopResult实例,他看起来像下面这样:

   
// 提供执行 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)

   Microsoft的语言集成查询(LINQ)功能提供了一个简捷的语法来查询数据集合。使用LINQ,可轻松对数据线进行筛选、排序、投射等。使用LINQ to Object时,只有一个线程顺序处理数据集合中的所有项;我们称为顺序查询。为了提高处理性能,可以使用并行LINQ,它将顺序查询转换成为一个并行查询,在内部使用任务(这些任务的排列由默认TaskScheduler来调度),将集合中的数据线的处理工作分散到多个CPU上,一边并发处理多个数据。和Prarllel的方法相似,如果同时要处理大量项,或者每一项的处理过程都是一个耗时的计算限制的操作,那么就可以使用PLINQ获得最大的收益。

    静态System.Linq.ParallelEnumerable类(在System.Core.dll中定义)实现了PLINQ的所有功能,所以必须通过C#的using指令将System.Linq命名空间导入到你的源码中。尤其是,这个类公开了所有标准LINQ操作符的并行版本,比如Where,Select,SelectMany,GroupBy,Join,Skip,Task等。所有这些方法都是扩展了System.Linq.ParallelQuery<T>类型的扩展方法。为了让自己的LINQ to Object查询调用这些方法的并行版本,必须将自己的顺序查询(基于IEnumberable或者IEnumerable<T>)转换成并行查询(基于ParallelQuery或者ParallelQuery<T>),这是用ParallelEnumerable的AsParallel扩展方法来实现的,如下所示:
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)
    这个方法将一个ParallQuery<T>转换回一个IEnumerable<T>。这样一来,在调用了AsDequential之后执行的操作只由一个线程执行。
    通常,一个LINQ查询的结果数是让某个线程执行一个foreach语句计算获得的。这意味着只有一个线程遍历查询的所有结果。如果希望以并行的方式处理查询的结果,就应该使用ParallelEnumerable的ForAll方法处理查询:
static void ForAll<TSource>(this ParallelQuery<TSource> source,Action<TSource> action)

这个方法允许多个线程同时 处理结果,可以修改前面的代码来使用该方法:

//显示结果
query.ForAll(Console.WriteLine);

然而,让多个线程同时调用Console.WriteLine反而会损害性能,因为Console类内部会对线程进行同步,确保每次只有一个线程能访问控制台程序窗口,避免来自多个线程的文本最后显示成一团乱麻。希望为每个结果都执行计算时,才使用ForAll方法。

    由于PLINQ可用多个线程处理数据项,所以数据项被并发处理,结果被无序返回。如果需要让PLINQ保存数据项的顺序,可调用ParallelEnumerable的AsOrderd方法。调用这个方法时,线程会成组处理数据项。然后,这些数据项被合并回去,以保持顺序。这样会损害到性能。一下操作符生成不排序的操作:Distinct,Except,Intersect,Union,Join,GroupBy,GroupJoin和ToLookup。在这些操作符之后,如果想再次强制排序,只需调用AsOrdered方法。
    以下操作符生成排序的操作:ORderBy,OrderByDescending,Thenby和ThenByDescending。在这些出操作符之后,如果想再次恢复不排序的处理,只需调用AsUnordered方法。
    PLINQ提供了一些额外的ParallelEnumerable方法,可调用它们来控制查询的处理方式:
// 设置要与查询关联的 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);
     显然,WithCancellation方法允许传递一个CancellationToken,使查询处理能提前停止。WithDegreeOfParallelism方法指定最多允许多少个线程处理查询;他不会强迫创建满全部线程,如果并不是全部都需要的话。你一般不会调用这个方法。另外,默认情况下,会为每个内核用一个线程来执行查询。
    PLINQ分析一个查询,然后决定如何最好地处理它。有的时候,顺序处理一个查询可以获得更好的性能,尤其在使用以下任何操作时:Concat,ElementAt(OrDefault),First(OrDefault),Last(OrDefault),Skip(While),Task(While)或Zip。使用Select(Many)或Where的重载版本,并向你的selector或predicate委托传递一个位置索引时也是如此。然而,可以调用WithExecutionMode,向它传递某个ParallelExecuteMode标志,从而强迫查询以并行方式处理:
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、执行定时计算限制操作

System.Threading命名空间定义了一个Timer类,可用它让一个线程池线程定时调用一个方法。构造Timer类的一个实例相当于告诉线程池:在将来的某个时间会执行你的一个方法。Timer类提供了几个构造函数,相互都非常相似:

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),线程池线程值调用回调方法一次。

    在内部,线程池为所有Timer对象只使用了一个线程。这个线程知道下一个Timer对象在什么时候到期。下一个Timer对象到期时,线程就会唤醒,在内部调用TreadPool的QueueUserWorkItem,将一个工作项添加到线程池队列中,使你的回调方法得到调用。如果回调方法的执行时间很长,计时器可能(在上个回调还没有完成时)再次触发。这个能造成多个线程同时执行你的对调方法。为解决这个问题,我的建议是:构造Timer时,为period参数指定Timeout.Infinite。这样,计时器就只触发一次。然后,在你的对调方法中,调用Change方法指定一个新的dueTime,并再次为period参数指定Timeout.Infinite。以下是Change方法的各个重载版本:
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对象的存货,否则对你的回调方法调用就会停止。

    以下代码演示了如何让一个线程池线程立即开始调用一个回调方法,以后每2秒钟调用一次:
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事实上提供了几个计时器,大多是开发人员都不清楚每个计时器到底有什么独到之处,在这里试着解释一下:

    System.Threading的Timer类    这是刚刚讨论过的计时器。要在一个线程池线程上执行定时的(周期性发生的)后台任务,它是最好的计时器。
    System.Windows.Forms的Timer类    构造这个类的一个实例,相当于告诉Windows将一个计时器和调用线程关联。当这个计时器触发时,WIndows将一条计时器消息(WM_TIMER)注入线程的消息队列。线程必须执行一个消息泵来提取这些消息,并把它们派遣给想要的回调方法。注意,所有这些工作都只有一个线程完成——设置计时器的线程保证就是执行回调方法的线程。这还意味着你的计时器方法不会由多个线程并发执行。
    System.Windows.Threading的DispatcherTimer类    这个类是 System.Windows.Forms的Timer类在Siverlight和WPF应用程序中的等价物。
    System.Timers的Timer类    这个计时器基本是System.Threading的Timer类的一个包装类。当计时器到触发时,会导致CLR将事件放到线程池的队列中。尽量不要使用这个类而是使用System.Threading的Timer类。
 
  9、线程池如何管理线程
  
     9.1、设置线程池限制
    CLR允许开发人员设置线程池要创建最大线程数。但实践证明,线程池永远都不该为池中的线程数设置上限,因为可能发生饥饿或死锁。假如队列中有1000个工作项,但这些工作项全都因为一个事件而阻塞,等1001个工作项发出信号才能解除阻塞。如果设置最大1000个线程,第1001个工作项就会执行,所有1000个线程都会一直阻塞,最终用户被迫终止应用程序,并丢失他们都做的为保存的工作。
    由于存在饥饿和死锁问题,CLR团队一直都在稳步地增加线程默认能够拥有的最大线程数。目前默认值是最大1000个线程。这基本可以看成是不限数量的,因为一个32位进程最大有2G的可用选址空间。加载一组Win32和CLR DLLs,并分配了本地堆和托管堆之后,剩余约1.5G的地址控制。由于每个线程都要为其用户模式栈和线程环境块(TEB)准备超过1MB的内存,所以一个32位进程中,最多能够有1360个线程。试图创建更多的线程,会抛出一个OutOfMemotyExcption。当然,64位就另当别论了。
    System.Threading.ThreadPool类提供了几个静态方法,可调用它们设置和查询线程池的线程数:GetMaxThreads,SetMaxThreads,GetMinThreads,SetMinThreads和GetAvailableThreads[获得可用的线程数量]。强烈建议你不要调用上述任何方法。限制线程池的线程数,一般只会造成应用程序性能变得更差。
 
    2、如何管理工作者线程
    图 26-1 展示了构成线程池的一部分的工作者线程的各种数据结构。ThreadPool.QueueUserWorkItem方法和Timer类总是将工作项放到全局队列中。工作者线程采用一个先入先出算法将工作项从这个队列中取出来,并处理它们。由于多个工作者线程可能同时从全局队列中拿走工作项,所以所有工作者线程都竞争一个线程同步锁,以保证两个或多个线程不会获取同一个工作项。这个线程同步锁在某些应用程序中可能成为瓶颈,对伸缩性和性能造成某种程序的限制。
 
    现在,让我们谈谈使用默认TaskScheduler(通过查询TaskScheduler的静态Default属性来获得)来调度的Task对象。当一个非工作者线程调度一个Task时,Task会添加到全局队列中。但是,每个工作者线程都有它自己的本地队列。当一个工作者线程调度一个Task时,Task会添加到调用线程的本地队列中。
    一个工作者线程准备好处理一个工作项时,它总是先检查它的本地队列来查找一个Task。入股存在一个Task,工作者线程就从它的本地队列中移出task,并对工作项进行处理。要注意的是,工作者线程采用后入先出的算法将任务从它的本地队列中取出。由于工作者线程是唯一允许访问它自己的本地队列列头的线程,所以无需同步锁,而且在队列中添加和删除Task的速度非常快。这个行为的副作用在于,Task是按照和进入队列时相当的顺序执行的。
    提示:线程池从来不保证排队中的工作项的处理顺序,这是合理的,尤其考虑到多线程可能同事处理工作项。然而,上述副作用使得这个问题变得更加恶化。你必须保证自己的应用程序对工作项或Task的执行顺序不做任何预设。
    如果一个工作者线程发现它的本地队列变空了,工作者线程就会尝试从另一个工作者线程的本例队列中"偷"一个Task。这个Task是从一个本地队列的尾部"偷"走的,并要求获得一个线程同步锁,这对性能可能有少许影响。当然,这种"偷窃"行为很少发生,所以很少需要获取这个锁。如果所有本地队列都变空,那么工作者线程会使用FIFO算法,从全局队列中提取一个工作项。如果全局队列也为空,那么线程就会进入睡眠状态,等待事情的发生。如果睡眠的时间太长,它会自己醒来,并销毁自己,允许系统回收线程使用的资源(包括内核对象、栈、TEB等)。
    线程池会快速常见工作者线程,是工作者线程的数量等于传给ThreadPool的SetMinThreads方法的值。如果从不调用这个方法(也不建议你调用),那么默认值等于你的进程允许使用的CPU数,这是由线程的affinity mask(关联掩码)决定的。通常,你的进程允许使用机器上的所有CPU数,所以线程池创建的工作者线程数量很快就会打到机器上的CPU数。创建了这么多的的线程后,线程池会监视工作项的完成速度。如果工作项完成的时间太长,线程池会创建更多的工作者线程。如果工作项的完成速度开始变快,工作者线程会被销毁。
 
  十、缓存线和伪共享
 
    为了提升反复访问内存的性能,如今的CPU在芯片上都集成了高速缓存。线程首次从RAM去取一些值时,CPU从RAM获取所需的值,并把它存储到CPU的高速缓存中。事实上,为了进一步提升性能,CPU会在逻辑上将所有内存都划分为所谓的缓冲行(cache line)。一个缓冲行有64个字节构成,所以CPU从RAM中获取并存储64字节的块。如果应用程序需要读取一个Int32值,那么会获取包含了那个Intt32的64个字节,这样会获取到比需要的更多字节,这样通常会造成性能增强,因为大多数应用程序在访问了一些数据之后,通常会继续访问存储在那些数据周围的数据。由于相邻的数据已经提取到CPU的缓存中,就避免了慢速的RAM访问。

    然而,如果两个或多个内核访问同一个缓冲行中的字节,内核必须相互通信,并在内核之间传递缓冲行,造成多个内核不能同时处理相邻的字节,这对性能会造成严重影响。
    下面用一些代码演示这一点:
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);
} }
}
    上述代码中,Data对象在构造时包含了两个字段。这两个字段极有可能在同一个缓冲行中。然后,两个线程池线程启动并执行AccessData方法。一个将1加到Data的filed1上的100 000 000 次,另一个线程对filed2字段做同样的事情。每个线程完成后,都递减s_operations字段中的值;最后一个将字段递减为0的线程就是最后一个结束的线程,它显示两个线程完成它们的工作总共发了多少时间。我的机器大约花了245毫秒。
    现在,让我们修改Data类,使它看起来项下面这样:

 
[StructLayout(LayoutKind.Explicit)]
private class Data {
// 这两个字段分开了,不再相同的缓冲行中
[FieldOffset(0)]
public Int32 field1;
[FieldOffset(64)]
public Int32 field2;
}

在上述代码中,现在用一个缓存线(64字节)分隔两个字段。再次运行,结果变成了201毫秒,比第一个版本快了一些。从程序角度看,两个线程处理的是不同的数据。但从CPU缓存线来看,CPU处理的是相同的数据。这称为伪共享(false sharing)。在第二个版本中,字段在不同的缓存线上,所以CPU可以真正做到独立,不必共享什么。

    通过上述讨论,应该知道在多个线程同时访问相邻的数据时,缓存线和伪共享可能对应用程序产生严重影响。在性能非常紧要的情形下,这是你应该注意的一点。如果检查到这个问题,通常都可以设计出一种方式来避免它(这里用的就是FiledOffset attribute)。
    要注意的是,数组在数组内存起始处维护着它的长度,具体位置是在前几个数据元素之后,访问一个数组元素时,CLR验证你使用的索引在数组的长度之内。这意味着访问一个数组总是牵涉到访问数组的长度。因此,为了避免产生额外的伪共享,应该避免让一个线程向数组的前几个元素写入,同时让其他线程访问数组中的其他元素。

~~~~~~待续。。。。

 

26计算限制的异步操作02-CLR的更多相关文章

  1. 【C#进阶系列】26 计算限制的异步操作

    什么是计算限制的异步操作,当线程在要使用CPU进行计算的时候,那么就叫计算限制. 而对应的IO限制就是线程交给IO设备(键鼠,网络,文件等). 第25章线程基础讲了用专用的线程进行计算限制的操作,但是 ...

  2. [CLR via C#]26. 计算限制的异步操作

    一.CLR线程池基础 前面说过,创建和销毁线程是一个比较昂贵的操作,太多的线程也会浪费内存资源.由于操作系统必须调度可运行的线程并执行上下文切换,所以太多的线程还有损于性能.为了改善这个情况,CLR使 ...

  3. CLR Via CSharp读书笔记(26) - 计算限制的异步操作

    执行上下文: 执行上下文包括安全设置(压缩栈.Thread的Principal属性和Windows身份),宿主设置(System.Threading.HostExecutionContextManag ...

  4. 26计算限制的异步操作01-CLR

    由CLR via C#(第三版) ,摘抄记录... 异步优点:在GUI应用程序中保持UI可响应性,以及多个CPU缩短一个耗时计算所需的时间. 1.CLR线程池基础:为提高性能,CLR包含了代码来管理他 ...

  5. 《CLR via C#》读书笔记 之 计算限制的异步操作

    <CLR via C#>读书笔记 之 计算限制的异步操作 2014-07-06 26.1 CLR线程池基础 返回 如25章所述,创建和销毁线程是一个比较昂贵的操作: 太多的线程也会浪费内存 ...

  6. 《CLR Via C#》读书笔记:27.计算限制的异步操作

    一.CLR 线程池基础 一般来说如果计算机的 CPU 利用率没有 100% ,那么说明很多进程的部分线程没有运行.可能在等待 文件/网络/数据库等设备读取或者写入数据,又可能是等待按键.鼠标移动等事件 ...

  7. CLR via C# 读书笔记-27.计算限制的异步操作(上篇)

    前言 学习这件事情是一个习惯,不能停...另外这篇已经看过两个月过去,但觉得有些事情不总结跟没做没啥区别,遂记下此文 1.CLR线程池基础 2.ThreadPool的简单使用练习 3.执行上下文 4. ...

  8. CLR via C# 计算限制的异步操作读书笔记

    1. 使用线程池搪行简单的计算限制操作 ThreadPool.QueueUserWorkItem(WaitCallback callback) 2.CLR默认情况下自动使初始线程的执行上下文流向辅助线 ...

  9. Clr Via C#读书笔记---计算限制的异步操作

    线程池基础 1,线程的创建和销毁是一个昂贵的操作,线程调度以及上下文切换耗费时间和内存资源. 2,线程池是一个线程集合,供应你的用程序使用. 3,每个CLR有一个自己的线程池,线程池由CLR控制的所有 ...

随机推荐

  1. HDU2491 Priest John&#39;s Busiest Day

    题目链接 题意: 有n个人要进行乒乓球比赛,每一个人都一个能力值.每一个人出现的次序就是他们住的位置 如今要求进行一场比赛,三个人,裁判的能力值在两个选手之间,住的位置也在两个人的之间 问这样的比赛一 ...

  2. PHPer 应聘见闻

    关于我自己 我,很普通的一个开发,88年出生在皖南山区.从小学到高中毕业都没想过自己会从事软件开发,高考的误打误撞,被某普通二本院校收编.大学浑浑噩噩,对软件开发也没多大的兴趣,11年毕业后来杭,面试 ...

  3. maven运行junit用例并生成报告maven-surefire-plugin,maven-antrun-extended-plugin

    转载:http://blog.csdn.net/hdyrz/article/details/78398964 测试类如下: package com.mmnn.test.testcase; import ...

  4. 使用IntelliJ IDEA创建Maven多模块项目

    转载:http://blog.csdn.net/xyw591238/article/details/52794788 使用Maven管理项目时,往往需要创建多个模块,模块之间存在相互引用的关系.对于M ...

  5. js 值和类型

    js中变量是没有类型的,只有值才有类型. 变量随时可以持有任何类型的值. <!DOCTYPE html> <html lang="zh"> <head ...

  6. SQL Server 2008R2发布与订阅的配置

    使用SQL Server的发布与订阅可以将一个数据库的数据实时传送到另一个数据库中,使用这种方式与Link Server相比可以减少对数据库的连接次数.下面介绍SQL Server 2008R2发布与 ...

  7. jmeter 之 BSF,BeanShell(转载)

    jmeter无法自行处理javascript,但是它可以用自带的BSF PreProcessor(BSF:面向java的脚本语言,支持javascript) (使用这个之前要把bsh-2.0b2.ja ...

  8. mysql-This version of MySQL doesn’t yet support ‘LIMIT & IN/ALL/ANY/SOME 错误解决

      这次国庆节回来后的测试中,在一个Mysql表达式中使用嵌套查询,出现了这个错误.原因是内层select语句带有limit子句.   在网上查了下,有文章指出: 比如这样的语句是不能正确执行的. s ...

  9. Python爬取豆瓣《复仇者联盟3》评论并生成乖萌的格鲁特

    代码地址如下:http://www.demodashi.com/demo/13257.html 1. 需求说明 本项目基于Python爬虫,爬取豆瓣电影上关于复仇者联盟3的所有影评,并保存至本地文件. ...

  10. iOS之基于FreeStreamer的简单音乐播放器(模仿QQ音乐)

    代码地址如下:http://www.demodashi.com/demo/11944.html 天道酬勤 前言 作为一名iOS开发者,每当使用APP的时候,总难免会情不自禁的去想想,这个怎么做的?该怎 ...