[倾情原创] 锁·二则——lock关键字详解

法律声明:本文章受到知识产权法保护,任何单位或个人若需要转载此文,必需保证文章的完整性(未经作者许可的任何删节或改动将视为侵权行为)。文章出处请务必注明CSDN以保障网站的权益,文章作者姓名请务必保留,并向bladey@tom.com发送邮件,标明文章位置及用途。转载时请将此法律声明一并转载,谢谢!

锁·二则

  者:刘铁猛
  期:2005-12-25
关键字:lock 多线程 同步

小序
锁者,lock关键字也。市面上的书虽然多,但仔细介绍这个keyword的书太少了。MSDN里有,但所给的代码非常零乱,让人不能参透其中的玄机。昨天是平安夜,今天自然就是圣诞节了,没别的什么事情,于是整理了一下思路,使用两个例子给大家讲解一下lock关键字的使用和一点线程同步的问题。

一.基础铺垫——进程与线程
         阅读提示:如果您已经了解什么是线程以及如何使用线程,只关心如何使用lock关键字,请跳过一、二、三节,直接登顶。
多线程(multi-thread)程序是lock关键字的用武之地。如果想编写多线程的程序,一般都要在程序的开头导入System.Threading这个名称空间。
        
一般初学者在看书的时候,一看到多线程一章就会跳过去,一是认为自己的小宇宙不够强、功力不够深——线程好神秘、好诡异——还是先练好天马流星拳之后再来收拾它;二是不了解什么时候要用到线程,认为自己还用不到那么高深的技术。其实呢,线程是个即简单又常用的概念。
1
.线程的载体——进程
   
要想知道什么是线程,就不得不先了解一下线程的载体——进程。
我们的程序在没有运行的时候,都是以可执行文件(*.exe文件)的形式静静地躺在硬盘里的。Windows下的可执行文件称为PE文件格式(Portable Executable File Format),这里的Portable不是指Portable Computer(便携式电脑,也就是本本)中的“便携”,而是指在所有Windows系统上都可以执行的便捷性、可移植性。可执行文件会一直那么静静地躺着,直到你用鼠标敲两下它的脑袋——这时候,Windows的程序管理功能就会根据程序的特征为程序分配一定的内存空间,并使用加载器(loader)把程序体装载入内存。不过值得注意的是,这个时候程序虽然已经被装载入了内存,但还没有执行起来。接下来Windows会寻找程序的“入口点”以开始执行它。当然,我们都已经知道——如果是命令行应用程序,那么它的入口点是main函数,如果是GUIGraphic User Interface,简言之就是带窗口交互界面一类的)应用程序,那么它的入口点将是_tWinMain函数(早先叫WinMain,参阅本人另一篇拙文《一个Win32程序的进化》)。一旦找到这个入口点函数,操作系统就要“调用”这个主函数,程序就从这里进入执行了。同时,系统会把开始执行的应用程序注册为一个“进程”(Process),欲知系统运行着多少进程,你可以按下Ctrl+Alt+Del,从Task Manager里查看(如果对Windows如何管理进程感兴趣,可以阅读与分时系统、抢先式多任务相关的文章和书籍)。
   
至此,我们可以说:如果把一个应用程序看成是一个Class的话,那么进程就是这个Class在内存中的一个“活”的实例——这是面向对象的理念。现在或许你也应该明白了为什么C#语言的Main函数要写在一个Class里,而不能像C/C++那样把main函数赤裸裸地写在外面。类是可以有多个实例的,一个程序也可以通过被双击N次在内存中运行N个副本,我们常用的Word 2003QQ等都是这样的程序。当然,也有的程序只允许在内存里有一个实例,MSN Messenger和杀毒软件就是是这样的一类。
2
.主角登场——线程
   
一个进程只做一件事情,这本无可非议,但无奈人总是贪心的。人们希望应用程序一边做着前台的程序,一边在后台默默无闻地干着其它工作。线程的出现,真可谓“将多任务进行到底”了。
   
