作  者:刘铁猛
日  期: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函数,如果是GUI(Graphic
User Interface,简言之就是带窗口交互界面一类的)应用程序,那么它的入口点将是_tWinMain函数(早先叫WinMain,参阅本人另一篇拙文《一个Win32程序的进化》)。一旦找到这个入口点函数,操作系统就要“调用”这个主函数,程序就从这里进入执行了。同时,系统会把开始执行的应用程序注册为一个“进程”(Process),欲知系统运行着多少进程,你可以按下Ctrl+Alt+Del,从Task
Manager里查看(如果对Windows如何管理进程感兴趣,可以阅读与分时系统、抢先式多任务相关的文章和书籍)。
    至此,我们可以说:如果把一个应用程序看成是一个Class的话,那么进程就是这个Class在内存中的一个“活”的实例——这是面向对象的理念。现在或许你也应该明白了为什么C#语言的Main函数要写在一个Class里,而不能像C/C++那样把main函数赤裸裸地写在外面。类是可以有多个实例的,一个程序也可以通过被双击N次在内存中运行N个副本,我们常用的Word
2003、QQ等都是这样的程序。当然,也有的程序只允许在内存里有一个实例,MSN Messenger和杀毒软件就是是这样的一类。
2.主角登场——线程
    
一个进程只做一件事情,这本无可非议,但无奈人总是贪心的。人们希望应用程序一边做着前台的程序,一边在后台默默无闻地干着其它工作。线程的出现,真可谓“将多任务进行到底”了。
    这儿有几个实际应用的例子。比如我在用Word杜撰老板交给的命题(这是Word的主线程),我的Word就在后台为我计时,并且每10分钟为我自动保存一次,以便在发生地震之后我能快速找回十分钟之前写的稿子并继续工作——死不了还是要交的。抑或是我的Outlook,它一边看我向手头的邮件里狠命堆诸如“预算正常”“进展顺利”之类的字眼,一边不紧不慢地在后台接收别人发给我的债务单和催命会议通知……它哪里知道我是多么想到Out去look一下,透透气。诸此IE,MSN
Messenger,QQ,Flashget,BT,eMule……尽数是基于多线程而得以生存的软件。现在,我们应该已经意识到,基本上稍微有点用的程序就得用到多线程——特别是在网络应用程序中,使用多线程格外重要。

二.进程和线程间的关系
         我们已经在感观上对进程和线程有了初步的了解,那么它们之间有什么关系呢?前面我们已经提到一点,那就是——进程是线程的载体,每个进程至少包含一个线程。接下来,我们来看看其它的关系。
         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的优先级为最低,所以尽管Thread1比Thread2先启动,而要等到Thread2执行完之后再执行(线程优先级有5级,大家可以自己动手试一试)。如果把给两个线程赋优先级的语句注释掉,你会发现两种颜色交错在一起……这就体现出了线程间的“非同步状态”。注意:在没有注释掉两句之前,两个线程虽然有先后顺序,但那是由优先级(操作系统)决定的,不能算是同步(线程间相互协同)

四.登顶
         很抱歉的一点是,lock的使用与线程的同步是相关的,而本文限于篇幅又不能对线程同步加以详述。本人将在近期再写一篇专门记述线程同步的文章,在此之前,请大家先参阅MSDN及其他同仁的作品。
1.使用lock关键字的第一个目的:保证共享资源的安全
    当多个线程共享一个资源的时候,常常会产生协同问题,这样的协同问题往往是由于时间延迟引起的。拿银行的ATM机举例,如果里面有可用资金5000元,每个人每次可以取50到200元,现在有100个人来取钱。假设一个人取钱的时候,ATM机与银行数据库的沟通时间为10秒,那么在与总行计算机沟通完毕之前(也就是把你取的钱从可用资金上扣除之前),ATM机不能再接受别一个人的请求——也就是被“锁定”。这也就是lock关键字得名的原因。
