C#笔记整理之线程(一)

原文地址: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方法不同的是,被挂起的线程可以唤醒

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值