原文地址:https://www.cnblogs.com/JimmyZheng/archive/2012/06/10/2543143.html
线程基础概念:
1.线程有时被称为轻量级进程,是程序执行流得最小 单位
2.线程是由线程ID,当前指令指针(PC),寄存器集合和堆栈组成。
3.线程自身不能拥有系统资源,但是可以使用线程所属进程所占有的系统资源
4.线程可以创建和撤销另一个线程
5.线程可以拥有自身的状态,例如 运行状态,挂起状态,销毁释放状态等等
6.线程具有优先级,每个线程都分配了0-31级别的其中一个优先级,数字越大,优先级越高,然而手动分配优先级过于复杂,所以微软为我们的Thread类提供一个优先级的枚举,ThreadPriority枚举便是优先级枚举,我们可以利用thread.Priority属性来进行设置
7.线程开销
线程优点:
概念:一个程序或者进程中同时运行多个线程完成不同的工作,优点如下:
1.能够实现并行操作,也就是说多个线程可以同时进行工作
2.利用多线程后许多复杂的业务或者是计算可以交给后台线程去完成,从而提高整体程序的性能
3.类似于第一条利用多线程可以达到异步的作用(注意,实现异步的一种方式是多线程)
线程同步和线程异步
1.线程同步:同步方法调用再程序继续执行之前,需要等待同步方法执行完毕返回结果。
注意:很有可能多个线程都会对一个资源进行访问,从而导致资源被破坏,所以必须采用线程的同步机制,例如为共享资源加锁,当其中一个线程占用了锁之后,其余线程均不能使用共享资源,只有等其释放锁之后,接下来的其中一个线程会占有该锁。
2.线程异步:指的是一个调用请求发给被调用者,而调用者不用等待其结果的返回,一般异步执行的任务都需要比较长的时间,所以为了不影响主线程的工作,可以使用多线程或新开辟一个线程来实现异步,同样,异步和线程池也有着非常紧密的联系。
前台线程与后台线程
前台线程:诸如Console程序的主线程,wpf或者sliverlight的界面线程,都是属于前台线程,一旦前台线程崩溃或者终止,相应的后台线程都会终止。必须注意的是,一旦前台线程全部运行完毕,应用程序的进程也会释放,但是假设Console程序中main函数运行完毕,但是其中几个前台线程还处于运行之中,那么这个Console程序的进程是不会释放的,仍然处于运行之中,直到所有的前台线程都释放为止。
后台线程:和前台线程唯一的区别是,后台线程默默无闻,甚至后台线程因某种情况,释放销毁时不会影响到进程,也就是说后台线程释放时不会导致进程的释放。
Thread构造函数
Thread类的构造函数提供了以下版本
ThreadStart和ParameterThreadStart参数都是委托,所以可以看出委托其实就是方法的抽象,前者用于不带参数的并且无返回值的方法的抽象,后者是带object参数的方法的抽象
using System.Threading;
public class ThreadTest
{
Thread thread = new Thread(new ThreadStart(ThreadMethod));
Thread thread2 = new Thread(new ParameterizedThreadStart(ThreadMethodWithPara));
public ThreadStartTest()
{
thread.Start();
thread.Start(new Parameter{ paraName = "Test"});
}
static void ThreadMethod()
{
}
static void ThreadMethodWithPara(object o)
{
}
}
public class Parameter
{
public string paraName {get;set;}
}
对于带参数的方法,当启动线程时,参数通过thread.Start方法传入。
Thread的Sleep方法
假设有源源不断的蛋糕(源源不断的时间),一副刀叉(一个CPU),10个等待吃蛋糕的人(10 个进程)。如果是 Unix 操作系统来负责分蛋糕,
那么他会这样定规矩:每个人上来吃 1 分钟,时间到了换下一个。最后一个人吃完了就再从头开始。于是,不管这10个人是不是优先级不同、饥饿
程度不同、饭量不同,每个人上来的时候都可以吃 1 分钟。当然,如果有人本来不太饿,或者饭量小,吃了30秒钟之后就吃饱了,那么他可以跟操
作系统说:我已经吃饱了(挂起)。于是操作系统就会让下一个人接 着来。如果是 Windows 操作系统来负责分蛋糕的,那么场面就很有意思了。
他会这样定规矩:我会根据你们的优先级、饥饿程度去给你们每个人计算一个优先级。优先级最高的那个人,可 以上来吃蛋糕——吃到你不想吃为止。
等这个人吃完了,我再重新根据优先级、饥饿程度来计算每个人的优先级,然后再分给优先级最高的那个人。这样看来,这个 场面就有意思了——
可能有些人是PPMM,因此具有高优先级,于是她就可以经常来吃蛋糕。可能另外一个人的优先级特别低,于是好半天了才轮到他一次(因为 随着时间
的推移,他会越来越饥饿,因此算出来的总优先级就会越来越高,因此总有一天会轮到他的)。而且,如果一不小心让一个大胖子得到了刀叉,因为他
饭量 大,可能他会霸占着蛋糕连续吃很久很久,导致旁边的人在那里咽口水。。。而且,还可能会有这种情况出现:操作系统现在计算出来的结果,是
5号PPMM总优 先级最高——高出别人一大截。因此就叫5号来吃蛋糕。5号吃了一小会儿,觉得没那么饿了,于是说“我不吃了”(挂起)。因此操作系
统就会重新计算所有人的 优先级。因为5号刚刚吃过,因此她的饥饿程度变小了,于是总优先级变小了;而其他人因为多等了一会儿,饥饿程度都变大了,
所以总优先级也变大了。不过这时 候仍然有可能5号的优先级比别的都高,只不过现在只比其他的高一点点——但她仍然是总优先级最高的啊。因此操作
系统就会说:5号mm上来吃蛋糕……(5号 mm心里郁闷,这不刚吃过嘛……人家要减肥……谁叫你长那么漂亮,获得了那么高的优先级)。那么,
Thread.Sleep 函数是干吗的呢?还用刚才的分蛋糕的场景来描述。上面的场景里面,5号MM在吃了一次蛋糕之后,觉得已经有8分饱了,她觉得在未来
的半个小时之内都不想再 来吃蛋糕了,那么她就会跟操作系统说:在未来的半个小时之内不要再叫我上来吃蛋糕了。这样,操作系统在随后的半个小时
里面重新计算所有人总优先级的时候, 就会忽略5号mm。Sleep函数就是干这事的,他告诉操作系统“在未来的多少毫秒内我不参与CPU竞争”。
Thread的join方法
在Console程序中,主线程自上而下运行着main函数,假如在main函数中新增一个线程thread对象的话,也就是说,在主线程中再开启一个子线程,这时子线程和主线程可以同时工作(前提是子线程使用Start方法),同理,假如在这个子线程中再开辟一个属于这个子线程的子线程,这三个爷爷,父亲,儿子线程可以使用Start()方法一起工作,假如再主线程中添加2个thread对象并开启,那么这2线程便属于同一层次的线程(兄弟线程)(和优先级无关,只同一个位置层次上的兄弟)
public static void ShowFatherAndSonThread(Thread grandFatherThread)
{
Console.WriteLine("爷爷主线程名:{0}", grandFatherThread.Name);
Thread brotherThread = new Thread(new ThreadStart(() => { Console.WriteLine("兄弟线程名:{0}", Thread.CurrentThread.Name); }));
Thread fatherThread = new Thread(new ThreadStart(
() =>
{
Console.WriteLine("父亲线程名:{0}", Thread.CurrentThread.Name);
Thread sonThread = new Thread(new ThreadStart(() =>
{
Console.WriteLine("儿子线程名:{0}", Thread.CurrentThread.Name);
}));
sonThread.Name = "SonThread";
sonThread.Start();
}
));
fatherThread.Name = "FatherThread";
brotherThread.Name="BrotherThread";
fatherThread.Start();
brotherThread.Start();
}
Join方法:继续执行标准的COM和SendMessage消息泵处理期间,阻塞调用线程,直到某个线程终止为止。就是说,假如在主线程(爷爷辈)的调用了父亲线程,父亲线程调用了儿子线程,假设现在必须开启爷爷辈和父亲辈的线程,但是爷爷线程必须等待父亲线程结束后再进行。这个时候Join方法就可以用了,我们的目标是阻塞爷爷线程,那就只需要让父亲线程(thread)对象去调用join方法就行。
public static void ThreadJoin()
{
Console.WriteLine("我是爷爷辈线程,子线程马上要来工作了我得准备下让个位给他。");
Thread t1 = new Thread(
new ThreadStart
(
() =>
{
for (int i = 0; i < 10; i++)
{
if (i == 0)
Console.WriteLine("我是父亲线层{0}, 完成计数任务后我会把工作权交换给主线程", Thread.CurrentThread.Name);
else
{
Console.WriteLine("我是父亲线层{0}, 计数值:{1}", Thread.CurrentThread.Name, i);
}
Thread.Sleep(1000);
}
}
)
);
t1.Name = "线程1";
t1.Start();
//调用join后调用线程被阻塞
t1.Join();
Console.WriteLine("终于轮到爷爷辈主线程干活了");
}
代码中当父亲线程启动后会立即进入Join方法,这时候调用该线程爷爷辈被阻塞,知道父亲线程中的方法执行完毕为止,最后父亲线程将控制权再次还给爷爷辈线程。
那么兄弟线程如何保证先后顺序,很明显如果不使用join,一并开启兄弟线程后结果是随机的不可预测的(暂时不考虑线程优先级),但是我们不能再兄弟线程全都开启后使用join,这样阻塞了父亲线程,而对兄弟线程是无效的。
public static void ThreadJoin2()
{
IList<Thread> threads = new List<Thread>();
for (int i = 0; i < 3; i++)
{
Thread t = new Thread(
new ThreadStart(
() =>
{
for (int j = 0; j < 10; j++)
{
if (j == 0)
Console.WriteLine("我是线层{0}, 完成计数任务后我会把工作权交换给其他线程", Thread.CurrentThread.Name);
else
{
Console.WriteLine("我是线层{0}, 计数值:{1}", Thread.CurrentThread.Name, j);
}
Thread.Sleep(1000);
}
}));
t.Name = "线程" + i;
//将线程加入集合
threads.Add(t);
}
foreach (var thread in threads)
{
thread.Start();
//每次按次序阻塞调用次方法的线程
thread.Join();
}
}
上面的操作存在缺陷:
1.必须要指定顺序
2.一旦一个运行了很久,后续的线程会一直等待很久
3.很容易产生死锁
Thread的Abort和Interrupt方法
Abort方法:释放并终止调用线程,其实当一个线程调用Abort方法时,会再调用此方法的线程上引发一个异常:ThreadAbortException
1.尝试对主线程终止释放
static void Main(string[] args)
{
try
{
Thread.CurrentThread.Abort();
}
catch
{
//Thread.ResetAbort();
Console.WriteLine("主线程接受到被释放销毁的信号");
Console.WriteLine( "主线程的状态:{0}",Thread.CurrentThread.ThreadState);
}
finally
{
Console.WriteLine("主线程最终被释放销毁");
Console.WriteLine("主线程的状态:{0}", Thread.CurrentThread.ThreadState);
Console.ReadKey();
}
}
输出:
主线程接受到被释放销毁的信号
主线程的状态:AbortRequested
主线程最终被释放销毁
主线程的状态:AbortRequested
从运行结果来看,主线程被终止时其实是报出了一个ThreadAbortException,从中我们可以捕获,但是注意的是,主线程直到finally语句块执行完毕之后才真正结束(可以仔细看下主线程的状态一直处于AbortRequested),如果再finally语句块中执行复杂逻辑,那么只有等待直到运行完毕才真正销毁主线程(也就是说主线程的状态会变成Aborted,但是由于是主线程所以无法看出)
2.尝试终止一个子线程
static void TestAbort()
{
try
{
Thread.Sleep(10000);
}
catch
{
Console.WriteLine("线程{0}接受到被释放销毁的信号",Thread.CurrentThread.Name);
Console.WriteLine("捕获到异常时线程{0}主线程的状态:{1}", Thread.CurrentThread.Name,Thread.CurrentThread.ThreadState);
}
finally
{
Console.WriteLine("进入finally语句块后线程{0}主线程的状态:{1}", Thread.CurrentThread.Name, Thread.CurrentThread.ThreadState);
}
}
Main:
static void Main(string[] args)
{
Thread thread1 = new Thread(TestAbort);
thread1.Name = "Thread1";
thread1.Start();
Thread.Sleep(1000);
thread1.Abort();
thread1.Join();
Console.WriteLine("finally语句块后,线程{0}主线程的状态:{1}", thread1.Name, thread1.ThreadState);
Console.ReadKey();
}
输出:
线程Thread1接受到被释放销毁的信号
捕获到异常时线程Thread1主线程的状态:AbortRequested
进入finally语句块后线程Thread1主线程的状态:AbortRequested
finally语句块后,线程Thread1主线程的状态:Aborted
子线程的销毁释放过程(Start->abortRequested->Aborted->Stop),这个过程跟主线程的例子一致,唯一区别是在main方法中故意让主线程阻塞,这样能看见thread1在finally语句块后的状态
3.尝试对尚未启动的线程调用Abort
如果对一个尚未启动的线程调用Abort的话,一旦该线程启动就被停止了
4.尝试对一个挂起的线程调用Abort
如果在已挂起的线程上调用Abort,则将在调用Abort的线程中引发ThreadStateException,并将AbortRequested添加到被中止的线程的ThreadState属性中。直到调用Resume后,才在挂起的线程中引发ThreadAbortException。如果在正在执行非托管代码的托管线程上调用Abort,则直到线程返回到托管代码才引发ThreadAbortException。
Interrupt方法:
Interrupt方法将当前的调用该方法的线程处于挂起状态,同样在调用此方法的线程上引发一个异常:ThreadInterruptedException,和Abort方法不同的是,被挂起的线程可以唤醒