如果不“锁定”ATM会出现什么情况呢?假设ATM里只剩下100元了,后面还有很多人等着取钱,一个人取80,ATM验证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()
         {
              //因为用到了实例方法,所以要锁this,this是本类运行时的实例
              //注释掉下面一行,回到哄抢方式,线程的优先级将产生效果
              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,还想掌握更多更高超的技能(比如……呃……六道轮回?柔破斩?无敌风火轮?如来神掌?),请参阅MSDN中System.Threading名称空间的内容,你会发现lock背后隐藏的秘密(Monitor 类),而且我也极力推荐你这么做:趁热打铁,你可以了解到为什么lock只能对引用类型加以使用、lock与Monitor的Enter/Exit和Try…Catch是如何互换的……

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

原文链接:http://blog.csdn.net/fantasiax/article/details/561797   刘铁猛

锁·——lock关键字详解的更多相关文章

  1. java 锁 Lock接口详解

    一:java.util.concurrent.locks包下常用的类与接口(lock是jdk 1.5后新增的) (1)Lock和ReadWriteLock是两大锁的根接口,Lock代表实现类是Reen ...

  2. Java多线程(三)—— synchronized关键字详解

    一.多线程的同步 1.为什么要引入同步机制 在多线程环境中,可能会有两个甚至更多的线程试图同时访问一个有限的资源.必须对这种潜在资源冲突进行预防. 解决方法:在线程使用一个资源时为其加锁即可. 访问资 ...

  3. “全栈2019”Java多线程第十六章:同步synchronized关键字详解

    难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java多 ...

  4. Java synchronized 关键字详解

    Java synchronized 关键字详解 前置技能点 进程和线程的概念 线程创建方式 线程的状态状态转换 线程安全的概念 synchronized 关键字的几种用法 修饰非静态成员方法 sync ...

  5. 【转载】C/C++中extern关键字详解

    1 基本解释:extern可以置于变量或者函数前,以标示变量或者函数的定义在别的文件中,提示编译器遇到此变量和函数时在其他模块中寻找其定义.此外extern也可用来进行链接指定. 也就是说extern ...

  6. python关键字详解

    今天依旧在啃:<笨方法学python>,其中习题37是复习各种关键字.我本想百度一下记一下就ok了,但是百度出来第一个就Hongten的博客.我才意识到我也有博客,我应该学习他,把这些积累 ...

  7. Java面试题04-final关键字详解

    Java面试题04-final关键字详解 本篇博客将会讨论java中final关键字的含义,以及final用在什么地方,感觉看书总会有一些模糊,而且解释的不是很清楚,在此做个总结,以备准备面试的时候查 ...

  8. Objective-C 实用关键字详解1「面试、工作」看我就 🐒 了 ^_^.

    在写项目 或 阅读别人的代码(一些优秀的源码)中,总能发现一些常见的关键字,随着编程经验的积累大部分还是知道是什么意思 的. 相信很多开发者跟我当初一样,只是基本的常用关键字定义属性会使用,但在关键字 ...

  9. java continue break 关键字 详解 区别 用法 标记 标签 使用 示例 联系

    本文关键词: java continue break 关键字 详解 区别  用法 标记  标签 使用 示例 联系   跳出循环 带标签的continue和break 嵌套循环  深入continue ...

随机推荐

  1. android listview 三种适配器设置

    1: public class ArrayAdapterActivity extends ListActivity { @Override public void onCreate(Bundle sa ...

  2. sqlplus handbook

    1.直接敲sqlplus并回车就是启动SQL*PLUS,输入user及password将使用户登陆到缺省的数据库. 请输入用户名: 2.sqlplus user/password@SERVICE_NA ...

  3. nginx源代码分析--配置文件解析

    ngx-conf-parsing 对 Nginx 配置文件的一些认识: 配置指令具有作用域,分为全局作用域和使用 {} 创建其他作用域. 同一作用域的不同的配置指令没有先后顺序:同一作用域能否使用同样 ...

  4. Egret及Node.js的安装部署

    最近在学Html5游戏开发,我选择的是国内的一个游戏开发框架egret.因为涉及到node.js这个近年来新兴起来的技术.借此机会把这方面知识学习一下. node.js以及egret的操作类似于Lin ...

  5. @synthesize 与@dynamic区别

    @synthesize 除非开发人员已经做了,否则由编译器自动生成getter/setter方法. 当开发人员自定义存或取方法时,自定义会屏蔽自动生成该方法. @dynamic 告诉编译器,不自动生成 ...

  6. (转载)JS中的prototype

    原文地址:http://www.cnblogs.com/yjf512/archive/2011/06/03/2071914.html#!comments JS中的phototype是JS中比较难理解的 ...

  7. 关于用自带摄像机录像无法捕获uri 问题解决

    这个 我自己调用,好像并没有出现什么问题. 下面是我的代码.你们可以参照一下 File file = new File(Environment.getExternalStorageDirectory( ...

  8. IntelliJ IDEA 14 注册码及注册码生成器

    几个license: (1) key:IDEA value:61156-YRN2M-5MNCN-NZ8D2-7B4EW-U12L4 (2) key:huangweivalue:97493-G3A41- ...

  9. js点击更多显示更多内容效果

    我写了一个简单的分段显示插件,用法很简单:1,把你要分面显示的内容的容器元素增加一个class=showMoreNChildren,并增加一个自定义属性pagesize="8" 这 ...

  10. 获取checkbox 的选中状态的id、checkbox的一些操作

    var id_array=new Array(); $('input[name="id"]:checked').each(function(){ id_array.push($(t ...