这儿有几个实际应用的例子。比如我在用Word杜撰老板交给的命题(这是Word的主线程),我的Word就在后台为我计时,并且每10分钟为我自动保存一次,以便在发生地震之后我能快速找回十分钟之前写的稿子并继续工作——死不了还是要交的。抑或是我的Outlook,它一边看我向手头的邮件里狠命堆诸如“预算正常”“进展顺利”之类的字眼,一边不紧不慢地在后台接收别人发给我的债务单和催命会议通知……它哪里知道我是多么想到Outlook一下,透透气。诸此IEMSN MessengerQQFlashgetBTeMule……尽数是基于多线程而得以生存的软件。现在,我们应该已经意识到,基本上稍微有点用的程序就得用到多线程——特别是在网络应用程序中,使用多线程格外重要。

二.进程和线程间的关系
         我们已经在感观上对进程和线程有了初步的了解,那么它们之间有什么关系呢?前面我们已经提到一点,那就是——进程是线程的载体,每个进程至少包含一个线程。接下来,我们来看看其它的关系。
         1
.进程与进程的关系:在.NET平台下,每个应用程序都被load进入自己独立的内存空间内,这个内存空间称为Application Domain,简称为AppDomain。一个一个AppDomain就像一个一个小隔间,把进程与进程、进程与系统底层之间隔绝起来,以防某个程序突然发疯的话会殃及近邻或者造成系统崩溃。
         2
