随笔 - 48  文章 - 16 评论 - 1175 trackbacks - 23

.net asp web c# vb VS2005 VS2008 VS2003

    姓名 景春雷
    网名 1-2-3
    生日 1980.2.29
    城市 沈阳
《一升的眼泪》看完了。少有的几部看完了还有些舍不得的好剧。也开始意思到执着于那一丁点名利和私欲的自己是多么渺小。 11-29 19:59

与我联系

搜索

 

常用链接

我参与的团队

我的标签

随笔分类(47)

随笔档案(45)

文章分类(15)

相册

收藏夹(2)

积分与排名

  • 积分 - 155821
  • 排名 - 262

最新评论

阅读排行榜

评论排行榜

     竞赛暂时胜过它的目的,永远如此。对于要建立殖民地的殖民主义者,生活的意义就在于征服。士兵看不起移民,但是,征服的目的不就是要让移民定居下来吗?因此,在进步的狂热中,我们把人招来修铁路,建工厂,钻油井。但是,我们不是记得很清楚,我们进行的这些建设是服务人类的。……真理,对于一些人来说就是建造房子,而对于另一些人来说就是居住。
                                                                                    ——圣埃克絮佩里
                                                                                          摘自《人的大地》

1-2-3 和比尔盖茨的一些往事

