首页 > temp > 简明python教程 >
-
C#线程 入门
第一部分: 入门
介绍和概念
C#支持通过多线程并行执行代码。线程是一个独立的执行路径,能够与其他线程同时运行。C#客户端程序(控制台,WPF或Windows窗体)在CLR和操作系统自动创建的单个线程(“主”线程)中启动,并通过创建其他线程而成为多线程。这是一个简单的示例及其输出:
所有示例均假定导入了以下名称空间:
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字段。这导致“完成”打印一次而不是两次:
完成
静态字段提供了另一种在线程之间共享数据的方法。这是同一示例,其作为静态字段完成了:
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 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” 1,000次,然后显示“线程t已结束!”。紧接着。您可以在调用Join时包含一个超时(以毫秒为单位)或作为TimeSpan。然后,如果线程结束,则返回true;如果超时,则返回false。
Thread.Sleep将当前线程暂停指定的时间:
1
2
|
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中几乎轻松地执行相同的操作:
1
2
3
4
|
new Thread ( delegate () { ... }).Start(); |
另一种技术是将参数传递给Thread的Start方法: