锁·——lock关键字详解
作 者:刘铁猛
日 期: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关键字详解的更多相关文章
- java 锁 Lock接口详解
一:java.util.concurrent.locks包下常用的类与接口(lock是jdk 1.5后新增的) (1)Lock和ReadWriteLock是两大锁的根接口,Lock代表实现类是Reen ...
- Java多线程(三)—— synchronized关键字详解
一.多线程的同步 1.为什么要引入同步机制 在多线程环境中,可能会有两个甚至更多的线程试图同时访问一个有限的资源.必须对这种潜在资源冲突进行预防. 解决方法:在线程使用一个资源时为其加锁即可. 访问资 ...
- “全栈2019”Java多线程第十六章:同步synchronized关键字详解
难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java多 ...
- Java synchronized 关键字详解
Java synchronized 关键字详解 前置技能点 进程和线程的概念 线程创建方式 线程的状态状态转换 线程安全的概念 synchronized 关键字的几种用法 修饰非静态成员方法 sync ...
- 【转载】C/C++中extern关键字详解
1 基本解释:extern可以置于变量或者函数前,以标示变量或者函数的定义在别的文件中,提示编译器遇到此变量和函数时在其他模块中寻找其定义.此外extern也可用来进行链接指定. 也就是说extern ...
- python关键字详解
今天依旧在啃:<笨方法学python>,其中习题37是复习各种关键字.我本想百度一下记一下就ok了,但是百度出来第一个就Hongten的博客.我才意识到我也有博客,我应该学习他,把这些积累 ...
- Java面试题04-final关键字详解
Java面试题04-final关键字详解 本篇博客将会讨论java中final关键字的含义,以及final用在什么地方,感觉看书总会有一些模糊,而且解释的不是很清楚,在此做个总结,以备准备面试的时候查 ...
- Objective-C 实用关键字详解1「面试、工作」看我就 🐒 了 ^_^.
在写项目 或 阅读别人的代码(一些优秀的源码)中,总能发现一些常见的关键字,随着编程经验的积累大部分还是知道是什么意思 的. 相信很多开发者跟我当初一样,只是基本的常用关键字定义属性会使用,但在关键字 ...
- java continue break 关键字 详解 区别 用法 标记 标签 使用 示例 联系
本文关键词: java continue break 关键字 详解 区别 用法 标记 标签 使用 示例 联系 跳出循环 带标签的continue和break 嵌套循环 深入continue ...
随机推荐
- android listview 三种适配器设置
1: public class ArrayAdapterActivity extends ListActivity { @Override public void onCreate(Bundle sa ...
- sqlplus handbook
1.直接敲sqlplus并回车就是启动SQL*PLUS,输入user及password将使用户登陆到缺省的数据库. 请输入用户名: 2.sqlplus user/password@SERVICE_NA ...
- nginx源代码分析--配置文件解析
ngx-conf-parsing 对 Nginx 配置文件的一些认识: 配置指令具有作用域,分为全局作用域和使用 {} 创建其他作用域. 同一作用域的不同的配置指令没有先后顺序:同一作用域能否使用同样 ...
- Egret及Node.js的安装部署
最近在学Html5游戏开发,我选择的是国内的一个游戏开发框架egret.因为涉及到node.js这个近年来新兴起来的技术.借此机会把这方面知识学习一下. node.js以及egret的操作类似于Lin ...
- @synthesize 与@dynamic区别
@synthesize 除非开发人员已经做了,否则由编译器自动生成getter/setter方法. 当开发人员自定义存或取方法时,自定义会屏蔽自动生成该方法. @dynamic 告诉编译器,不自动生成 ...
- (转载)JS中的prototype
原文地址:http://www.cnblogs.com/yjf512/archive/2011/06/03/2071914.html#!comments JS中的phototype是JS中比较难理解的 ...
- 关于用自带摄像机录像无法捕获uri 问题解决
这个 我自己调用,好像并没有出现什么问题. 下面是我的代码.你们可以参照一下 File file = new File(Environment.getExternalStorageDirectory( ...
- IntelliJ IDEA 14 注册码及注册码生成器
几个license: (1) key:IDEA value:61156-YRN2M-5MNCN-NZ8D2-7B4EW-U12L4 (2) key:huangweivalue:97493-G3A41- ...
- js点击更多显示更多内容效果
我写了一个简单的分段显示插件,用法很简单:1,把你要分面显示的内容的容器元素增加一个class=showMoreNChildren,并增加一个自定义属性pagesize="8" 这 ...
- 获取checkbox 的选中状态的id、checkbox的一些操作
var id_array=new Array(); $('input[name="id"]:checked').each(function(){ id_array.push($(t ...