.线程与线程的关系:在同一个进程内可以存在很多线程,与进程同时启动的那个线程是主线程。非主线程不可能自己启动,一定是直接或间接由主线程启动的。线程与线程之间可以相互通信,共同使用某些资源。每个线程具有自己的优先级,优先级高的先执行,低的后执行。众线之间的关系非有趣——如果它们之间是互相独立、谁也不用顾及谁的话,那么就是“非同步状态”(Unsynchronized,比较像路上的行人;而如果线程与线程之间是相互协同协作甚至是依赖的,那么就是“同步状态”(Synchronized,这与反恐特警执行Action一样,需要互相配合,绝不能一哄而上——投手雷不能像招聘会上投简历那样!

三.线程小例
         这里给出一个C#写的多线程的小范例,如果有哪里不明白,请参阅MSDN。我在以后的文章中将仔细讲解这些小例子。
//==============================================//
//                    
水之真谛                 //
//                                              //
//       http://blog.csdn.net/FantasiaX         //
//                                              //
//                
上善若水润物无声             //
//==============================================//

using System;
using System.Threading;//
多线程程序必需的

namespace ThreadSample1
{
     class A
     {
         //
为了能够作为线程的入口点,程序必需是无参、无返回值
         public static void Say()
         {
              for (int i = 0; i < 1000; i++)
              {
                  Console.ForegroundColor = ConsoleColor.Yellow;
                   Console.WriteLine("A merry Christmas to you!");
              }
         }
     }

     class B
     {
         //
为了能够作为线程的入口点,程序必需是无参、无返回值
         public void Say()
         {
              for (int i = 0; i < 1000; i++)
              {
                   Console.ForegroundColor = ConsoleColor.Green;
                   Console.WriteLine("A merry Christmas to you!");
              }
         }
     }
    
     class Program
     {
         static void Main(string[] args)
         {
              //
用到两个知识点:A类的静态方法;匿名的ThreadStart实例
              //如果想了解如何构造一个线程(Thread)请查阅MSDN
              Thread Thread1 = new Thread(new ThreadStart(A.Say));
             
              B b = new B();
              //
这次是使用实例方法来构造一个线程
              Thread Thread2 = new Thread(new ThreadStart(b.Say));

              //
试试把下面两句同时注释掉,会发生什么结果?
              Thread2.Priority = ThreadPriority.Highest;
              Thread1.Priority = ThreadPriority.Lowest;

              Thread1.Start();
              Thread2.Start();
         }
     }
}
         这个例子完全是为了我们讲解lock而做的铺垫,希望大家一定要仔细读懂。其中最重要的是理解由静态方法和实例方法构造线程。还要注意到,本例中使用到了线程的优先级:Thread2的优先级为最高,Thread1的优先级为最低,所以尽管Thread1Thread2先启动,而要等到Thread2执行完之后再执行(线程优先级有5级,大家可以自己动手试一试)。如果把给两个线程赋优先级的语句注释掉,你会发现两种颜色交错在一起……这就体现出了线程间的“非同步状态”。注意:在没有注释掉两句之前,两个线程虽然有先后顺序,但那是由优先级(操作系统)决定的,不能算是同步(线程间相互协同)

四.登顶
         很抱歉的一点是,lock的使用与线程的同步是相关的,而本文限于篇幅又不能对线程同步加以详述。本人将在近期再写一篇专门记述线程同步的文章,在此之前,请大家先参阅MSDN及其他同仁的作品。
1
.使用lock关键字的第一个目的:保证共享资源的安全
   
当多个线程共享一个资源的时候,常常会产生协同问题,这样的协同问题往往是由于时间延迟引起的。拿银行的ATM机举例,如果里面有可用资金5000元,每个人每次可以取50200元,现在有100个人来取钱。假设一个人取钱的时候,ATM机与银行数据库的沟通时间为10秒,那么在与总行计算机沟通完毕之前(也就是把你取的钱从可用资金上扣除之前),ATM机不能再接受别一个人的请求——也就是被“锁定”。这也就是lock关键字得名的原因。
如果不“锁定”ATM会出现什么情况呢?假设ATM里只剩下100元了,后面还有很多人等着取钱,一个人取80ATM验证80<100成立,于是吐出80,这时需要10秒钟与银行总机通过网络沟通(网络,特别是为了保证安全的网络,总是有延迟的),由于没有锁定ATM,后面的客户也打算取80……戏剧性的一幕出现了:ATM又吐出来80!因为这时候它仍然认为自己肚子里有100元!下面的程序就是这个例子的完整实现。
        
这个例子同时也展现了lock关键第的第一种用法:针对由静态方法构造的线程,由于线程所执行的方法并不具有类的实例作为载体,所以,“上锁”的时候,只能是锁这个静态方法所在的类——lock (typeof(ATM))
//======================================================//
//                         
水之真谛                    //
//                                                      //
//            http://blog.csdn.net/FantasiaX            //
//                                                      //
//                    
上善若水润物无声                 //
//======================================================//
using System;
using System.Threading;

namespace LockSample
{
     class ATM
     {
         static int remain = 5000;//
可用金额

         public static void GiveOutMoney(int money)
         {
              lock (typeof(ATM))//
核心代码!注释掉这句,会得到红色警报
              {
                   if (remain >= money)
                   {
                       Thread.Sleep(100);//
模拟时间延迟
                       remain -= money;
                   }
              }
              if (remain >= 0)
              {
                   Console.ForegroundColor = ConsoleColor.Green;
                   Console.WriteLine("{0}$ /t in ATM.", remain);
              }
              else
              {
                   Console.ForegroundColor = ConsoleColor.Red;
                   Console.WriteLine("{0}$ /t remained.", remain);
              }
         }
     }

     class Boy
     {
         Random want = new Random();
         int money;

         public void TakeMoney()
         {
                   money = want.Next(50, 200);
                   ATM.GiveOutMoney(money);
         }
     }
    
     class Program
     {
         static void Main(string[] args)
         {
              Boy[] Boys = new Boy[100];
              Thread[] Threads = new Thread[100];
              for (int i = 0; i < 100; i++)
              {
                   Boys[i] = new Boy();
                   Threads[i] = new Thread(new ThreadStart(Boys[i].TakeMoney));
                   Threads[i].Name = "Boy" + i.ToString();
                   Threads[i].Start();
              }
         }
     }
}
2.使用lock关键字的第二个目的:保证线程执行的顺序合理
        
回想上面的例子:取钱这件事情基本上可以认为是一个操作就能完成,而很多事情并不是一步就能完成的,特别是如果每一步都与某个共享资源挂钩时,如果在一件事情完成(比如十个操作步骤)之前不把资源锁进来,那么N多线程乱用资源,肯定会混乱不堪的。相反,如果我们在一套完整操作完成之前能够锁定资源(保证使用者的“独占性”),那么想使用资源的N多线程也就变得井然有序了。
        
狗年快到了,让我们来看看我们的狗妈妈是怎样照顾她的小宝贝的。狗妈妈“花花”有三个小宝贝,它们的身体状况不太相同:壮壮很壮,总是抢别人的奶吃;灵灵体格一般,抢不到先也不会饿着;笨笨就比较笨了,身体弱,总是喝不着奶。这一天,狗妈妈决定改善一下给小宝贝们喂奶的方法——由原来的哄抢方式改为一狗喂十口,先喂笨笨,然后是灵灵,最后才是壮壮……在一只小狗狗吮完十口之前,别的小狗狗不许来捣蛋!OK,让我们看下面的代码:
        
注意,这段代码展示了lock的第二种用法——针对由实例方法构造的线程,lock将锁住这个方法的实例载体,也就是使用了——lock (this)
//======================================================//
//                         
水之真谛                    //
//                                                      //
//            http://blog.csdn.net/FantasiaX            //
//                                                      //
//                    
上善若水润物无声                 //
//======================================================//
using System;
using System.Threading;

namespace LockSample2
{
     class DogMother
     {
         //
喂一口奶
         void Feed()
         {
              //Console.ForegroundColor = ConsoleColor.Yellow;
              //Console.WriteLine("Puzi...zi...");
              //Console.ForegroundColor = ConsoleColor.White;
              Thread.Sleep(100);//
喂一口奶的时间延迟
         }
         //
每只狗狗喂口奶
         public void FeedOneSmallDog()
         {
              //
因为用到了实例方法,所以要锁thisthis是本类运行时的实例
              //注释掉下面一行,回到哄抢方式,线程的优先级将产生效果
              lock (this)
              {
                   for (int i = 1; i <= 10; i++)
                   {
                       this.Feed();
                       Console.WriteLine(Thread.CurrentThread.Name.ToString() + " sucked {0} time.", i);
                   }
              }
         }
     }

     class Program
     {
         static void Main(string[] args)
         {
              DogMother huahua = new DogMother();

              Thread DogStrong = new Thread(new ThreadStart(huahua.FeedOneSmallDog));
              DogStrong.Name = "Strong small Dog";
              DogStrong.Priority = ThreadPriority.AboveNormal;

              Thread DogNormal = new Thread(new ThreadStart(huahua.FeedOneSmallDog));
              DogNormal.Name = "Normal small Dog";
              DogNormal.Priority = ThreadPriority.Normal;

              Thread DogWeak = new Thread(new ThreadStart(huahua.FeedOneSmallDog));
              DogWeak.Name = "Weak small Dog";
              DogWeak.Priority = ThreadPriority.BelowNormal;

              //
由于lock的使用,线程的优先级就没有效果了,保证了顺序的合理性
              //注释掉lock句后,线程的优先级将再次显现效果
              DogWeak.Start();
              DogNormal.Start();
              DogStrong.Start();
         }
     }
}
小结:
祝贺你!至此,我们已经初步学会了如何使用C#语言的lock关键字来使一组共享同一资源的线程进行同步、保证执行顺序的合理以及共享资源的安全。
相信如果你已经仔细看过例子,并在自己的机器上进行了实践,那么你对线程已经不再陌生、害怕(就像小宇宙爆发了一样)。如果你不满足于仅仅是学会一个lock,还想掌握更多更高超的技能(比如……呃……六道轮回?柔破斩?无敌风火轮?如来神掌?),请参阅MSDNSystem.Threading名称空间的内容,你会发现lock背后隐藏的秘密(Monitor ),而且我也极力推荐你这么做:趁热打铁,你可以了解到为什么lock只能对引用类型加以使用lockMonitorEnter/ExitTry…Catch是如何互换的……

五.祝愿
         今天是圣诞节呢,祝所有看到本文的XDJM圣诞节快乐!新年也快乐!
        
再过几天,新年也要到啦!狗狗年!我喜欢……祝所有明年是本命年的XDJM生活幸福,万事如意!噢~~~~~对了,明年是大狗高立的本命年,借这当先祝你吉祥如意啦!

六.祈福
         我有一个好朋友,小迪,心脏不太好……明年还要动大手术。在这离上帝最近的日子里,我为她祈祷:小迪,祝你平安!一切都会好起来的!我祈求上帝:当我在圣诞节的时候把我知道的知识尽心尽力讲给每一个愿意听的人时,算不算是帮您做事呢?如果是……请您赐予小迪以健康。我同样祈求每个读到这里的朋友:请您也为我的朋友祝福一下,谢谢。

 

 

法律声明:本文章受到知识产权法保护,任何单位或个人若需要转载此文,必需保证文章的完整性(未经作者许可的任何删节或改动将视为侵权行为)。文章出处请务必注明CSDN以保障网站的权益,文章作者姓名请务必保留,并向bladey@tom.com发送邮件,标明文章位置及用途。转载时请将此法律声明一并转载,谢谢!

阅读更多
个人分类: C#语言
上一篇暴强贴:从.NET平台调用Win32 API
下一篇学C#不学Java的最正当理由
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

关闭
关闭
关闭