在上一篇里我们说道,1-2-3写了一段程序,并且在使用了2个线程分别执行foo1()和foo2()之后,程序的结果就不对了。
class Program
{
    
static int n = 0;
    
static void foo1()
    {
        
for (int i = 0; i < 1000000000; i++// 10 浜?/span>
        {
                
int a = n;
                n 
= a + 1;
        }
        Console.WriteLine(
"foo1() complete n = {0}", n);
    }
    
static void foo2()
    {
        
for (int j = 0; j < 1000000000; j++// 10 浜?/span>
        {
                
int a = n;
                n 
= a + 1;
        }
        Console.WriteLine(
"foo2() complete n = {0}", n);
    }
    
static void Main(string[] args)
    {
        
new Thread(foo1).Start();
        
new Thread(foo2).Start();
    }
}

究其原因,就是因为Windows总是不问青红皂白随随便便就把我的线程给停掉了。例如,上面的那个程序很可能会以下面的顺序来执行(黄色底色的代码属于第一个线程,绿色底色的代码属于第二个线程):


这样,第一、第二个线程里面的循环各自执行了3次,n的值是3,而不是我们期望的6。
所以呢,我就打算建议比尔盖茨在C#里加一个关键字:

对foo2()也做同样的修改,这样,就可以确保程序以下图所示的顺序执行了:


如果这个建议被微软接受,它将创造两个记录:
  1. 它将是C#里面第一个中文关键字。
  2. 它将是C#里面最长的关键字。

可是,比尔盖茨听了我的建议之后,却把眉毛皱成了个大疙瘩,叹道:“大哥,不行呀。你知道,Windows里会同时运行着上千个线程,且不说那些居心不良的病毒和木马,就是那些干正经事的线程,谁又能保证在你那个超长关键字里包裹的代码不会运行个二、三十秒?CPU可只有一个,在那个线程运行的二、三十秒里,整个Windows都会一动不动的,不知情的用户还以为是Windows又挂掉了,最后挨骂的可是兄弟我呦!”

“不过,”比尔又接着说,“我可以提供另一种方案来达到同样的效果。我可以让线程1里面的指定代码块不执行完,线程2就一直处于阻塞(ThreadState.WaitSleepJoin)状态。”

要达到这个效果,需要使用.net里的两个函数。

Monitor.Enter(n); // 尝试获取对n的控制权。如果n没主儿,则成功获取了n的控制权;如果n已经有主儿了,则此线程阻塞,死等。
Monitor.Exit(n); // 释放对n的控制权。等待着n的那个阻塞中的线程将获取n的控制权,并从阻塞状态变成运行状态。

可以把n想像成WC里的一个蹲位,线程1 Enter了之后,其它线程就不能Enter了,只能干等着,直到线程1 Exit,下一个等着的线程才能Enter,之后才能继续办事。如果一个线程Enter了之后迟迟不Exit(例如Enter了之后,发生了异常,比如忘了带SZ),就是所谓的“占着MK不LS”了。(一边吃午饭一边看贴的兄弟对不住啦~~)

使用 Monitor

现在就可以在我的代码里使用Monitor了。
class Program
{
    
static int n = 0;
    
static void foo1()
    {
        
for (int i = 0; i < 1000000000; i++// 10 亿
        {
            Monitor.Enter(n);
            
int a = n;
            n 
= a + 1;
            Monitor.Exit(n);
        }
        Console.WriteLine(
"foo1() complete n = {0}", n);
    }

    
static void foo2()
    {
        
for (int j = 0; j < 1000000000; j++// 10 亿
        {
            Monitor.Enter(n);
            
int a = n;
            n 
= a + 1;
            Monitor.Exit(n);
        }
        Console.WriteLine(
"foo2() complete n = {0}", n);
    }
    
static void Main(string[] args)
    {
        
new Thread(foo1).Start();
        
new Thread(foo2).Start();
    }
}

这段代码很可能会以下图所示的顺序执行(黄色底色的代码属于线程1,绿色底色的代码属于线程2。下图演示了线程1循环2次,线程2循环1次,n的值为3):


如果我们把上图之中与Monitor相关的行和演示线程状态的行去掉,就可以得到下图:


怎么样?和我的那个超长关键字的效果一样吧?

不过,如果你尝试运行上面那个代码,就会发现它根本无法通过编译!这是因为Monitor.Enter()只接受类型为Object的参数。那么,可不可以写 Monitor.Enter((Object)n); 呢?它确实能够通过编译,但是这样岂不是要装箱20亿次?所以千万别这么写。没法子了,我们只能再声明一个Object类型的变量,专门用于这两个线程的同步。
class Program
{
    
static int n = 0;
    
static object mk = new object();
    
static void foo1()
    {
        
for (int i = 0; i < 1000000000; i++// 10 亿
        {
            Monitor.Enter(mk);
            
int a = n;
            n 
= a + 1;
            Monitor.Exit(mk);
        }
        Console.WriteLine(
"foo1() complete n = {0}", n);
    }
    
static void foo2()
    {
        
for (int j = 0; j < 1000000000; j++// 10 亿
        {
            Monitor.Enter(mk);
            
int a = n;
            n 
= a + 1;
            Monitor.Exit(mk);
        }
        Console.WriteLine(
"foo2() complete n = {0}", n);
    }
    
static void Main(string[] args)
    {
        
new Thread(foo1).Start();
        
new Thread(foo2).Start();
    }
}

这段代码在我的赛扬800的机器上运行时间为3分零6秒。

lock 关键字

在C#里面有一个lock关键字,它其实是一个语法糖。

小贴士:在VB里与lock等价的关键字是SyncLock。用法是
SyncLock (mk)
    
Dim a As Integer = n
    n 
= a + 1
End SyncLock

死锁

还有比占着MK不LS更恶劣的行径么?有,那就是吃着碗里的望着锅里的。在下面的这段代码中,线程1喜欢先占着mk1然后在mk2里办事;线程2呢,喜欢先占着mk2,然后在mk1里办事,要是这两个活宝碰到一起……
class Program
{
    
static object mk1 = new object();
    
static object mk2 = new object();
    
static void foo1()
    {
        
for (int i = 0; i < 100; i++)
        {
            Monitor.Enter(mk1);
            Console.WriteLine(
"i={0} 线程1:\"先占着mk1,再去mk2里办事。\"", i);
            Monitor.Enter(mk2);
            Console.WriteLine(
"i={0} 线程1:\"进入了mk2,办事\"", i);
            Monitor.Exit(mk2);
            Console.WriteLine(
"i={0} 线程1:\"办完事了,离开mk2\"", i);
            Monitor.Exit(mk1);
            Console.WriteLine(
"i={0} 线程1:\"办完事了,离开mk1\"", i);
        }
    }
    
static void foo2()
    {
        
for (int j = 0; j < 100; j++)
        {
            Monitor.Enter(mk2);
            Console.WriteLine(
"j={0} 线程2:\"先占着mk2,再去mk1里办事。\"", j);
            Monitor.Enter(mk1);
            Console.WriteLine(
"j={0} 线程2:\"进入了mk1,办事\"", j);
            Monitor.Exit(mk1);
            Console.WriteLine(
"j={0} 线程2:\"办完事了,离开mk1\"", j);
            Monitor.Exit(mk2);
            Console.WriteLine(
"j={0} 线程2:\"办完事了,离开mk2\"", j);
        }
    }
    
static void Main(string[] args)
    {
        
new Thread(foo1).Start();
        
new Thread(foo2).Start();
    }
}

运行这段代码,可以得到这样的结果:


如上图所示,当程序恰巧以“线程1 Enter mk1 -> 线程2 Enter mk2 -> 线程1 想要Enter mk2 发现 mk2 已经被占用,线程1阻塞 -> 线程2 想要Enter mk1 发现 mk1 己经被占用,线程2阻塞”这个顺序执行时,线程1等待线程2释放mk2,线程2等待线程1释放mk1,两个线程双双陷入阻塞状态,直到山无棱、天地合……这就是死锁。

参考文献

Jeffrey Richter, CLR via C#, Second Edition. Microsoft Press, 2006.



posted on 2008-06-02 08:35 1-2-3 阅读(3036) 评论(38)  编辑 收藏 网摘 所属分类: 白话线程同步系列

FeedBack:
#1楼  2008-06-02 08:41 kyorry      
哈哈,2终于出来了,等了好久,学习了
  回复  引用  查看    
#2楼 [楼主] 2008-06-02 08:44 1-2-3      
@kyorry
呵呵,还有3呢,只是还没想好要怎么写。
  回复  引用  查看    
#3楼  2008-06-02 08:53 优哉@游哉      
总结的相当不错, 文字也很有意思,
不过有一点需要探讨一下:
可不可以写 Monitor.Enter((Object)n); 呢?
它确实能够通过编译,但是这样岂不是要装箱20亿次?
所以千万别这么写。

我的印象中这样写不光是装箱多导致的效率问题,
其实根本达不到同步的目的,因为 Enter 的对象和 Exit
的对象都是装箱后的, 他们不是同一个对象.

  回复  引用  查看    
#4楼  2008-06-02 08:55 jillzhang      
总结的很好,也很风趣
  回复  引用  查看    
#5楼  2008-06-02 08:59 BlueMountain      
lz等的你好辛苦哦
  回复  引用  查看    
#6楼  2008-06-02 09:06 路缘      
不错,深入浅出。让人一看就明白,赞一个。
  回复  引用  查看    
#7楼 [楼主] 2008-06-02 09:06 1-2-3      
@优哉@游哉
这是个很有意思的问题。感觉Monitor.Enter((Object)n);应该是临时创建的对象所以应该达不到同步的目的。但是运行这样的代码却会发现结果是正确的!但是它和Monitor.Enter(mk);的效果却并不完全相同。比如写
lock(mk)
{
for(int i=0; i<10亿; i++)
{
...
}
}
就会一直运行里面的那个for循环10亿次。
而写
lock((object)n)
{
for(int i=0; i<10亿; i++)
{
...
}
}
却发现里面的for循环未执行完就会切换到别的线程,但是结果依然是正确的。让人很迷惑。

  回复  引用  查看    
#8楼 [楼主] 2008-06-02 09:07 1-2-3      
@jillzhang
@路缘
@BlueMountain
谢谢夸奖!
  回复  引用  查看    
#9楼  2008-06-02 09:11 henry      
@1-2-3
线程2本来就不会等线程1完全执行后才执行.锁是针对线程的代码片.
lock(mk) 体现出执行1才执行2这才是巧合.
如果锁是针对整个线程生命周期,估计asp.net很多应用就完蛋了....


  回复  引用  查看    
#10楼  2008-06-02 09:12 狼Robot      
学习
  回复  引用  查看    
#11楼  2008-06-02 09:27 飄lá┽蕩去      
经典,比尔兄的造型 比较牛
  回复  引用  查看    
#12楼  2008-06-02 09:29 solunar66      
终于等到2了,看来还得继续等3
  回复  引用  查看    
#13楼  2008-06-02 09:39 craboYang      
写得太好了,赶上园里“大话设计模式”了。 啥时候您也整本书, 咱立马捧场!
  回复  引用  查看    
好东西,学习
  回复  引用    
#15楼  2008-06-02 11:16 顺心 [未注册用户]
不错,总结挺好!
  回复  引用    
#16楼  2008-06-02 11:36 bill024 [未注册用户]
mark
  回复  引用    
#17楼  2008-06-02 11:46 BlueMountain      
lz 下面这个测试结果很让人迷惑,是不是你的代码得lock的位置应该放在for循环外面阿
class Program
{
static int n = 0;
static object mk = new object();
static void foo1()
{
for( int i = 0; i < 1000; i++ ) // 10 亿
{
Monitor.Enter( mk );
int a = n;
n = a + 1;
Monitor.Exit( mk );
Thread.Sleep( new Random().Next( 5, 20 ) );
}
Console.WriteLine( "foo1() complete n = {0}", n );
}
static void foo2()
{
for( int j = 0; j < 1000; j++ ) // 10 亿
{
Monitor.Enter( mk );
int a = n;
n = a + 1;
Monitor.Exit( mk );
Thread.Sleep( new Random().Next( 3, 50 ) );
}
Console.WriteLine( "foo2() complete n = {0}", n );
}
static void Main( string[] args )
{
new Thread( foo1 ).Start();
new Thread( foo2 ).Start();
}
}



至于这个 恩 至于下面这个 n装箱后会在堆上产生临时对象 这个时候lock的仅仅是那个临时对象,等n+1之后,在次装箱会产生新的临时对象,他们是两个不同的临时对象,所以根本就没有lock一个对象哦 俄 其实Lock 就是lock那个syncRoot了
lock((object)n)
{
for(int i=0; i<10亿; i++)
{
...
}
}
  回复  引用  查看    
#18楼 [楼主] 2008-06-02 11:57 1-2-3      
@BlueMountain
> 是不是你的代码得lock的位置应该放在for循环外面阿
要是把lock放到for循环的外面,就会变成线程1完全运行完毕之后线程2才会继续运行,那使用多线程就没啥意义啦。
  回复  引用  查看    
#19楼  2008-06-02 12:12 簡簡單單..      
Mark
  回复  引用  查看    
这盖茨哥哥谁画的? cute!!!
  回复  引用    
#21楼 [楼主] 2008-06-02 12:48 1-2-3      
@xiao_p(匿名)
偶也不知道呀,Google图片搜索里找的。
  回复  引用  查看    
#22楼  2008-06-02 12:51 成长的强强      
楼主你太有才了!!很佩服你!!
  回复  引用  查看    
#23楼  2008-06-02 13:00 Anytao      
如饮醇酒,怎一个爽字了得:-)
  回复  引用  查看    
#24楼 [楼主] 2008-06-02 13:08 1-2-3      
@Anytao
谢谢你这么高的评价,高兴~~
  回复  引用  查看    
#25楼  2008-06-02 14:04 airwolf2026      
俺打酱油的,嘎嘎路过
  回复  引用  查看    
#26楼  2008-06-02 14:05 gxh9731 [未注册用户]
赞一下,兄弟出品的白话系列都是精品,通俗易懂
  回复  引用    
#27楼 [楼主] 2008-06-02 15:12 1-2-3      
@gxh9731
谢谢。
  回复  引用  查看    
#28楼  2008-06-02 16:05 玉开      
深入浅出,great.
  回复  引用  查看    
#29楼  2008-06-02 16:53 Tony Zhou      
有意思
  回复  引用  查看    
#30楼  2008-06-02 19:44 侯垒      
赞一个.
  回复  引用  查看    
#31楼  2008-06-03 07:59 BlackCat      
写的真的很不错啊,等我啥时候用到多线程的时候就来你这里看看,哈哈
  回复  引用  查看    
#32楼 [楼主] 2008-06-03 08:10 1-2-3      
@BlackCat
谢谢,你不是不用再写代码了么?
  回复  引用  查看    
#33楼  2008-06-03 10:03 Solog      
冲着LZ文章排版的认真劲,不能不看,看着舒服哇~
  回复  引用  查看    
#34楼  2008-06-03 13:08 zzz [未注册用户]
精辟!完全合理的好帖,期待继续整理!!!
太有逻辑性了,不得不顶,不知楼主是不是也写书啊 !!!!!
  回复  引用    
#35楼 [楼主] 2008-06-03 13:40 1-2-3      
@zzz
呵呵,谢谢夸奖。俺肚子里这点墨水哪够写书啊,只是现学现卖,跟大家交流一下。
  回复  引用  查看    
#36楼  2008-06-03 21:02 lt1 [未注册用户]
不过可能只有初学者会这样用,事实上几乎不能出现这样的代码。但教学意义一流,景仰ing
  回复  引用    
#37楼  2008-07-03 10:44 lbq1221119      
呵呵 你写的文章,可真有个性,这一篇文章,要贴多少图啊..
  回复  引用  查看    
#38楼 [楼主] 2008-07-03 12:35 1-2-3      
@lbq1221119
其实贴图挺累的。可是除非文章写得特别吸引人,否则一眼望去满屏都是文字很可能就把读者吓跑了,弄点图片可以缓解一下。
  回复  引用  查看    

标题  
姓名  
主页
Email (博主才能看到) 
验证码 *  看不清,换一张 [登录][注册]
内容(请不要发表任何与政治相关的内容)  
  登录  使用高级评论  新用户注册  返回页首  恢复上次提交      
Google站内搜索

China-pub 计算机图书网上专卖店!6.5万品种 2-8折!
近千种 9-95 新二手计算图书火热销售中!
开发者征途系统新作:《设计模式——基于C#的工程化实现及扩展》



相关文章:

相关链接:

所属分类的其他文章: