多线程开发技术基础
多线程开发扫盲系列第二编:多线程开发技术基础
在.NET应用程序中,线程由Thread类创建的对像代表。Thread类提供了许多属性和方法对线程进行控制
Thread类拥有4个重载的构造函数,最常用的一个可接收一个ThreadStart类型的参数:public Thread(ThreadStart start)
ThreadStart是一个委托,其定义如下:
Public delegate void ThreadStart();
从以上定义可知,在创建线程对像时必须传给它一个方法,此方法无参数且不返回从任何值。这个方法被称为”线程方法”,由于在面向对像程序中,方法本质上就是一个函数,因此人们习惯地又将”线程方法”称为”线程函数”,每个线程都对应着一个特定的线程函数,线程的执行体现着线程函数的执行
线程函数可以是静态的,也可以是实例的
Class MyThread
{
Public static void ThreadMethod1(){ ......}
Public void ThreadMethod2(){......}
}
//将静态方法当作线程函数
Thread th1=new Thread(MyThread.ThreadMethod1);
//将实例方法当作线程函数
MyThread obj=new MyThread();
Thread th2=new Thread(obj.ThreadMethod2);
当线程对像创建以后,调用它的Start方法启动线程
Th1.Start();
线程终止可以使用线程对像的Abort方法。
这时线程对像自身会引发一个ThreadAboutException异常。代码如:
namespace ThreadAbort
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("主线程开始");
MyThread mythread = new MyThread();
Thread th = new Thread(mythread.SomeLongTask);
// th.IsBackground = true;
th.Start();
Thread.Sleep(300);
Console.WriteLine("主线程调用abort方法提前终止辅助线程");
th.Abort();
Console.WriteLine("主线程结束");
Console.ReadKey();
}
}
class MyThread
{
public void SomeLongTask()
{
try
{
for (int i = 0; i < 10; i++)
{
Console.WriteLine(i);
Thread.Sleep(100);
}
}
catch (ThreadAbortException e)
{
Console.WriteLine("辅助线程被提前中断"+e.Message);
Thread.ResetAbort();
}
finally
{
Console.WriteLine("完成清理辅助线程的工作");
}
Console.WriteLine("辅助线程结束");
}
}
}
Background属性为true的线程称为"背景线程",即主线程结束时让CLR自动地强行结束所有还在运行的辅助线程
可以使用Thread.Sleep方法让线程对像休息一段特定的时间后再继续(称为线程休眠)
Thread.Sleep(3000);
Thread.Sleep方法的调用将导致线程从running状态转换为waitsleepjoin状态,这将导致一个线程上下文的切换,因而会对程序性能有一定影响。如果仅仅是希望线程能等待一定的时间,即让线程在一段时间内空转,可以调用Thread.SpinWait。
等待一个线程的完成
主线程启动一个辅助线程后,要等待它运行结束后才能继续干某些工作
线程之间的这种协作关系称为”线程同步”
有关JOIN的方法用图来分析,在线程A的执行流程中调用线程B的JOIN方法,其实是相当于把线程B的整个执行流程”嵌入”线程A的执行流程中。线程A会在调用JOIN方法的那句代码处等待线程B的工作完成。因此,在这种情况下必须保证线程B是可以在有限时间内结束的,可以想像整个程序会始处于等待状态。这种状态称为”死锁”。
在.net中每个托管线程都拥有一系列的状态,在任何时候每个线程对像一定处于一个确定的状态之中,可通过Thread类的ThreadState属性了解线程对像所处的状态
线程状态。
线程状态
值 |
说明 |
Abort |
线程处于Stopped状态中 |
AbortRequested |
已对线程调用了Thread.Abort方法,但线程尚未接收到试图终止它挂起的Threading.ThreadAbortException |
Background |
线程正作为后台线程执行,相对于前台线程而言 |
Running |
线程已启动,正在运行中 |
Stopped |
线程已停止 |
StopRequested |
正在请求线程停止,仅在.Net Framework内部使用 |
Suspended |
线程已挂起 |
SuspendRequested |
正在请求线程挂起 |
Unstarted |
尚未对线程调用Thread.Start方法 |
WaitSleepJoin |
由于调用Montor.Enter申请锁,Sleep或Join线程已被阻止 |
线程优先级
每个线程还关联着一个线程优先级,由Thread.Priority属性标识
.NET Framwork提供了五种线程优先级,从高到低依次为
Highest—>AboveNormal->Normal->BelowNormal->Lowest
新创建的线程对像具体优先级”Normal”,但可以将它的Priority属性更改为上述的任何一个值。
操作系统线程调度策略
Windows操作系统按照时间片来将CPU分配给各个线程使用,由于可能有多个线程正处于Running状态,Windows就按照线程优先级将等待分配的CPU线程分成几个队列,根据特定的线程调度算法从队列中选一个线程运行。当高优先级队列中还有线程等待分配CPU时,低优先级的线程就只能等待了。如果高优先级的线程都已执行完毕,则线程调度程序为低优先级线程队列中的线程分配CPU运行。如果某一线程正在CPU执行,这时windows发现一个高优先级的线程进入就绪队列等待运行,则在当前这个低优先级的线程执行完一个时间片后,操作系统会进行一次线程调度,以让这个新到的高优先级线程有机会获取CPU运行。
一个线程优先级不直接影响该线程的状态,只影响windows选中它占用 CPU运行的时间的概率。
使用外壳方法
线程类Thread接收一个ThreadStart委托类型的参数,而ThreadStart不能有返回值,也没有参数。这就大大限制了线程函数的选择,因此不得不手动增加一个符合ThreadStart委托要求的"外壳"方法,示例如:
class Program
{
static void Main(string[] args)
{
MyThread2 obj = new MyThread2 { x = 10, y = 20 };
Thread th = new Thread(obj.ThreadMethod);
th.Start();
th.Join();
Console.WriteLine("结果为:" + obj.returnValue.ToString());
Console.ReadKey();
}
}
class MyThread2
{
public int x;
public int y;
public long returnValue;
public void ThreadMethod()
{
returnValue = SomeFunc(x, y);
}
long SomeFunc(int x, int y)
{
long ret = 0;
return ret = x + y;
}
}
使用带参数的ParameterIzedThreadStart
.NET2.0给Thread类加入一个重载形式
public Thread(ParameterizedThreadStart start);
ParameterizedThreadStart委托可以接收含有一个参数且无返回值的线程函数,由于其参数类型为Object,所以此委托可接收任何类型的方法参数。示例如:
class MyThread
{
public void Method1(object x){......}
public void Method2(object str){......}
}
Thread th1=new Thread(new ParameterizedThreadStart(Method1));
Thread th2=new Thread(new ParameterizedThreadStart(Method2));
th1.Start(100);
th2.Start("Hello");
使用ParameterIzedThreadStart委托,可以避免将线程函数的参数外化为类的字段,但还是有限制,即纯种函数只能有一个参数,且不能有返回值
设计线程信息输入输出辅助类
见下方示例代码,代码一看都明白了。
namespace UseArray
{
class Program
{
static void Main(string[] args)
{
ThreadMethodHelper argu = new ThreadMethodHelper();
argu.arr = new int[] { -1, 9, 100, 78, 20, 56 };
Thread th = new Thread(DoWithArray);
th.Start(argu);
th.Join();
Console.WriteLine("数组元素清单");
for (int i = 0; i < argu.arr.Length; i++)
{
Console.WriteLine(i + " ");
}
Console.ReadKey();
}
static void DoWithArray(object obj)
{
ThreadMethodHelper argu = obj as ThreadMethodHelper;
for (int i = 0; i < argu.arr.Length; i++)
{
if (argu.arr[i] > argu.MaxVal)
argu.MaxVal = argu.arr[i];
if (argu.arr[i] < argu.MinVal)
argu.MinVal = argu.arr[i];
argu.Sum += argu.arr[i];
}
argu.Average = argu.Sum / argu.arr.Length;
}
}
class ThreadMethodHelper
{
public int[] arr;
public int MaxVal=0;
public int MinVal=0;
public long Sum=0;
public double Average = 0;
}
}
在多线程程序运行中,由用户取消操作是一种非常常见的场景,比如用户使用windows资源管理器在当前文件夹中搜索文件时,可以通过点击其它文件夹而取消搜索。
中途停止一个线程的执行,通常用Thread.Abort方法,但这种方式会造成程序涉及的数据完整性受到破坏,线程所占用的一些系统资源(比如文件句柄等)也可能无法完成。比较合理的方式是外界提出"取消操作"的请求,然后由线程自身来决定如何处理这一请求。
在设计多线程程序时,可设置一个用于接收外部取消消息的属性,然后在线程函数中分阶段地检测这一属性,每个阶段的检查点由软件开发者确定,并且决定线程如何优雅退出。