作 者:刘铁猛
日 期: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 刘铁猛