您的位置:首页 > 其它

lock关键字详解

2009-08-21 14:19 281 查看

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

收藏

document.body.oncopy = function() {
if (window.clipboardData) {
setTimeout(function() {
var text = clipboardData.getData("text");
if (text && text.length>300) {
text = text + "/r/n/n本文来自CSDN博客,转载请标明出处:" + location.href;
clipboardData.setData("text", text);
}
}, 100);
}
}

function StorePage(){d=document;t=d.selection?(d.selection.type!='None'?d.selection.createRange().text:''):(d.getSelection?d.getSelection():'');void(keyit=window.open('http://www.365key.com/storeit.aspx?t='+escape(d.title)+'&u='+escape(d.location.href)+'&c='+escape(t),'keyit','scrollbars=no,width=475,height=575,left=75,top=20,status=no,resizable=yes'));keyit.focus();}
法律声明:

本文章受到知识产权法保护,任何单位或个人若需要转载此文,必需保证文章的完整性(未经作者许可的任何删节或改动将视为侵权行为)。文章出处请务必注明
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


函数

,如果是
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

是如何互换的……

五.祝愿

今天是圣诞节呢,祝所有看到本文的
XDJM

圣诞节快乐!新年也快乐!

再过几天,新年也要到啦!狗狗年!我喜欢……祝所有明年是本命年的
XDJM

生活幸福,万事如意!噢
~~~~~

对了,明年是大狗高立的本命年,借这当先祝你吉祥如意啦!

六.祈福


有一个好朋友,小迪,心脏不太好……明年还要动大手术。在这离上帝最近的日子里,我为她祈祷:小迪,祝你平安!一切都会好起来的!我祈求上帝:当我在圣诞
节的时候把我知道的知识尽心尽力讲给每一个愿意听的人时,算不算是帮您做事呢?如果是……请您赐予小迪以健康。我同样祈求每个读到这里的朋友:请您也为我
的朋友祝福一下,谢谢。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: