多线程技术(1):开篇
时间:2010-10-05 来源:HorsonJin
http://www.crazydevelop.com/Content.aspx?ID=402
(文/金延涛)
概念介绍
通过多线程,C#支持并行处理。每个线程和其他线程并行运行,并且每个线程有自己的执行路径。
CLR和OS("Main"thread)为一个C#程序(Console,WPF或者Windows Forms)自动创建一个线程,在这些程序中通过创建新的线程来运用多线程技术。
以一个简单的例子来演示多线程技术:
所有的例子都假定如下的命名空间被引入。
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");
}
}
输出结果:
xxxxxxxxxxxxxxxxyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
yyyxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx...
主线程创建了一个新的线程,新线程运行一个方法反复输出字符"y”,同步地,主线程反复输出字符"x“。
一旦被启动,线程的IsAlive属性就返回true,直到线程终止的那一刻。当委托通知线程构造方完成运行,线程就终止了。一旦线程终止了,线程就不能重新启动。CLR为每个线程分配内存栈空间,所以线程中的本地变量是相互隔离的。在下一个例子中,我定义了一个方法,该方法包含一个本地变量,然后在主线程和新线程中分别调用该方法:
static void Main()
{
new Thread (Go).Start(); // Call Go() on a new thread
Go(); // Call Go() on the main thread
}
static void Go()
{
// Declare and use a local variable - 'cycles'
for (int cycles = 0; cycles < 5; cycles++) Console.Write ('?');
}
输出结果:
??????????
cycles变量在每个线程自己的内存栈空间中被创建,所以输出结果显示的是10个问号。
如果是对同一个对象实例的引用,线程将共享数据。
例子:
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");
}
}
}
输出结果:
Done
因为两个线程调用同一个类"ThreadTest"的实例的方法"Go()”,它们共享属性"done”。所以结果显示一个done,而不是两个。
静态属性提供了另外一种在两个线程间共享数据的方式。示例:
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");
}
}
}
两个例子显示另一个关键问题:那就是线程安全(相当缺乏的处理!)输出事实上是不确定的,它有可能也可能不,"done”被输出两次。然而,我们调整一下Go方法语句的顺序,"done"本来输出一次,却会莫名其妙地输出了两次。示例:
static void Go()
{
if (!done)
{
Console.WriteLine ("Done");
done = true;
}
}
输出结果:
Done
Done (usually!)
出现这种结果的原因是:在程序有机会吧done设置成true之前,一个线程正在执行if语句,同时另一个线程正在执行WriteLine语句。
解决办法是在读取和写入共享数据时获取一个唯一锁。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;
}
}
}
}
当两个线程竞争一个锁时(该示例锁为locker),一个线程等待或阻塞,直到锁重新变成可用。在该示例中,可以确保只有一个线程在同一时间进入代码的临界块,"done"将被输出一次。代码被以这种方式保护,避免多线程产生的不确定被叫做“线程安全”。
注意:共享的数据是在多线程中产生复杂性和模糊错误的主要因素。所以要尽可能保持简单;一个线程被阻塞时不会耗费cpu资源。
Join and Sleep
通过调用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"1000次,然后立即在后面输出"Thread t has ended!"。在调用Join方法时也可以设置超时时间,指定毫秒数或使用TimeSpan都可以。如果线程结束了或者超时了Join方法将返回true或false。
使当前线程暂停指定时间:
Thread.Sleep (TimeSpan.FromHours (1)); // sleep for 1 hour
Thread.Sleep (500); // sleep for 500 milliseconds
使用Sleep或Join方法时,一个线程被阻塞,并且不会耗费CPU资源。
Thread.Sleep(0)放弃线程当前的时间片,移交CPU中的其他线程。Framework 4.0中的新方法Thread.Yield()方法也可以实现同样的功能,但是放弃的CPU时间片仅移交给运行在同一处理器中的其它线程。
(原文:Sleep(0) or Yield is occasionally useful in production code for advanced performance tweaks. It’s also an excellent diagnostic tool for helping to uncover thread safety issues: if inserting Thread.Yield() anywhere in your code makes or breaks the program, you almost certainly have a bug.)
多线程通过thread scheduler被管理,thread scheduler是CLR和OS的中间功能。一个thread scheduler确保所有活动线程被分配到需要的执行时间,处于等待状态的线程和被阻塞的线程(比如被唯一锁阻塞的或者等待用户输入的)不会耗费CPU时间。
在单处理器计算机,线程调度程序执行时间片 - 快速地在活动线程间切换执行。在Windows下,一个时间片通常在数万的毫秒范围 - 远低于CPU开销较大的在一个线程和另一个线程的之间的切换。(通常是在几微秒)。
在一个多处理器的计算机中,实现了多线程的时间片和并发性的真正混合,不同的线程运行在不同的CPU中。几乎可以肯定还会有一些时间片,需要服务操作系统的自己的线程 - 也会有其他操作系统自己的应用程序。
据说一个线程的执行因为一些外部因素被中断,线程会被抢占(比如时间片被中断)。在非常多的情况下,线程不能控制什么时候和在哪里被抢占。
线程 vs 进程
在你运行的应用程序中线程和进程是非常相似的。仅仅是进程平行地运行在计算机中,线程平行地运行在单个进程中。进程是彼此完全独立的,线程仅仅是部分独立的。特别是,线程和运行在同一应用程序中的其它线程共享内存。这对线程是非常有用的:例如,一个线程在在后台取数据,同时另外一个线程显示取到的数据。
线程的用处和滥用
多线程有很多用处,这里列出一些最常用的用处:
保持可响应的用户界面 在一个需要时间消耗的任务运行在"worker"线程上,主UI线程可以自由地处理键盘和鼠标事件。 高效利用一个没有阻塞的CPU 多线程是非常有用的,当一个线程等待另一台计算机或硬件的响应时。在一个线程被阻塞时,其他的线程能充分利用其他已经空闲的计算机。 并行程序使用多核或多处理器的计算机,如果负载被分配到多个线程中,代码的运算能力会执行地更快。 (see Part 5). Speculative execution 在多核处理器中,有时你能通过预测需要处理的内容来改善性能。LINQPad使用这个技术加速创建新的查询。并行运行多个不同的算法来解决同一个任务。允许多个请求被同步处理 在一台服务器上,客户端请求同时到达,需要并行处理(如果你使用ASP.NET,WCF,Web Services,或者Remoting)。这在客户端也是非常有用的(例如P2P的网络处理或者是来自一个用户的多个请求)。With technologies such as ASP.NET and WCF, you may be unaware that multithreading is even taking place — unless you access shared data (perhaps via static fields) without appropriate locking, running afoul of thread safety.
多线程也会产生额外的问题。最大的问题是多线程增加复杂度。多线程本身或内部就比较复杂;线程之间相互影响(特别是通过共享数据)。这些应用无论是否是有意的相互影响,会导致较长的开发周期和持续间歇易出现的不易重复的bugs。出于这个原因,应使相互影响减到最少,坚持简单和尽可能避免过度设计。这篇文章非常强调处理这些复杂问题;而对于消除相互影响说的很少。
一个好的策略是把多线程的逻辑封装到一个可复用的类里,这个类可以独立地被检查和测试。The Framework itself offers many higher-level threading constructs, which we cover later.
Threading会在调度和转换线程时消耗资源和CPU成本(当活动线程超过CPU Cores时),而且还有线程的创建和释放成本。多线程不是总是在加速你的应用程序 - 如果你使用的过多或者不恰当。例如,当消耗很多的磁盘I/O被调用时,使用两个工作线程按顺序运行任务,就比用10个线程同时运行要快。 (In Signaling with Wait and Pulse, we describe how to implement a producer/consumer queue, which provides just this functionality.)
创建和启动一个线程
就像我们在介绍中看到的,线程在类Thread的的构造器中被创建,通过委托"ThreadStart"指明在哪里开始运行。以下代码显示ThreadStart如何被定义:
public delegate void ThreadStart();
调用线程的Start方法设定线程开始运行。线程持续执行,直到线程终止。这里有一个例子使用C#的语法创建委托"ThreadStart"的一个实例:
class ThreadTest
{
static void Main()
{
Thread t = new Thread (new ThreadStart (Go));
t.Start(); // Run Go() on the new thread.
Go(); // Simultaneously run Go() in the main thread.
}
static void Go()
{
Console.WriteLine ("hello!");
}
}
在这个例子中,在线程执行Go()的同时,主线程也调用了Go()。结果是出现两个一样的"hello"。
A thread can be created more conveniently by specifying just a method group — and allowing C# to infer the ThreadStart delegate:
Thread t = new Thread (Go); // No need to explicitly use ThreadStart
另一个快捷方式是使用lambda表达式或者匿名方法:
static void Main()
{
Thread t = new Thread ( () => Console.WriteLine ("Hello!") );
t.Start();
}
给线程传递数据
最简单的方式是使用lambda表达式,使用期望的参数调用方法:
static void Main()
{
Thread t = new Thread ( () => Print ("Hello from t!") );
t.Start();
}
static void Print (string message)
{
Console.WriteLine (message);
}
在这个步骤中,你能专递给方法任意数目的参数。你能实现多语句的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表达式和捕获参数(capturing variables)
正如我们看到的,Lambda表达式是向线程传递参数的一种非常棒的方式,你一定要小心在启动线程之后以外地修改捕获参数,因为这些参数是共享的。请思考下面的例子:
for (int i = 0; i < 10; i++) new Thread (() => Console.Write (i)).Start();
输出结果是不确定的,如下是一种典型的结果:
0223557799
问题是在循环运行期间,变量i使用相同的内存地址。因此每一个线程调用Console.Write输出变量时,变量的值都有可能在运行时改变。
这和Chapter 8 of C# 4.0 in a Nutshell描述的"Captured Variables"非常相似。只是关于多线程的更少,关于C#捕获变量的规则更多。
使用临时变量的解决方案如下:
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表达式使用相同的变量"text",t2被输出两次,结果为:
t2t2
命名线程
每个线程有一个Name属性,你可以设置Name属性值以方便调试。这在Visual Studio中是非常有用的,因为线程的名字可以在Threads window和Debug Location toolbar中显示。你只能设置一次线程的名字,之后再修改会抛出异常。
通过静态属性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如果这个程序被调用时没有参数,线程"worker"假设是前台线程并且等待ReadLine语句输入参数。同时,主线程存在,应用程序保持运行,因为有一个前台线程保持活动状态。
{
static void Main (string[] args)
{
Thread worker = new Thread ( () => Console.ReadLine() );
if (args.Length > 0)
worker.IsBackground = true;
worker.Start();
}
}
从另一方面讲,如果一个参数被传给了Main(),线程"worker"被设置成后台线程,主线程结束时后台线程也就结束了(ReadLine被强制结束)。
当进程以这种方式结束了,在所有后台线程中的finallly块都会终止。如果你的finally快执行清理工作如释放资源或者删除临时文件,这时就会有问题产生。如果在应用程序中存在后台线程,为了避免问题出现,你可以采用如下两种方式来解决:
- 如果你自己创建线程,在线程中调用Join方法。
- 如果你使用了线程池,使用EventWaitHandle。
在任何一种情况下,你应该指定TimeOut,因此你能够放弃一些因为某些原因不会结束的线程。这是你的备选策略:在最后你想关闭你的应用程序而不是通过Task Manage寻求帮助!
前台线程不要求这样处理,但是你一定要小心避免导致线程不会结束的bug,一个导致应用程序退出失败的普遍原因是有活动的前台线程存在。
线程优先级
线程Priority属性决定了线程相对于其他活动线程能够获得多少执行时间,优先级有如下的范围:
enum ThreadPriority { Lowest, BelowNormal, Normal, AboveNormal, Highest }
线程优先级只跟多个线程同步运行时才有关联。
在提升线程优先级之前先仔细思考,它会导致一些问题,比如其他线程的资源会获取不到。
提升线程优先级并不能让线程马上运行,因为线程优先级会被线程优先级扼杀掉。为了让线程马上运行,你有需要通过System.Diagnostics中的Process类提升进程的优先级:
using (Process p = Process.GetCurrentProcess()) p.PriorityClass = ProcessPriorityClass.High;
ProcessPriorityClass.High事实上是一个最高的优先级:实时运行。设置进程的优先级为实时运行,表明告诉操作系统相对于其他进程来说从不让该进程等待操作系统时间。如果你的程序意外输入了死循环,你或许会发现OS被锁死了,也只能使用电源按钮来帮助你了。因为这个原因,High优先级对于实时运行的程序是最好的选择。
如果实时运行程序有用户界面,提升进程优先级给界面更新更多CPU时间,将是整台电脑变慢(特别是界面比较复杂时)。降低主线程优先级的同时提高进程的优先级确保实时运行进程不会被屏幕刷新进程抢占,但是不能解决其他应用程序获取不到CPU时间的问题,因为OS将从整体上按比例给进程分配资源。理想的解决方案是让实时处理任务和用户界面以不同的进程优先级分别运行在单独的应用程序中,通过Remoting或者memory-mapped 文件通信。Memory-mapped文件非常适合这个任务;它们是如何工作的可以参考Chapters 14 and 25 of C# 4.0 in a Nutshell。
通过提升进程优先级,在托管环境中满足实时运行的需求有一个限制。在托管环境中使用自动垃圾回收,OS面临一些额外的挑战,即便对非托管的应用程序,最好的解决方案是指定硬件或者指定特定的实时运行平台。
异常处理
当线程开始运行时,任何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语句是无效的,新创建的线程被Go方法中的未处理的异常中断。这使你意识到每一个线程有自己独立的执行路径。
以上例子中问题的解决办法是把异常处理移到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和Winform程序的"global"异常处理事件仅仅把异常抛出到主UI线程。你仍然必须手工处理工作线程上的异常。
AppDomain.CurrentDomain.UnhandledException处理未处理异常,但是不会避免应用程序关闭。
但是有些情况,你不必处理工作线程上的异常,因为.NET Framework会为你处理。这些将在以下章节被讲解:
- Asynchronous delegates
- BackgroundWorker
- The Task Parallel Library (conditions apply)
线程池
当你启动一个线程,需要几百微秒时间组织一些事情,比如一个新的私有变量堆栈。默认情况下,一个线程消耗1MB的内存。线程池通过共享和回收线程削减开支,允许多线程以非常小粒度的级别被应用,不会造成性能损失。当使用多核处理器以“分而治之”的方式并行运行密集计算代码时非常有用。
线程池保持工作线程总数的上限。大多的活动线程因为管理负担和CPU缓存的无效处理会拖累操作系统。一旦达到极限,工作队列只有在另一个结束时启动。这是并发应用程序成为可能,比如Web server。(一部方法模式是一种先进的技术,可以充分利用线程池线程;我们在Chapter 23 of C# 4.0 in a Nutshell中有描述。)
有几个使用线程池的方式:
- 通过 Task Parallel Library (from Framework 4.0)
- 调用方法 ThreadPool.QueueUserWorkItem
- 通过使用 asynchronous delegates
- 通过使用 BackgroundWorker
以下几种技术间接使用线程池:
- WCF, Remoting, ASP.NET, 和ASMX Web Services 应用程序服务器
- System.Timers.Timer 和 System.Threading.Timer
- Framework中以Async结束的方法,比如WebClient(event-based asynchronous pattern),还有很多BeginXXX的方法(the asynchronous programming model pattern)
- PLINQ
TPL和PLINQ有强大的功能和很高的级别,即便线程池不是必要的,你仍然想在多线程使用它们。我们在Part 5会讨论这些内容。现在我们简单地看一下你如何使用 Task类实现在一个线程池中运行委托。
何时使用线程池线程,有一些问题需要考虑:
- 你不能设置池中线程的Name属性,调试更困难(尽管在Visual Studio的Threads Window中你能附加描述信息)
- 池中线程始终是 后台线程 (这常常不是一个问题).
- 如果你没有调用ThreadPool.SetMinThreads(查看Optimizing the Thread Pool),在应用程序早期Blocking一个池中线程可能触发额外的风险。
你可以自由改变池中线程的优先级 - 当线程被释放会池中时优先级被保存为Normal。
如果你通过属性Thread.CurrentThread.IsThreadPoolThread执行池中线程,你可以查询线程。
通过TPL进入线程池
在TPL中你能使用Task类非常容易地进入线程池。Task类在Framework4..0中有介绍。如果你对旧的类库非常熟悉,你可以考虑非泛型的Task类ThreadPool.QueueUserWorkItem,也可以使用asynchronous delegates。新的构造方法比老的更快,更方便,更有弹性。
如果使用非泛型的Task类,调用Task.Factory.StartNew,通过目标目标方法的委托传递给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的对象,你能使用该对象监控Task - 举个例子,能通过调用对象的Wait方法等待Task完成。
当你调用任务的Wait method时,任何未处理异常能够方便地被抛出到宿主线程。(如果你没有调用wait方法和抛弃任务,对于普通线程普通线程来说异常将关闭进程。)
泛型类Task<TResult>是非泛型类Task的子类。你能够在线程结束运行之后从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>类型参数高亮显示以便清晰地看到:我们有可能忽略它。)
当你调用Task的Result属性时,任何未处理的异常会自动抛出,异常被AggregateException包装。然而,如果查询Task的Result属性失败(并且你没有调用Wait方法),任何未处理异常将拖累进程。
TPL有很多的特性,特别适合于多核处理器。我们会在Part 5中继续讨论这个话题。
不使用TPL进入线程池
如果你使用.NET Framework的早期版本(4.0之前的版本),你不能使用TPL。取而代之的是,你必须使用一个老的进入线程池的方法:ThreadPool.QueueUserWorkItem和异步委托。两者不同的是异步委托能让你从线程中返回数据。异步委托也能集合异常给调用者。
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方法必须接受一个Object类型的参数(满足委托WaitCallback)。这提供了一个方便的方法来传递数据给方法,就像使用ParameterizedThreadStart。不想Task,QueueUserWorkItem没有返回一个对象帮你管理线程执行。而且,你必须在目标代码中处理异常 - 未处理异常将会拖累应用程序。
异步委托
在线程结束以后,ThreadPool.QueueUserWorkItem不提供一个获取线程返回值的简单方法。异步委托调用(简称异步委托)解决了这个问题,允许任意数目任意类型的参数在两个方向上传递。此外,异步委托的未处理异常也可以方便地被抛给原有线程(或者更准确地说,是调用EndInvoke的线程),所以他们不需要显示地处理异常。
不要混淆了异步委托和异步方法(以Begin或End开头的方法,例如File.BeginRead/File.EndRead)。异步方法遵循一个相似的协议表,他们的存在是为了解决更难得问题,我们将在Chapter 23 of C# 4.0 in a Nutshell描述。
这里描述了你如何通过异步委托启动一个工作任务:
- 使用你想同步运行的一个方法实例化一个委托。 (通常是一个已经预定义好的委托).
- 调用委托的BeginInvoke方法,保存返回值IAsyncResult,这个返回值在调用BeginInvoke时立即返回给调用者。在池中线程运行期间,你可以处理其他活动。
- 当你需要结果是,调用EndInvoke方法,把保存的IAsyncResult传递给该方法。
在下面的示例中,我们用一个异步委托调用和主线程同步执行,委托调用一个简单的方法,用于返回一个字符串的长度:
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做三件事情。第一,它等待异步委托结束运行。第二,它接受返回值。第三,它抛出任何未处理的异常给调用线程。
在调用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属性。它包含你想要的东西;在这个例子中,我们使用它把委托method传递给Callback方法,所以我们能在EndInvoke中使用它。
优化线程池
线程池启动在线程池中的线程。当任务被分配,池管理器插入新的线程处理额外的负载,知道最大的线程数。在一段充足的不活动期之后,池管理器清除掉那些不活动的线程,这使线程池有比较好的吞吐量。
你可以调用ThreadPool.SetMaxThreads设置线程池中线程数目的上限。默认值为:
- 1023 in Framework 4.0 in a 32-bit environment
- 32768 in Framework 4.0 in a 64-bit environment
- 250 per core in Framework 3.5
- 25 per core in Framework 2.0
(这些特点依据硬件和OS变化。)
你也可以调用ThreadPool.SetMinThreads设置更低的线程数下限。设置更低的线程数下限是非常奇妙的:这是一个先进的优化技术,通知池管理器不要再在线程分配时延误,直到到达下限。提高线程数下限将会在线程阻塞时提高并发。
默认的下限是一个处理器一个线程 - 最小线程数允许完全充分利用CPU。在服务器环境中(如在IIS下ASP.NET),最低线程数非常高 - 多达50或者更多。
最小线程数如何工作?
该段为原文内容,请大家帮忙翻译
Increasing the thread pool’s minimum thread count to x doesn’t actually force x threads to be created right away — threads are created only on demand. Rather, it instructs the pool manager to create up to x threads the instant they are required. The question, then, is why would the thread pool otherwise delay in creating a thread when it’s needed?
The answer is to prevent a brief burst of short-lived activity from causing a full allocation of threads, suddenly swelling an application’s memory footprint. To illustrate, consider a quad-core computer running a client application that enqueues 40 tasks at once. If each task performs a 10 ms calculation, the whole thing will be over in 100 ms, assuming the work is divided among the four cores. Ideally, we’d want the 40 tasks to run on exactly four threads:
- Any less and we’d not be making maximum use of all four cores.
- Any more and we’d be wasting memory and CPU time creating unnecessary threads.
And this is exactly how the thread pool works. Matching the thread count to the core count allows a program to retain a small memory footprint without hurting performance — as long as the threads are efficiently used (which in this case they are).
But now suppose that instead of working for 10 ms, each task queries the Internet, waiting half a second for a response while the local CPU is idle. The pool manager’s thread-economy strategy breaks down; it would now do better to create more threads, so all the Internet queries could happen simultaneously.
Fortunately, the pool manager has a backup plan. If its queue remains stationary for more than half a second, it responds by creating more threads — one every half-second — up to the capacity of the thread pool.
The half-second delay is a two-edged sword. On the one hand, it means that a one-off burst of brief activity doesn’t make a program suddenly consume an extra unnecessary 40 MB (or more) of memory. On the other hand, it can needlessly delay things when a pooled thread blocks, such as when querying a database or calling WebClient.DownloadFile. For this reason, you can tell the pool manager not to delay in the allocation of the first x threads, by calling SetMinThreads, for instance:
ThreadPool.SetMinThreads (50, 50);
(The second value indicates how many threads to assign to I/O completion ports, which are used by the APM, described in Chapter 23 of C# 4.0 in a Nutshell.)
The default value is one thread per core.