随笔 - 60  文章 - 24 评论 - 1560 trackbacks - 29

.net asp web c# vb VS2005 VS2008 VS2003

    姓名 景春雷
    网名 1-2-3
    生日 1980.2.29
    城市 沈阳
[生活]昨晚看了变形金刚2。场面火爆,观众也火爆,居然买不到7:30的票,只好等9:30那场。PS:高中以后就没去过电影院了。

与我联系

搜索

 

常用链接

我参与的团队

我的标签

随笔分类(59)

随笔档案(57)

文章分类(23)

相册

收藏夹(2)

积分与排名

  • 积分 - 207375
  • 排名 - 223

最新评论

阅读排行榜

评论排行榜

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

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 阅读(3426) 评论(41)  编辑 收藏 网摘 所属分类: 白话线程同步系列

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
其实贴图挺累的。可是除非文章写得特别吸引人,否则一眼望去满屏都是文字很可能就把读者吓跑了,弄点图片可以缓解一下。

  回复  引用  查看    
你的超长关键字没有必要存在,也并不是一定需要锁才能达到你要的效果,只需要将你不愿意被打断的线程声明为一个实时线程即可。操作系统中实时线程仅能被实时线程的中断抢走CPU,如果仅有它自身一个或者有其他实时线程存在,只要第二个线程不是实时线程,第二个线程是永远抢不走CPU的。
  回复  引用    
#40楼[楼主] 2009-04-28 13:04 1-2-3      
@lonegunman
嗯,这招很不错。可是如果有第三个线程咋办呢?两个实时线程或两个普通线程之间还是会抢的呀。

  回复  引用  查看    
@1-2-3
我认为,如果存在两个实时线程,那么你独占CPU的动作很可能造成严重后果,特别是在实施系统中。并且,独占CPU本身这个想法就是非常错误的。所以这样的关键字是没有必要存在的,正如你所说的,会导致恶意抢cpu的情况发生。
想要避免你所描述的乱序执行的副作用,最好最彻底的办法就是严格地隔离线程,使得执行顺序不影响逻辑结果。

  回复  引用    



发表评论

昵称: [登录] [注册]

主页:

邮箱:(仅博主可见)

评论内容:

  登录  注册

[使用Ctrl+Enter键快速提交评论]

0 1207029




相关文章:

相关链接: