Fork me on GitHub

前线解释多线程《二》

接着前一篇博文的内容我们开始学习线程的同步和异步相关的内容,很多自学的新手同学可能精力的回避这个问题,其实很简单的,下面先给那些不理解这个概念的同学讲两个关于某人的故事,听完了,你就明白实战出真理的道理了(如果新手从本文中略有所获就支持一下同样是新手的偶,给我个信息知道我没耽误你的时间,当然大家可以给我点建设性的意见和指导)。

什么是线程同步?

    从前某人混社会的时候,某人第一次去江湖厮杀,结果马上就遇到了地头蛇,某人靠被砍的很惨,连掏手机打个电话的机会都不给我,这时候有两条路给某人选择,要吗掏手机命没了,要吗继续扛着结果手机没法掏出来,于是某人发挥了珍惜生命的优良传统,某人就硬是扛到最后。你现在明白我在干什么了,呵呵,这就是同步,就是说你在干一件事的时候不能去同时做别的事情,否则就会发生意外(那就是你把命掉了)。这就是线程同步的意义,假设多个线程同时访问一个资源,.NET的定义的线程优先级还要看操作系统的心情,人家爱理不理,此外就是你应该有这么一个概念:Win32线程调度程序和CLR允许线程可以自由的跨越应用程序域的边界,但是任何一个时间点上,任何应用程序域上都可以有多个线程,但是一个线程只能在一个特定的应用程序域内,也就是说一个线程在任何时刻在多个应用程序域内是不可能同时执行的。很经典的例子就是那个啥,假设你不用多线程你使用Winfrom在其中运行一个超大循环的时候,(我发挥我高超的美术功底画了个了画演示一下)(这个程序虽然简单但是涉及一个跨线程访问的问题,如果有时间就到后面的博文拿出来分析一下)。

 

  你再去拖动程序窗体,结果你发现你力气没window力气大,而且还吃了没文化的亏,因为这个循环的执行和你这个托这个动作都是出于同一个线程(主线程中),同时干这两件事,如果你乱来,就算拖动了,他也就崩溃了,哈哈,现在明白了吧,这就是同步。
现在我们解释什么是异步!
    经过第一次的教训出去的时候某人带了给小弟,结果还是更背,这次遇到了古惑仔,但是某人在招架的时候给小弟用嘴说你快打电话给老子搬救兵,最少三卡车,结果,某人没有自己掏电话,某人只是用特定的信息告诉小弟,某人继续干自己的事,搬救兵的事小弟就替我办了,结果这次某人没有吃亏,还是某人在小学学好了统筹学,同样的时间,办了几件事,还是小弟听话,某人自豪的笑了。多说无益,怕兄弟们烦,这就是异步操作,ajax的异步机制就是这样了,可以实现网页的局部更新等等......就不扯到asp.net上了,现在你应该明白了什么是异步了吧。

 


  我在这系列博文中不打算提到线程的优先级以及线程状态,这些大家就自己看吧,毕竟是次要的内容,瞄一眼就懂了。


.NET提供了两种方法可以实现同步,就是简单方法和高级方法,这就废话了,哈哈,对了据说.NET1.0还支持跨进程访问线程的,后还就给禁止掉了,不过现在还是可以通过取消跨进程访问的某个属性可以解除这种不安全的访问手段,靠又扯远了。

继续,简单方法就是轮询和等待,高级方法就是使用同步对象。

什么是轮询?

  轮询其实效率低的都没人去用它,它的原理就是循环的去侦听线程的状态,而且就算侦听到了都有可能误判,假设砸门使用IsAlive来检测某个线程是否退出的时候我们要有这么个概念处于活动状态的线程不一定是运行的,就是说他亦可能出于休眠状态。

什么又是等待?

  所谓的等待,懂汉语的孩子都知道就是等着某个对象办事情了,这个我在前面的一篇博文用到的很多,那就是Join,同样,好多小弟弟们搞不明白Join到底是啥,好多文章加了那么个调用线程#¥%……的概念就把孩子们搞晕了,看群里搞晕的小弟弟还是蛮多的,其实你管那么多干嘛,试验下不就知道了,下面由于我在前面的博文中已经大量使用Join了,这里就不演示了,最要给个通俗的解释就是那个线程执行了Join方法,那么其他的线程都必须等待到该线程执行完为止才会有反应,但是我在这里再补充一下,就是这个“执行完”也是相对的,就是在该线程的执行过程中万一有调用了Sleep()方法休眠了一下,那么别的线程管你还有没有执行完,CPU就会把时间片分给别的线程去执行他们的任务,直到这个线程再次唤醒为止。当然既然是简单问题肯定只能解决简单的逻辑,要是流程一复杂,你就找比尔盖兹的工程师给你用Join给你解决线程同步的问题吧^@^!具体的实例可以看上一篇文章。

演示并发

  说了这么久,我们干脆先演示下并发的程序,来分析下并发产生的问题,在进入下文介绍解决并发的高级方法。

