我们将写一个简单的例子。对于我们为什么使用一个新的线程来说这不是一个好例子但是它将我们稍后要提到的复杂问题都去掉了。创建一个simple_thread.cs文件并把下面的代码粘贴进去:
/************************************* /* Copyright (c) 2012 Daniel Dong * * Author:Daniel Dong * Blog: www.cnblogs.com/danielWise * Email: guofoo@163.com * */ using System; using System.Collections.Generic; using System.Text; using System.Threading; namespace SimpleThread { class SimpleThread { void SimpleMethod() { int i = 5; int x = 10; int result = i * x; Console.WriteLine("This code calculated the value " + result.ToString() + " from thread ID: " + Thread.CurrentThread.ManagedThreadId); } static void Main(string[] args) { //Calling the method from our current thread. SimpleThread simpleThread = new SimpleThread(); simpleThread.SimpleMethod(); //Calling the method on a new thread. ThreadStart ts = new ThreadStart(simpleThread.SimpleMethod); Thread t = new Thread(ts); t.Start(); Console.ReadLine(); } } }
现在保存,编译然后运行。你的输出会类似下面的结果:
我们继续研究这个小例子以确定我们确实了解发生了什么。由于我们已经知道线程相关功能都被封装到System.Threading命名空间中去了。所以我们必须首先将这个命名空间导入到我们的工程。一旦命名空间导入了,我们就可以在主线程中和新的工作线程中创建一个方法。我们使用SimpleMethod()方法:
void SimpleMethod() { int i = 5; int x = 10; int result = i * x; Console.WriteLine("This code calculated the value " + result.ToString() + " from thread ID: " + Thread.CurrentThread.ManagedThreadId); }
我们使用第一章介绍的AppDomain来找出正在运行哪个线程。这个方法不论何时执行,都显示一个哪个线程在执行操作的报告。
我们的程序入口是Main()方法。首先会调用与主方法运行在同一个线程的SimpleMethod()方法。下一步很重要:我们第一次可以看看如何创建一个线程。在C#中创建一个线程之前,我们首先必须创建一个ThreadStart委托实例。委托是一个面向对象的类型安全的程序指针。由于我们将要告诉一个线程执行什么方法,所以我们必须将程序指针传递给线程的构造函数。我们的程序中会描述这个:
ThreadStart ts = new ThreadStart(simpleThread.SimpleMethod);
需要注意这里的方法名是不加括号的;它只是简单的使用方法名。当我们创建完ThreadStart委托,我们接下来可以创建我们的线程来执行代码。Thread唯一的构造函数将ThreadStart委托的实例作为参数。我们来看看代码中是如何表示的:
Thread t = new Thread(ts);
我们定义了一个变量t作为新线程的名字。线程类的构造函数将ThreadStart委托作为它唯一的参数。
下一行代码是Thread对象的Start()方法。通过调用我们传给构造函数的ThreadStart委托,我们开始一个新执行线程。最后通过Console.ReadLine()来等待我们的键盘输入:
t.Start();
Console.ReadLine();
额,我们已经创建了一个线程,但是仅仅这些还不能帮助我们洞悉线程的力量。事实上除了显示不同的线程ID我们还没有做别的。为了在一个更加真实的应用场景中看一下如何使用同样的线程代码,我们将创建另外一个模拟后台有一个长时间运行线程同时前台有另外一个线程的程序。在一个新文件do_something_thread.cs中创建一个新的控制台应用程序:
/************************************* /* Copyright (c) 2012 Daniel Dong * * Author:Daniel Dong * Blog: www.cnblogs.com/danielWise * Email: guofoo@163.com * */ using System; using System.Collections.Generic; using System.Text; using System.Threading; namespace SimpleThread { public class DoSomethingThread { static void WorkerMethod() { for (int i = 1; i < 1000; i++) { Console.WriteLine("Worker Thread: " + i.ToString()); } } static void Main() { ThreadStart ts = new ThreadStart(WorkerMethod); Thread t = new Thread(ts); t.Start(); for (int i = 1; i < 1000; i++) { Console.WriteLine("Primary Thread: " + i.ToString()); } Console.ReadLine(); } } }
输出结果每时每刻都不同。线程执行在循环过程中会切换。最后的结果看起来会是这样:
由于上面的这段代码并没有介绍新技术所以咱们就略过。然而,需要注意在两个线程在分享执行时间。任意一个线程都会被阻塞直到另外一个执行完。每个线程都被分配一个很短的时间来执行。在一个线程用完自己的执行时间后,下一个线程会在自己的时间片内开始执行。两个线程互相交替直到执行结束。事实上,不是只有我们的两个线程在交替并共享时间片。我们不是仅在我们的程序中交换时间片。现实是,我们与当前计算机中运行的很多线程共享执行时间。
ThreadStart 和 执行分支
我们再看一下之前提到的ThreadStart委托。我们可以使用这些委托做一些有意思的事情。举个现实世界中的例子。假设你想在一个用户运行某个程序时做一些后台的例行任务。不同的角色运行同样程序会在后台执行不同的例行任务。例如,假设一个管理员运行了一个程序,你想在后台运行一个线程来收集报告数据并对数据进行格式化处理(做一个报表)。后台线程会在报表准备好的时候通知管理员。你可能不想对普通用户也提供类似管理员的这种报表服务。这就是ThreadStart的面向对象功能有用的地方。
现在看一些简单的例子。我们不会准确地模拟上面描述的场景,但是我们将演示如何依据ThreadStart定义的特定标准处理不同情况。创建一个应用程序以及一个新文件ThreadStartBranching.cs, 然后把下面代码复制到新文件中:
/************************************* /* Copyright (c) 2012 Daniel Dong * * Author:Daniel Dong * Blog: www.cnblogs.com/danielWise * Email: guofoo@163.com * */ using System; using System.Collections.Generic; using System.Text; using System.Threading; namespace SimpleThread { public class ThreadStartBranching { enum UserClass { ClassAdmin, ClassUser, } static void AdminMethod() { Console.WriteLine("Admin Method"); } static void UserMethod() { Console.WriteLine("User Method"); } static void ExecuteFor(UserClass uc) { ThreadStart ts; ThreadStart tsAdmin = new ThreadStart(AdminMethod); ThreadStart tsUser = new ThreadStart(UserMethod); if (uc == UserClass.ClassAdmin) { ts = tsAdmin; } else { ts = tsUser; } Thread t = new Thread(ts); t.Start(); } static void Main() { //execute in the context of an admin user ExecuteFor(UserClass.ClassAdmin); //execute in the context of a regular user ExecuteFor(UserClass.ClassUser); Console.ReadLine(); } } }
代码的输出结果:
我们将描述从上面代码中得到的一些重点。首先,你将注意到我们创建了可能执行程序的用户集合:
enum UserClass { ClassAdmin, ClassUser, }
接下来我们创建了两个方法:AdminMethod() 和 UserMethod(). 这两个方法会执行一系列指令并依据类型的不同而得到不同结果。我们这里只想确定它们已经运行了所以就简单地把结果输出到控制台:
static void AdminMethod() { Console.WriteLine("Admin Method"); } static void UserMethod() { Console.WriteLine("User Method"); }
下一个你要注意的是在Execute()方法内我们声明了一个叫做ts的变量作为一个ThreadStart类,但是没有使用New关键字创建一个新实例。我们然后创建了两个指向上面创建的不同方法的ThreadStart对象:
ThreadStart ts; ThreadStart tsAdmin = new ThreadStart(AdminMethod); ThreadStart tsUser = new ThreadStart(UserMethod);
所以,现在我们有了两个ThreadStart对象和一个可以存储ThreadStart实例的变量。然后我们使用If语句来让我们的代码形成分支并根据我们的商业逻辑为空的变量设置ThreadStart实例:
if (uc == UserClass.ClassAdmin) { ts = tsAdmin; } else { ts = tsUser; }
最后,我们向我们的线程构造函数传递动态赋值的ThreadStart委托来创建一个线程,然后开始执行:
Thread t = new Thread(ts); t.Start();
线程属性和方法
我们在本章的开头曾说过线程类有很多属性和方法。我们承诺过使用System.Threading命名空间来控制线程执行会变得更加简单。到目前为止,我们所做的就是创建线程然后启动它们。
让我们再看一两个线程类的成员;Sleep()方法和IsAlive属性。我们在第一章曾说过一个线程可能会在一段时间内进入睡眠状态直到发生了时钟中断。让一个线程进入睡眠状态和调用静态Sleep()方法一样简单。我们也说过我们可以确定一个线程的状态。在下面的例子中我们将使用IsAlive属性来确定一个线程是否已经执行完,使用Sleep()方法来暂停一个线程的执行。在下面的这段代码中我们将演示如何使用这两个成员:
/************************************* /* Copyright (c) 2012 Daniel Dong * * Author:Daniel Dong * Blog: www.cnblogs.com/danielWise * Email: guofoo@163.com * */ using System; using System.Collections.Generic; using System.Text; using System.Threading; namespace SimpleThread { public class ThreadState { static void WorkerFunction() { string ThreadState; for (int i = 1; i < 50000; i++) { if (i % 5000 == 0) { ThreadState = Thread.CurrentThread.ThreadState.ToString(); Console.WriteLine("Worker: " + ThreadState); } } Console.WriteLine("Worker Function Complete."); } static void Main() { string ThreadState; Thread t = new Thread(new ThreadStart(WorkerFunction)); t.Start(); while (t.IsAlive) { Console.WriteLine("Still waiting, I'm going back to sleep."); Thread.Sleep(200); } ThreadState = t.ThreadState.ToString(); Console.WriteLine("He's finally done! Thread state is: " + ThreadState); Console.ReadLine(); } } }
你的输出结果应该和下面显示的类似(喜欢动手的可以自己将for 循环次数改一下,然后看看不同结果):
让我们看一下我们首先使用新概念的Main()方法,我们创建了一个线程并将我们想让委托执行的函数传递过去:
Thread t = new Thread(new ThreadStart(WorkerFunction)); t.Start();
注意不是创建一个变量来存储我们的ThreadStart类,而是创建一个临时变量并把它作为我们线程构造函数的参数。通常来说,由于处理器会进行线程切换所以我们的Main()方法会与新线程一起执行。然后我们使用新创建线程的IsAlive属性来看它是否在执行。并将持续检测这个变量。当工作线程活动时,主线程将持续睡眠200毫秒,然后醒来并再次测试工作线程是否是活动的:
while (t.IsAlive) { Console.WriteLine("Still waiting, I'm going back to sleep."); Thread.Sleep(200); }
下面我们想看看代码中使用了两次的ThreadState属性。ThreadState实际上是一个返回枚举类型的属性。枚举值会告诉你线程状态。我们可以使用上一个例子中的if语句来判断这个属性也可以将它转换成字符串格式并输出到控制台:
ThreadState = t.ThreadState.ToString(); Console.WriteLine("He's finally done! Thread state is: " + ThreadState); Console.ReadLine();
剩下是一些不需要看的标准代码。要注意一些重要内容。首先是我们告诉一个线程睡眠一段时间以便于把执行时间放弃给其他线程。使用Thread对象的Sleep()方法-将我们想要线程睡眠的时间值传递进去。其次,我们可以使用IsAlive属性来判断线程是否执行完。最后,我们可以使用线程实例的ThreadState属性来确定他们的精确状态。
线程优先级
线程优先级确定每个线程相对其他线程的优先级。ThreadPriority枚举定义了线程优先级的值。总共有以下几个:
1. Highest
2. AboveNormal
3. Normal
4. BelowNormal
5. Lowest
当运行时创建一个线程且没有给它设置任何优先级时,线程会采用默认的Normal 优先级。然而,这不能通过ThreadPriority枚举改变。在看一下有关线程优先级的例子之前,我们来看看什么是线程优先级。我们创建一个简单的线程例子来显示线程的名字,状态以及优先级信息,thread_priority.cs:
/************************************* /* Copyright (c) 2012 Daniel Dong * * Author:Daniel Dong * Blog: www.cnblogs.com/danielWise * Email: guofoo@163.com * */ using System; using System.Collections.Generic; using System.Text; using System.Threading; namespace SimpleThread { public class ThreadPriority { public static Thread worker; static void Main() { Console.WriteLine("Entering void Main()"); worker = new Thread(new ThreadStart(FindPriority)); //Let's give a name to the thread. worker.Name = "FindPriority() Thread"; worker.Start(); Console.WriteLine("Exiting void Main()"); Console.ReadLine(); } public static void FindPriority() { Console.WriteLine("Name: " + worker.Name); Console.WriteLine("State: " + worker.ThreadState.ToString()); Console.WriteLine("Priority: " + worker.Priority.ToString()); } } }
输出结果如下:
我们知道工作线程优先级是Normal。我们加一个新线程,然后以不同优先级调用同样方法。thread_priority2.cs:
/************************************* /* Copyright (c) 2012 Daniel Dong * * Author:Daniel Dong * Blog: www.cnblogs.com/danielWise * Email: guofoo@163.com * */ using System; using System.Collections.Generic; using System.Text; using System.Threading; namespace SimpleThread { public class Thread_priority2 { public static Thread worker; public static Thread worker2; static void Main() { Console.WriteLine("Entering void Main()"); worker = new Thread(new ThreadStart(FindPriority)); worker2 = new Thread(new ThreadStart(FindPriority2)); //Let's give a name to the thread worker.Name = "FindPriority() Thread"; worker2.Name = "FindPriority() Thread 2"; //Give the new thread object the highest priority worker2.Priority = ThreadPriority.Highest; worker.Start(); worker2.Start(); Console.WriteLine("Exiting void Main()"); Console.ReadLine(); } public static void FindPriority() { Console.WriteLine("Name: " + worker.Name); Console.WriteLine("State: " + worker.ThreadState.ToString()); Console.WriteLine("Priority: " + worker.Priority.ToString()); } public static void FindPriority2() { Console.WriteLine("Name: " + worker2.Name); Console.WriteLine("State: " + worker2.ThreadState.ToString()); Console.WriteLine("Priority: " + worker2.Priority.ToString()); } } }
输出结果类似下面:
线程基于使用Priority属性设置的优先级来调度执行。每个操作系统对不同优先级的线程处理都不同且每个操作系统都可以改变线程优先级。
我们的应用程序没有办法禁止操作系统改变由程序员设置的线程优先级,这是因为操作系统是所有线程的管理者且它知道何时、如何调度它们。例如,线程优先级可以由操作系统基于以下几个因素动态修改,比如用户输入等系统事件有较高优先级而内存紧缺将触发垃圾回收机制。
下一篇我们将会介绍时钟和回调…