C#支持经过多线程并行执行代码。线程是一个独立的执行路径,可以与其余线程同时运行。C#客户端程序(控制台,WPF或Windows窗体)在CLR和操做系统自动建立的单个线程(“主”线程)中启动,并经过建立其余线程而成为多线程。这是一个简单的示例及其输出:算法
全部示例均假定导入了如下名称空间:shell
using System;
using System.Threading;
class ThreadTest { static void Main() { Thread t = new Thread (WriteY); // Kick off a new thread t.Start(); // running WriteY() // Simultaneously, do something on the main thread. for (int i = 0; i < 1000; i++) Console.Write ("x"); } static void WriteY() { for (int i = 0; i < 1000; i++) Console.Write ("y"); } }
xxxxxxxxxxxxxxxxyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxyyyyyyyyyyyyy
yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
yyyyyyyyyyyyyxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
...
主线程建立一个新线程t,在该线程上运行一种方法,该方法反复打印字符“ y”。同时,主线程重复打印字符“ x”:数据库
一旦启动,线程的IsAlive属性将返回true,直到线程结束为止。当传递给线程构造函数的委托完成执行时,线程结束。一旦结束,线程将没法从新启动。编程
1 static void Main() 2 { 3 new Thread (Go).Start(); // Call Go() on a new thread 4 Go(); // Call Go() on the main thread 5 } 6 7 static void Go() 8 { 9 // Declare and use a local variable - 'cycles' 10 for (int cycles = 0; cycles < 5; cycles++) Console.Write ('?'); 11 }
??????????
在每一个线程的内存堆栈上建立一个单独的cycles变量副本,所以,能够预见的是,输出为十个问号。缓存
若是线程具备对同一对象实例的公共引用,则它们共享数据。例如:安全
class ThreadTest { bool done; static void Main() { ThreadTest tt = new ThreadTest(); // Create a common instance new Thread (tt.Go).Start(); tt.Go(); } // Note that Go is now an instance method void Go() { if (!done) { done = true; Console.WriteLine ("Done"); } } }
因为两个线程在同一个ThreadTest实例上调用Go(),所以它们共享done字段。这致使“完成”打印一次而不是两次:服务器
完成cookie
静态字段提供了另外一种在线程之间共享数据的方法。这是同一示例,其做为静态字段完成了:网络
class ThreadTest { static bool done; // Static fields are shared between all threads static void Main() { new Thread (Go).Start(); Go(); } static void Go() { if (!done) { done = true; Console.WriteLine ("Done"); } } }
这两个示例都说明了另外一个关键概念:线程安全的概念(或更确切地说,缺少安全性)。输出其实是不肯定的:“完成”有可能(尽管不太可能)打印两次。可是,若是咱们在Go方法中交换语句的顺序,则两次打印完成的机率会大大提升:多线程
static void Go() { if (!done) { Console.WriteLine ("Done"); done = true; } }
完成
完成(一般!)
问题在于,一个线程能够评估if语句是否正确,而另外一个线程正在执行WriteLine语句-在有机会将done设置为true以前。
补救措施是在读写公共字段时得到排他锁。 C#为此提供了lock语句:
class ThreadSafe { static bool done; static readonly object locker = new object(); static void Main() { new Thread (Go).Start(); Go(); } static void Go() { lock (locker) { if (!done) { Console.WriteLine ("Done"); done = true; } } } }
当两个线程同时争用一个锁(在这种状况下为锁柜)时,一个线程将等待或阻塞,直到锁可用为止。在这种状况下,能够确保一次只有一个线程能够输入代码的关键部分,而且“完成”将仅打印一次。以这种方式受到保护的代码(在多线程上下文中不受不肯定性的影响)被称为线程安全的。共享数据是形成多线程复杂性和模糊错误的主要缘由。尽管一般是必不可少的,但保持尽量简单是值得的。线程虽然被阻止,但不会消耗CPU资源。
您能够经过调用其Join()来等待另外一个线程结束。例如:
static void Main() { Thread t = new Thread (Go); t.Start(); t.Join(); Console.WriteLine ("Thread t has ended!"); } static void Go() { for (int i = 0; i < 1000; i++) Console.Write ("y"); }
这将打印“ y” 1,000次,而后显示“线程t已结束!”。紧接着。您能够在调用Join时包含一个超时(以毫秒为单位)或做为TimeSpan。而后,若是线程结束,则返回true;若是超时,则返回false。
Thread.Sleep将当前线程暂停指定的时间:
Thread.Sleep (TimeSpan.FromHours (1)); // sleep for 1 hour Thread.Sleep (500); // sleep for 500 milliseconds
在等待睡眠或加入时,线程被阻塞,所以不消耗CPU资源。
Thread.Sleep(0)当即放弃线程的当前时间片,自动将CPU移交给其余线程。 Framework 4.0的新Thread.Yield()方法具备相同的做用-只是它只放弃运行在同一处理器上的线程。
Sleep(0)或Yield在生产代码中偶尔用于进行高级性能调整。它也是帮助发现线程安全问题的出色诊断工具:若是在代码中的任意位置插入Thread.Yield()会破坏程序,则几乎确定会出现错误。
多线程由线程调度程序在内部进行管理,这是CLR一般委托给操做系统的功能。线程调度程序确保为全部活动线程分配适当的执行时间,而且正在等待或阻塞的线程(例如,排他锁或用户输入)不会浪费CPU时间。
在单处理器计算机上,线程调度程序执行时间切片-在每一个活动线程之间快速切换执行。在Windows下,时间片一般在数十毫秒的区域中-远大于在一个线程与另外一个线程之间实际切换上下文时的CPU开销(一般在几微秒的区域)。
在多处理器计算机上,多线程是经过时间片和真正的并发实现的,其中不一样的线程在不一样的CPU上同时运行代码。几乎能够确定,因为操做系统须要服务本身的线程以及其余应用程序的线程,所以还会有一些时间片。
当线程的执行因为外部因素(例如时间分段)而中断时,能够说该线程被抢占。在大多数状况下,线程没法控制其抢占的时间和地点。
线程相似于您的应用程序在其中运行的操做系统进程。正如进程在计算机上并行运行同样,线程在单个进程中并行运行。流程彼此彻底隔离;线程的隔离度有限。特别是,线程与在同一应用程序中运行的其余线程共享(堆)内存。这部分是为何线程有用的缘由:例如,一个线程能够在后台获取数据,而另外一个线程能够在数据到达时显示数据
线程的使用和滥用
多线程有不少用途。这是最多见的:
维护响应式用户界面
经过在并行的“工做者”线程上运行耗时的任务,主UI线程能够自由继续处理键盘和鼠标事件。
有效利用本来被阻塞的CPU
当线程正在等待另外一台计算机或硬件的响应时,多线程颇有用。当一个线程在执行任务时被阻塞时,其余线程能够利用本来没有负担的计算机。
并行编程
若是以“分而治之”策略在多个线程之间共享工做负载,则执行密集计算的代码能够在多核或多处理器计算机上更快地执行(请参阅第5部分)。
投机执行
在多核计算机上,有时能够经过预测可能须要完成的事情而后提早进行来提升性能。 LINQPad使用此技术来加快新查询的建立。一种变化是并行运行许多不一样的算法,这些算法均可以解决同一任务。不论哪个先得到“胜利”,当您不知道哪一种算法执行最快时,这才有效。
容许同时处理请求
在服务器上,客户端请求能够同时到达,所以须要并行处理(若是使用ASP.NET,WCF,Web服务或远程处理,.NET Framework会为此自动建立线程)。这在客户端上也颇有用(例如,处理对等网络-甚至来自用户的多个请求)。
使用ASP.NET和WCF之类的技术,您可能甚至不知道多线程正在发生-除非您在没有适当锁定的状况下访问共享数据(也许经过静态字段),不然会破坏线程安全性。
线程还附带有字符串。最大的问题是多线程会增长复杂性。有不少线程自己并不会带来不少复杂性。确实是线程之间的交互(一般是经过共享数据)。不管交互是不是有意的,这都适用,而且可能致使较长的开发周期以及对间歇性和不可复制错误的持续敏感性。所以,必须尽可能减小交互,并尽量地坚持简单且通过验证的设计。本文主要侧重于处理这些复杂性。删除互动,无需多说!
好的策略是将多线程逻辑封装到可重用的类中,这些类能够独立检查和测试。框架自己提供了许多更高级别的线程结构,咱们将在后面介绍。
线程化还会在调度和切换线程时(若是活动线程多于CPU内核)会致使资源和CPU成本的增长,而且还会产生建立/拆除的成本。多线程并不老是能够加快您的应用程序的速度-若是使用过多或使用不当,它甚至可能减慢其速度。例如,当涉及大量磁盘I / O时,让几个工做线程按顺序运行任务比一次执行10个线程快得多。 (在“使用等待和脉冲发送信号”中,咱们描述了如何实现仅提供此功能的生产者/消费者队列。)
正如咱们在简介中所看到的,线程是使用Thread类的构造函数建立的,并传入ThreadStart委托,该委托指示应从何处开始执行。定义ThreadStart委托的方法以下:
public delegate void ThreadStart();
在线程上调用Start,而后将其设置为运行。线程继续执行,直到其方法返回为止,此时线程结束。这是使用扩展的C#语法建立TheadStart委托的示例:
1 class ThreadTest 2 { 3 static void Main() 4 { 5 Thread t = new Thread (new ThreadStart (Go)); 6 7 t.Start(); // Run Go() on the new thread. 8 Go(); // Simultaneously run Go() in the main thread. 9 } 10 11 static void Go() 12 { 13 Console.WriteLine ("hello!"); 14 } 15 }
在此示例中,线程t在主线程调用Go()的同一时间执行Go()。结果是两个接近即时的问候。
经过仅指定一个方法组,并容许C#推断ThreadStart委托,能够更方便地建立线程:
Thread t = new Thread (Go); //无需显式使用ThreadStart
另外一个快捷方式是使用lambda表达式或匿名方法:
static void Main() { Thread t = new Thread ( () => Console.WriteLine ("Hello!") ); t.Start(); }
将参数传递给线程的target方法的最简单方法是执行一个lambda表达式,该表达式使用所需的参数调用该方法:
1 static void Main() 2 { 3 Thread t = new Thread ( () => Print ("Hello from t!") ); 4 t.Start(); 5 } 6 7 static void Print (string message) 8 { 9 Console.WriteLine (message); 10 }
使用这种方法,您能够将任意数量的参数传递给该方法。您甚至能够将整个实现包装在多语句lambda中:
new Thread (() => { Console.WriteLine ("I'm running on another thread!"); Console.WriteLine ("This is so easy!"); }).Start();
您可使用匿名方法在C#2.0中几乎轻松地执行相同的操做:
new Thread (delegate() { ... }).Start();
另外一种技术是将参数传递给Thread的Start方法:
static void Main() { Thread t = new Thread (Print); t.Start ("Hello from t!"); } static void Print (object messageObj) { string message = (string) messageObj; // We need to cast here Console.WriteLine (message); }
之因此可行,是由于Thread的构造函数被重载为接受两个委托之一:
public delegate void ThreadStart(); public delegate void ParameterizedThreadStart (object obj);
ParameterizedThreadStart的局限性在于它仅接受一个参数。并且因为它是object类型的,所以一般须要强制转换。
如咱们所见,lambda表达式是将数据传递到线程的最强大的方法。可是,您必须当心在启动线程后意外修改捕获的变量,由于这些变量是共享的。例如,考虑如下内容:
for (int i = 0; i < 10; i++) new Thread (() => Console.Write (i)).Start();
输出是不肯定的!这是一个典型的结果:
0223557799
问题在于,i变量在循环的整个生命周期中都指向相同的内存位置。所以,每一个线程都会在变量上调用Console.Write,该变量的值可能会随着运行而改变!
这相似于咱们在C#4.0的第八章“捕获变量”中描述的问题。问题不在于多线程,而是与C#捕获变量的规则有关(在for和foreach循环的状况下这是不但愿的)。
解决方案是使用以下临时变量:
for (int i = 0; i < 10; i++) { int temp = i; new Thread (() => Console.Write (temp)).Start(); }
如今,可变温度是每一个循环迭代的局部变量。所以,每一个线程捕获一个不一样的内存位置,这没有问题。咱们能够经过如下示例更简单地说明早期代码中的问题:
string text = "t1"; Thread t1 = new Thread ( () => Console.WriteLine (text) ); text = "t2"; Thread t2 = new Thread ( () => Console.WriteLine (text) ); t1.Start(); t2.Start();
由于两个lambda表达式都捕获相同的文本变量,因此t2被打印两次
t2
t2
每一个线程都有一个Name属性,能够设置该属性以利于调试。这在Visual Studio中特别有用,由于线程的名称显示在“线程窗口”和“调试位置”工具栏中。您只需设置一个线程名称便可;稍后尝试更改它会引起异常。
静态Thread.CurrentThread属性为您提供当前正在执行的线程。在如下示例中,咱们设置主线程的名称:
class ThreadNaming { static void Main() { Thread.CurrentThread.Name = "main"; Thread worker = new Thread (Go); worker.Name = "worker"; worker.Start(); Go(); } static void Go() { Console.WriteLine ("Hello from " + Thread.CurrentThread.Name); } }
默认状况下,您显式建立的线程是前台线程。只要前台线程中的任何一个正在运行,它就可使应用程序保持活动状态,然后台线程则不会。一旦全部前台线程完成,应用程序结束,全部仍在运行的后台线程终止。
线程的前台/后台状态与其优先级或执行时间的分配无关。
您可使用其IsBackground属性查询或更改线程的背景状态。这是一个例子:
class PriorityTest { static void Main (string[] args) { Thread worker = new Thread ( () => Console.ReadLine() ); if (args.Length > 0) worker.IsBackground = true; worker.Start(); } }
若是不带任何参数调用此程序,则工做线程将处于前台状态,并将在ReadLine语句上等待用户按Enter。同时,主线程退出,可是应用程序继续运行,由于前台线程仍然处于活动状态。
另外一方面,若是将参数传递给Main(),则会为工做程序分配背景状态,而且在主线程结束(终止ReadLine)时,程序几乎当即退出。
当进程以这种方式终止时,将规避后台线程执行堆栈中的全部finally块。若是您的程序最终使用(或使用)块来执行清理工做(例如释放资源或删除临时文件),则会出现问题。为了不这种状况,您能够在退出应用程序后显式等待此类后台线程。
有两种方法能够实现此目的:
在这两种状况下,您都应指定一个超时时间,以便在因为某种缘由而拒绝完成的叛逆线程时能够放弃它。这是您的备份退出策略:最后,您但愿您的应用程序关闭-无需用户从任务管理器中寻求帮助!
若是用户使用任务管理器强制结束.NET进程,则全部线程都“掉线”,就好像它们是后台线程同样。这是观察到的,而不是记录的行为,而且它可能因CLR和操做系统版本而异。
前景线程不须要这种处理,可是您必须注意避免可能致使线程没法结束的错误。应用程序没法正常退出的常见缘由是活动的前台线程的存在。
线程的“优先级”属性肯定相对于操做系统中其余活动线程而言,执行时间的长短以下:
枚举ThreadPriority {最低,低于正常,正常,高于正常,最高}
仅在同时激活多个线程时,这才有意义。
在提升线程的优先级以前,请仔细考虑-这可能致使诸如其余线程的资源匮乏之类的问题。
提高线程的优先级并使其没法执行实时工做,由于它仍然受到应用程序进程优先级的限制。要执行实时工做,您还必须使用System.Diagnostics中的Process类提升流程优先级(咱们没有告诉您如何执行此操做):
using (Process p = Process.GetCurrentProcess()) p.PriorityClass = ProcessPriorityClass.High;
实际上,ProcessPriorityClass.High比最高优先级低了一个等级:实时。将进程优先级设置为“实时”会指示OS,您从不但愿该进程将CPU时间浪费给另外一个进程。若是您的程序进入意外的无限循环,您甚至可能会发现操做系统已锁定,只剩下电源按钮能够拯救您!所以,“高”一般是实时应用程序的最佳选择。
若是您的实时应用程序具备用户界面,则提升进程优先级将给屏幕更新带来过多的CPU时间,从而减慢整个计算机的速度(尤为是在UI复杂的状况下)。下降主线程的优先级并提升进程的优先级可确保实时线程不会因屏幕重绘而被抢占,但不会解决使其余应用程序耗尽CPU时间的问题,由于操做系统仍会分配整个过程的资源不成比例。理想的解决方案是使实时工做程序和用户界面做为具备不一样进程优先级的单独应用程序运行,并经过远程处理或内存映射文件进行通讯。内存映射文件很是适合此任务。简而言之,咱们将在C#4.0的第14和25章中解释它们的工做原理。
即便提升了流程优先级,托管环境在处理严格的实时需求方面的适用性也受到限制。除了由自动垃圾收集引发的延迟问题外,操做系统(甚至对于非托管应用程序)可能还会带来其余挑战,而这些挑战最好经过专用硬件或专用实时平台来解决。
建立线程时,做用域中的任何try / catch / finally块都与线程开始执行时无关。考虑如下程序:
public static void Main() { try { new Thread (Go).Start(); } catch (Exception ex) { // We'll never get here! Console.WriteLine ("Exception!"); } } static void Go() { throw null; } // Throws a NullReferenceException
此示例中的try / catch语句无效,而且新建立的线程将受到未处理的NullReferenceException的阻碍。当您认为每一个线程都有一个独立的执行路径时,此行为颇有意义。
补救措施是将异常处理程序移至Go方法中:
public static void Main() { new Thread (Go).Start(); } static void Go() { try { // ... throw null; // The NullReferenceException will get caught below // ... } catch (Exception ex) { // Typically log the exception, and/or signal another thread // that we've come unstuck // ... } }
在生产应用程序中的全部线程进入方法上都须要一个异常处理程序,就像在主线程上同样(一般在执行堆栈中处于更高级别)。未处理的异常会致使整个应用程序关闭。与一个丑陋的对话!
在编写此类异常处理块时,不多会忽略该错误:一般,您会记录异常的详细信息,而后显示一个对话框,容许用户自动将这些详细信息提交到您的Web服务器。而后,您可能会关闭该应用程序-由于该错误有可能破坏了程序的状态。可是,这样作的代价是用户将丢失其最近的工做-例如打开的文档。
WPF和Windows Forms应用程序的“全局”异常处理事件(Application.DispatcherUnhandledException和Application.ThreadException)仅针对在主UI线程上引起的异常触发。您仍然必须手动处理工做线程上的异常。
AppDomain.CurrentDomain.UnhandledException在任何未处理的异常上触发,但没有提供防止应用程序随后关闭的方法。可是,在某些状况下,您不须要处理工做线程上的异常,由于.NET Framework会为您处理异常。这些将在接下来的部分中介绍,分别是:
每当启动线程时,都会花费数百微秒来组织诸如新鲜的私有局部变量堆栈之类的事情。每一个线程(默认状况下)也消耗大约1 MB的内存。线程池经过共享和回收线程来减小这些开销,从而容许在很是细粒度的级别上应用多线程,而不会影响性能。当利用多核处理器以“分而治之”的方式并行执行计算密集型代码时,这颇有用。
线程池还限制了将同时运行的工做线程总数。过多的活动线程限制了操做系统的管理负担,并使CPU缓存无效。一旦达到限制,做业将排队并仅在另外一个做业完成时才开始。这使任意并发的应用程序(例如Web服务器)成为可能。 (异步方法模式是一种高级技术,经过高效利用池化线程来进一步实现这一点;咱们在C#4.0的第23章“ Nutshell”中对此进行了描述)。
有多种进入线程池的方法:
如下构造间接使用线程池:
任务并行库(TPL)和PLINQ具备足够的功能和高级功能,即便在线程池不重要的状况下,您也但愿使用它们来协助多线程。咱们将在第5部分中详细讨论这些内容。如今,咱们将简要介绍如何使用Task类做为在池线程上运行委托的简单方法。
使用池线程时须要注意如下几点:
您能够经过Thread.CurrentThread.IsThreadPoolThread属性查询当前是否在池化线程上执行。
您可使用“任务并行库”中的“任务”类轻松地输入线程池。 Task类是在Framework 4.0中引入的:若是您熟悉较早的构造,请考虑将非通用Task类替换为ThreadPool.QueueUserWorkItem,而将通用Task <TResult>替换为异步委托。与旧版本相比,新版本的结构更快,更方便且更灵活。
要使用非泛型Task类,请调用Task.Factory.StartNew,并传入目标方法的委托:
static void Main() // The Task class is in System.Threading.Tasks { Task.Factory.StartNew (Go); } static void Go() { Console.WriteLine ("Hello from the thread pool!"); }
Task.Factory.StartNew返回一个Task对象,您可使用该对象来监视任务-例如,您能够经过调用其Wait方法来等待它完成。
调用任务的Wait方法时,全部未处理的异常均可以方便地从新抛出到主机线程中。 (若是您不调用Wait而是放弃任务,则未处理的异常将像普通线程同样关闭进程。)
通用Task <TResult>类是非通用Task的子类。它使您能够在完成执行后从任务中获取返回值。在下面的示例中,咱们使用Task <TResult>下载网页:
static void Main() { // Start the task executing: Task<string> task = Task.Factory.StartNew<string> ( () => DownloadString ("http://www.linqpad.net") ); // We can do other work here and it will execute in parallel: RunSomeOtherMethod(); // When we need the task's return value, we query its Result property: // If it's still executing, the current thread will now block (wait) // until the task finishes: string result = task.Result; } static string DownloadString (string uri) { using (var wc = new System.Net.WebClient()) return wc.DownloadString (uri); }
(突出显示<string>类型的参数是为了清楚:若是咱们省略它,则能够推断出它。)
查询包含在AggregateException中的任务的Result属性时,全部未处理的异常都会自动从新抛出。可是,若是您没法查询其Result属性(而且不调用Wait),则任何未处理的异常都会使该过程失败。
任务并行库具备更多功能,特别适合利用多核处理器。咱们将在第5部分中继续讨论TPL。
若是目标是.NET Framework的早期版本(4.0以前),则不能使用任务并行库。相反,您必须使用一种较旧的结构来输入线程池:ThreadPool.QueueUserWorkItem和异步委托。二者之间的区别在于异步委托使您能够从线程返回数据。异步委托也将任何异常封送回调用方。
要使用QueueUserWorkItem,只需使用要在池线程上运行的委托调用此方法:
static void Main() { ThreadPool.QueueUserWorkItem (Go); ThreadPool.QueueUserWorkItem (Go, 123); Console.ReadLine(); } static void Go (object data) // data will be null with the first call. { Console.WriteLine ("Hello from the thread pool! " + data); } Hello from the thread pool! Hello from the thread pool! 123
咱们的目标方法Go必须接受单个对象参数(以知足WaitCallback委托)。就像使用ParameterizedThreadStart同样,这提供了一种将数据传递给方法的便捷方法。与Task不一样,QueueUserWorkItem不会返回对象来帮助您随后管理执行。另外,您必须在目标代码中显式处理异常-未处理的异常将使程序瘫痪。
ThreadPool.QueueUserWorkItem没有提供一种简单的机制来在线程执行完毕后从线程取回返回值。异步委托调用(简称异步委托)解决了这一问题,容许在两个方向上传递任意数量的类型化参数。此外,异步委托上未处理的异常能够方便地在原始线程(或更准确地说是调用EndInvoke的线程)上从新抛出,所以不须要显式处理。
不要将异步委托与异步方法(以Begin或End开头的方法,例如File.BeginRead / File.EndRead)混淆。异步方法在外部遵循相似的协议,可是它们存在是为了解决更难的问题,咱们将在C#4.0的第23章“简而言之”中进行描述。
经过异步委托启动工做任务的方法以下:
在下面的示例中,咱们使用异步委托调用与主线程并发执行,主线程是一种返回字符串长度的简单方法:
static void Main() { Func<string, int> method = Work; IAsyncResult cookie = method.BeginInvoke ("test", null, null); // // ... here's where we can do other work in parallel... // int result = method.EndInvoke (cookie); Console.WriteLine ("String length is: " + result); }
static int Work (string s) { return s.Length; }
EndInvoke作三件事。首先,它会等待异步委托完成执行(若是还没有执行)。其次,它接收返回值(以及任何ref或out参数)。第三,它将全部未处理的工做程序异常抛出回调用线程。
若是您使用异步委托调用的方法没有返回值,则仍然(在技术上)有义务调用EndInvoke。实际上,这是有争议的。没有EndInvoke警察对违规者进行处罚!可是,若是您选择不调用EndInvoke,则须要考虑worker方法上的异常处理,以免无提示的失败。
您还能够在调用BeginInvoke时指定一个回调委托-一种接受IAsyncResult对象的方法,该方法在完成后会自动调用。这容许煽动线程“忘记”异步委托,可是在回调端须要一些额外的工做:
static void Main() { Func<string, int> method = Work; method.BeginInvoke ("test", Done, method); // ... // } static int Work (string s) { return s.Length; } static void Done (IAsyncResult cookie) { var target = (Func<string, int>) cookie.AsyncState; int result = target.EndInvoke (cookie); Console.WriteLine ("String length is: " + result); }
BeginInvoke的最后一个参数是填充IAsyncResult的AsyncState属性的用户状态对象。它能够包含您喜欢的任何内容;在这种状况下,咱们使用它将方法委托传递给完成回调,所以咱们能够在其上调用EndInvoke。
线程池从其池中的一个线程开始。分配任务后,池管理器会“注入”新线程以应对额外的并发工做负载(最大限制)。在足够长时间的不活动以后,若是池管理器怀疑这样作会致使更好的吞吐量,则能够“退出”线程。
您能够经过调用ThreadPool.SetMaxThreads;来设置池将建立的线程的上限。默认值为:
(这些数字可能会因硬件和操做系统而异。)之因此有不少缘由,是为了确保某些线程被阻塞(在等待某种条件(例如,来自远程计算机的响应)时处于空闲状态)的进度。
您还能够经过调用ThreadPool.SetMinThreads设置下限。下限的做用是微妙的:这是一种高级优化技术,它指示池管理器在达到下限以前不要延迟线程的分配。当线程被阻塞时,提升最小线程数可提升并发性(请参见侧栏)。
默认的下限是每一个处理器内核一个线程-容许所有CPU利用率的最小值。可是,在服务器环境(例如IIS下的ASP.NET)上,下限一般要高得多-多达50个或更多。
实际上,将线程池的最小线程数增长到x并不会实际上强制当即建立x个线程-线程仅根据须要建立。相反,它指示池管理器在须要它们时当即最多建立x个线程。那么,问题是,为何在须要时线程池会延迟建立线程的时间呢?
答案是防止短暂的短暂活动致使线程的彻底分配,从而忽然膨胀应用程序的内存空间。为了说明这一点,请考虑运行一个客户端应用程序的四核计算机,该应用程序一次可处理40个任务。若是每一个任务执行10毫秒的计算,则假设工做在四个核心之间分配,整个任务将在100毫秒内结束。理想状况下,咱们但愿40个任务刚好在四个线程上运行:
这正是线程池的工做方式。只要将线程数与内核数进行匹配,只要有效地使用了线程(在这种状况下就是这样),程序就能够在不影响性能的状况下保留较小的内存占用。
可是如今假设,每一个任务而不是工做10毫秒,而是查询Internet,在本地CPU空闲时等待半秒以响应。池管理器的线程经济策略崩溃了;如今建立更多线程会更好,所以全部Internet查询均可以同时发生。
幸运的是,池管理器有一个备份计划。若是其队列保持静止状态超过半秒,它将经过建立更多线程(每半秒一个)来响应,直至达到线程池的容量。
延迟的半秒是一把两刃剑。一方面,这意味着一次短暂的短暂活动不会使程序忽然消耗掉没必要要的40 MB(或更多)内存。另外一方面,当池中的线程阻塞时,例如查询数据库或调用WebClient.DownloadFile时,它可能没必要要地延迟事情。所以,能够经过调用SetMinThreads来告诉池管理器不要延迟前x个线程的分配:
ThreadPool.SetMinThreads(50,50);
(第二个值指示要分配给I / O完成端口的线程数,由APM使用,具体请参见C#4.0第23章的内容。)
默认值为每一个内核一个线程。