先在有这么一点代码,先贴出来咱们在分析。

 

 1 using System;
 2 using System.Threading;
 3 public class Printer
 4 {
 5     public void PrintNumbers() 
 6             { 
 7          
 8               for (int i = 0; i < 10; i++) 
 9               { 
10                 Random r = new Random(); 
11                 Thread.Sleep(500 * r.Next(3)); 
12                 Console.Write("{0}, ", i); 
13               } 
14               Console.WriteLine(); 
15             }
16     class Program
17     {
18         static void Main(string[] args)
19         {
20             Console.WriteLine("*****线程同步 *****\n");
21 
22             Printer p = new Printer();
23 
24             Thread[] threads = new Thread[10];
25             for (int i = 0; i < 10; i++)
26             {
27                 threads[i] =new Thread(new ThreadStart(p.PrintNumbers));
28                 threads[i].Name = string.Format("工作线程 thread #{0}启动执行!", i);
29                 Console.WriteLine(threads[i].Name);
30             }
31             foreach (Thread t in threads)
32                 t.Start();
33             Console.ReadLine();
34         }
35     }
36 }

 
乍一看,咋们肯定知道要是按正常的运行,肯定是每次循环输出10个数字,但是结果并非我们一厢情愿,我们运行看一下:(注:由于使用随机函数这个结果只是其中一种)

结果我们发现结果不是我们预料大的,毛呀,这是肿么了,有木有搞错啊,为什么会这样?

threads[i] =new Thread(new ThreadStart(p.PrintNumbers));

  我们来看这一句代码,我们发现每一个线程都是调用同一个对象p的PrintNumbers方法,或许这只是个线索而已,
接着我们再看这句:
Thread.Sleep(500 * r.Next(3));
我们就会发现,这里随机挂起线程的时间不能确定 ,可能的情况就是当即将发生printNumbers方法的时候,还没等输出
到控制台,当前的线程就被挂起了,win32的线程调度程序就切换线程,于是就发生了我们不可预料的结果。
怎么解决了,哈哈是不是想用上面的上面的Join一下啊,完全可以有什么不可以的,咋们试试先,不过我们要引入高级方法,
又要用低级方法验证一下,可不可以,我们的思路是什么呢:就是要线程调度程序等到哥执行完了当前线程再去执行下一个线
程,稍加修改代码我们测试一下,
我们只在这里动一下手脚:
 foreach (Thread t in threads)
            {
                t.Start();
                t.Join();
            }

但是结果就立马不同了,因为我们都知道每个线程都执行完成了,结果如你所愿:

 


那么怎么用高级方法解决这个问题呢?

我们为了节约时间先做一个最简单lock方法,这个其实是为了方便Monitor类的使用应用而生(完全等价于Monitor类的调用形式)。具体的后期再解释,咋们先只要知道高级方法解决这些问题,还是最优的选择就行了,先有个大概影响,后续博文慢慢研究。不可能一次都写完,我还要上课,还要去看电影,呵呵....

  我们先解释下lock关键字,这个关键字允许定义一段线程定义的代码语句,后进入的线程不会中断当前的线程,而是如同实现Join类似的功能,停止自身的线程执行。lock需要指定一个标记(即一个对象的引用),你不指定,你锁了人家大门咋办,这个要注意O(∩_∩)O哈!当线程进入锁定范围的时候就需要获得这个标记,知道你家大门被锁了,进不去了,那就等等呗。

    当然,如果我们去锁定一个实例对象的私有方法的时候,这个方法只有你这个对象可以访问,那么这个对象的引用(也就是标记)使用方法本身的对象引用就OK了,简单点就是this.锁定的就是你自家的门啦。。。

 

1  private void SomePrivateMethod() 
2           { 
3           //使用当前对象为锁定标记
4             lock(this) 
5             { 
6                //这个语句块(范围)中的代码是线程安全的 
7              } 
8           } 

 

              问题是我们有个锁子不一定所的都是自家的大门,指不定你就是个看大门的,哈哈。这样问题就来了,如果锁定公共成员中的一段代码,.NET推荐的方式就是使用Object成原来作为锁标记,所谓的标记你别看的那么神圣,那就是个ID而已,就是用来唯一识别的而已,不要深究这个问题。

 1 private Object myLock=new Object();   
 2 public void PrintNumbers() 
 3    { 
 4      //使用Object成员作为锁标记
 5      lock ( myLock) 
 6      { 
 7         ... 
 8      } 
 9    } 
10 } 


  好了,当我们了解完这些我们就开始解决上面的那个问题,我们分析,每次循环的线程挂起和控制台输出的部分有可能出问题,OK,那我们把它锁起来,于是我们这样做了:

 1 public void PrintNumbers()
 2     {
 3         lock (myLock)
 4         {
 5             for (int i = 0; i < 10; i++)
 6             {
 7                 Random r = new Random();
 8                 Thread.Sleep(500 * r.Next(3));
 9                 Console.Write("{0}, ", i);
10             }
11             Console.WriteLine();
12         }
13     }

 

我们运行看下效果:

事实证明我们成功的掌握了基本的解决多线程并发的一点点知识

本篇博客结束语:

多线程虽然可以发挥我们多核电脑的优势,即便是俺曾经用过的那台02年单核的邵阳笔记本也是支持超线程的,但是不是任何时候都可以用多线程,就好比再贵的法拉利你在我们这里的山村里你也飙不起来,但是要是让我开着拖拉机上了高速公路跑是可以跑,就是都遭人围观,于是有生之年哥决定要开个悍马,高速山村都能爽爽的跑上那么一会!!开玩笑,意思就是,不一定多核了你访问的资源就多了,看情况决定使用多线程这才是正确的决定。

如果我在写博客的过程中帮到了你,就支持自学的孩子们包括我一下!

posted @ 2012-05-30 07:51  Halower  阅读(2135)  评论(17编辑  收藏  举报