在上一篇博客中对线程有了一个初步的概要框架,从这篇博客开始,从代码层面深入学习C#的多线程编程艺术……
线程的重点在于线程的控制,这里使用C#创建控制台程序,模拟超人和小怪兽赛跑的故事,涉及到.NET平台,线程的创建、开启和线程控制中锁的使用,算是多线程学习的一道开胃小菜吧
故事的背景:
一天,超人见到了小怪兽,说要进行一场跑步比赛,但是小怪兽(20m/s)说:“超人(50m/s)你跑的速度快,你要比我多跑一段路程”。超人想了想,我是超人,我怕谁?于是就答应道:“好吧,那我就让你200米”。就这样,两人一拍即合,于是乎,请来了一位教练。 可是教练比较懒,在比赛开始前,先宣布比赛规则:第一个跑到终点的选手,自己将名字写到终点的黑板上,然后又分别悄悄地跟超人和小怪兽叫到一边,小声的说:“等你跑完的时候通知我一声,然后我宣布比赛结束”。裁判一声枪响,两人开始冲锋,可是超人起初怎么能想到今年是2012,世界末日?!于是就在他跑到一半的时候,去拯救地球,回来后继续接着跑。小怪兽对超人全然不知,只顾低头跑步。但是在这场逐鹿汇中,究竟鹿死谁家……???
程序模拟:
分析上面的过程,我们需要创建超人、小怪兽这两个线程,来执行跑步的方法;定义一个静态的白板,用于记录第一个跑到终点的选手的姓名;裁判要等到两个线程都结束的时候才宣布比赛结束(线程的阻塞);超人跑到一半的时候拯救地球和第一个跑到终点的选手去找粉笔,然后记下自己的名字,都是模拟的线程中执行了其他的方法导致的阻塞或延时;系统中定义的白板变量是两个线程公用的数据,在程序中使用锁的方式达到异步读和同步写的目的。
1、定义超人和小怪兽的线程,并声明白板变量,并附带一把锁。
//定义静态的超人和小怪兽的线程,可以让主线程识别——定义线程 private static Thread superman; private static Thread master; //设计一个记录选手名单的白板 private static string nameboard; //定义一个专门用于多线程的锁对象,是一种互斥锁。是引用类型的,引用的是对象的地址,这样不管其他的程序拿到多少个引用,最终还是同一个实际数据;不能是值类型,因为如果是值类型的时候,就是实际数据的副本,不同的对象使用的时候,是对象数据的副本,这样修改后数据就不一致了;引用类型,不管被引用了多少次,但是实例只有一个,一旦这个实例被上锁后,实例只可以被一个人抢占。 private static object lockobj = new object();
/// <summary> /// 赛跑的方法。是每一个选手公共工作的方法,返回值是空(Thread定义的委托决定的),传入object表示赛跑的路程(封装的是int类型的参数,必须是object类型,系统已经定义好的委托的类型决定的) /// obj封装赛跑的路程,设计一个拆装箱的过程 /// </summary> /// <param name="obj"></param> private static void runnerWork( object obj ) { int length = Int32.Parse(obj.ToString()); #region 关于获取线程名的几种写法比较 //最安全的获取线程名称的写法。但在赋值号中可能会被其他线程打断,这种可能性很小,另外我们还可以配合锁,来解决这个问题 string currentname = Thread.CurrentThread.Name; ////这种写法是安全的,因为线程内部会保留线程的一个副本名称,线程与线程之间是相互隔离的,每一个线程会在内存中开辟自己的一段空间,所以,并不是线程越多越好。 //Thread curThread = Thread.CurrentThread; //string strThreadName = curThread.Name; ////这种写法是不安全的,因为两行代码之间随时可能会有其他的线程插入 //Thread curThread = Thread.CurrentThread; //string strThreadName = Thread.CurrentThread.Name; #endregion #region 给超人和小怪兽的速度定义一个数值 int speed ; if (currentname == superman.Name) { speed = 50; } //针对已知的线程使用else if,目的是为了提高线程程序的可扩展性,在多线程中尤为重要 else if (currentname == master.Name) { speed = 20; } else { speed = 1; } #endregion #region 模拟跑步的过程 Console.WriteLine("<" + currentname +">开始起跑……\n"); for (int count = speed; count <= length; count += speed) { Thread.Sleep(1000); Console.WriteLine("<" + currentname +">跑到了第"+ count.ToString()+"米\n"); #region 跑到一半的时候,超人去拯救地球 if (count == length / 2) { if (currentname == superman.Name) { Console.WriteLine("<" + currentname + ">开发现了一个地球危机,去拯救地球……"); //Thread.Sleep(20000); string waitinfo = ".."; //模拟超人线程在执行赛跑的时候,被阻塞 for (int j = 1; j < 20; j += 2) { Console.WriteLine("<" + currentname + ">拯救地球中" + waitinfo); waitinfo += ".."; Thread.Sleep(2000); } Console.WriteLine("<" + currentname + ">拯救地球归来,继续赛跑……"); } else { //什么也不做 } } else { //什么也不做 } #endregion } Console.WriteLine("<" + currentname + ">冲线!\n"); #endregion //自主方式记下自己的名字 writeName(currentname); }
/// <summary> /// 自主的方式记下选手自己的名字 /// </summary> /// <param name="name"></param> private static void writeName(string name) { if (nameboard.Length == 0) //异步读,筛选掉那些没有机会写的线程,提高效率 { lock (lockobj) { //如果发现白班是空的,那么就记下名字,否则,失意的走开 if (nameboard.Length == 0) //如果记名板是空的,那么就去写下自己的名字 //同步读 //筛选那些同时进入第一次的异步读时,名字板还是空的时候,有机会排队等锁的人,保证写入安全 { Console.WriteLine("<" + name + "去找白粉笔……"); //Thread.Sleep(1000); Thread.Sleep(9000); Console.WriteLine("<" + name + "找到了白粉笔,开始写自己的名字……"); //任何实际发生之前都有可能被各种原因阻塞,此处用找粉笔模拟延时的过程 nameboard = name; //同步写 Console.WriteLine("<" + name + ">在锁住的房间中,得意洋洋的写下了自己的名字!"); } else { Console.WriteLine("<" + name + ">在锁住的房间中很失望,失意的走开……"); } } } else { Console.WriteLine("<" + name + ">在房间外,没有机会进入房间,很失望,失意的走开……"); } }
/// <summary> /// 裁判宣布胜利者 /// </summary> private static void announceResult( ) { //裁判宣布比赛结束,等待所有子线程结束后才进行 Console.WriteLine("我是裁判,我宣布比赛结束!"); Console.WriteLine("本次比赛的优胜者为:" + nameboard); }
/// <summary> /// 裁判工作,作为赛跑的主线程 /// </summary> private static void judgeWork() { Console.WriteLine("下面要准备开始比赛!"); Console.Write("两位比赛选手分别为: "); Console.Write( superman.Name ); Console.Write(" PK "); Console.Write(master.Name); Console.Write("\n"); //驱动线程执行 Console.Write("回车后开始赛跑:"); Console.ReadLine(); Console.Beep(456, 1200); Console.Write("发令枪响,选手起跑…… \n"); int supermanlength = 500; int masterlength = 300; //向线程需要执行的方法传递参数————启动线程,这两个线程不一定是superman先启动,因为每个线程启动的时间,可能不一样,会受到运行环境还有内部的堆栈的影响 //这里只是表示申请执行,还需要判断资源是否够用等 //Console.WriteLine("回车后超人起跑!"); ////这句话可以让主线程阻塞 //Console.ReadLine(); //事实上,屏幕上可能会先输出“回车后小怪兽起跑”,因为超人线程启动需要时间,这里面的主线程在继续执行。 superman.Start(supermanlength); //————启动线程 //Console.WriteLine("回车后小怪兽起跑!"); //Console.ReadLine(); master.Start(masterlength); //————启动线程 //Thread.Sleep(16000); //主线程等待superman线程 Console.WriteLine("裁判与超人商量,等超人跑完后通知裁判!"); Console.WriteLine("裁判与小怪兽商量,等小怪兽跑完后通知裁判!"); //让主线程等待子线程全部跑完后再宣布比赛结束 //主线程等待superman线程 superman.Join(); //主线程等待master线程。这里的小怪兽线程与上面的超人的线程没有先后的关系。 master.Join(); //宣布获胜选手 announceResult(); }
/// <summary> /// 初始化线程数据:将runnerWork方法传给委托,当某个线程启动的时候,启动注入的事件 /// 线程在执行的时候,是通过委托调用的方法;通过委托,我们可以为一个线程启动多个方法 /// </summary> private static void initDate() { superman = new Thread(new ParameterizedThreadStart(runnerWork)); master = new Thread(new ParameterizedThreadStart(runnerWork)); //在一个线程中启动多个方法 //ParameterizedThreadStart pstart; //pstart = new ParameterizedThreadStart(runnerWork); //pstart+=其他方法; //master = new Thread(pstart); superman.Name = "superman"; master.Name = "小怪兽"; nameboard = ""; }
/// <summary> /// Main函数象征主线程,无特殊性,只是工作线程中的一个领跑者而已 /// </summary> /// <param name="args"></param> static void Main(string[] args) { Console.Clear(); //初始化数据,定义线程,封装函数 initDate(); //裁判开始工作,线程在这个方法的里面 judgeWork(); }
总结分析:
创建线程的方法:
private static Thread superman;
开启线程的方法:
superman.Start(supermanlength);
阻塞主线程的方法:
这里是阻塞的是裁判工作,只有当superman这个线程执行完后才可以继续执行裁判工作中这行代码后面的程序。另外,程序并不是先等superman这个线程执行完后再去等待master这个线程执行完的,这两个线程之间是互补干扰的。
superman.Join(); master.Join();
线程中锁的使用:
if (nameboard.Length == 0) //异步读,筛选掉那些没有机会写的线程,提高效率 { lock (lockobj) { //如果发现白班是空的,那么就记下名字,否则,失意的走开 if (nameboard.Length == 0) //如果记名板是空的,那么就去写下自己的名字 //同步读 //筛选那些同时进入第一次的异步读时,名字板还是空的时候,有机会排队等锁的人,保证写入安全 { ………………模拟被各种原因阻塞线程的代码 nameboard = name; //同步写 Console.WriteLine("<" + name + ">在锁住的房间中,得意洋洋的写下了自己的名字!"); } else { Console.WriteLine("<" + name + ">在锁住的房间中很失望,失意的走开……"); } } } else { Console.WriteLine("<" + name + ">在房间外,没有机会进入房间,很失望,失意的走开……"); }
看下程序运行结果:
通过上面这个小的程序,相信大家可以对线程有个初步的认识,尤其是对上篇博客中:“线程表示一种执行权限”这句话,有所理解了。
我们通过线程就可以让跑步、写名字这些方法还有黑板这些方法、变量在程序中多个内存中执行、访问。因为cpu在工作的时候是按照时间片的方式执行的,就是在一个最小的单位时间内只执行一个程序,但是在不同的时间片之间可能会执行不同的程序,这样在宏观的时间概念上,就可以cpu是在同时进行着多个工作。
如果我们的程序中有许多工作要做,就可以将工作分类,交给不同的线程来执行。比如说,一个线程专门负责,数据的输入、输出,另一个线程专门负责数据的计算,这样就可以让大工作量的计算单独执行,不用让其他的程序一直在等待。
另外一些比较相似的工作,比如有1万块砖要搬,如果让一个人搬的话,会花上很长很长的时间,可是,如果使用100个人来共同完成这项工作,那么工作效率岂不是大大的提高了吗?但是也并不是人越多约好的,试想一个可以容下100个人自由行动的道路,如果硬是塞了500个人,这样一来,不但没有提高板砖的效率,反而还需要花费精力来协调多余的400人。所以,线程的数量不是越多越好。
还有这么一个问题:如果有了多个人在搬砖的时候,可能就会考虑采用协作的方法了,如果我们事先在装车的时候就把转装到一个一个的框中,这样来板砖的人一看有装好的框,直接就可以提走了,这就是线程池的概念;另外,在砖厂还没有把框里砖装够数量的时候,负责拉砖的人,是不可以把这个框装车的,这样买砖的人会不干的,因为数量不够嘛,所以,这就涉及到了线程同步的关系。
关于线程池和线程同步的方法,会在后续的博客中陆续贴出,这里只做一个过渡。
声明:上面的程序,并非本人原创,这里只是作为一个学习的材料。