白话并发冲突与线程同步(2)--Monitor.lock和死锁

2013-12-08  来源:本站原创  分类:系统架构  人气:0 

系列文章汇总:
白话并发冲突与线程同步(1)
白话并发冲突与线程同步(2)——Monitor、lock和死锁
白话并发冲突与线程同步(3)——Mutex、EventWaitHandle、AutoResetEvent 和 ManualResetEvent

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

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();
}
}
白话并发冲突与线程同步(2)--Monitor.lock和死锁

究其原因,就是因为Windows总是不问青红皂白随随便便就把我的线程给停掉了。例如,上面的那个程序很可能会以下面的顺序来执行(黄色底色的代码属于第一个线程,绿色底色的代码属于第二个线程):
白话并发冲突与线程同步(2)--Monitor.lock和死锁

这样,第一、第二个线程里面的循环各自执行了3次,n的值是3,而不是我们期望的6。
所以呢,我就打算建议比尔盖茨在C#里加一个关键字:
白话并发冲突与线程同步(2)--Monitor.lock和死锁

对foo2()也做同样的修改,这样,就可以确保程序以下图所示的顺序执行了:
白话并发冲突与线程同步(2)--Monitor.lock和死锁

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

白话并发冲突与线程同步(2)--Monitor.lock和死锁
白话并发冲突与线程同步(2)--Monitor.lock和死锁
可是,比尔盖茨听了我的建议之后,却把眉毛皱成了个大疙瘩,叹道:“大哥,不行呀。你知道,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):
白话并发冲突与线程同步(2)--Monitor.lock和死锁

如果我们把上图之中与Monitor相关的行和演示线程状态的行去掉,就可以得到下图:
白话并发冲突与线程同步(2)--Monitor.lock和死锁

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

不过,如果你尝试运行上面那个代码,就会发现它根本无法通过编译!这是因为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关键字,它其实是一个语法糖。
白话并发冲突与线程同步(2)--Monitor.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();
}
}

运行这段代码,可以得到这样的结果:
白话并发冲突与线程同步(2)--Monitor.lock和死锁

如上图所示,当程序恰巧以“线程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.

来自:白话并发冲突与线程同步(2)——Monitor、lock和死锁

相关文章
  • 白话并发冲突与线程同步(2)--Monitor.lock和死锁 2013-12-08

    系列文章汇总:白话并发冲突与线程同步(1) 白话并发冲突与线程同步(2)--Monitor.lock和死锁 白话并发冲突与线程同步(3)--Mutex.EventWaitHandle.AutoResetEvent 和 ManualResetEvent 竞赛暂时胜过它的目的,永远如此.对于要建立殖民地的殖民主义者,生活的意义就在于征服.士兵看不起移民,但是,征服的目的不就是要让移民定居下来吗?因此,在进步的狂热中,我们把人招来修铁路,建工厂,钻油井.但是,我们不是记得很清楚,我们进行的这些建设是服

  • 白话并发冲突与线程同步(1) 2014-03-23

    系列文章汇总:白话并发冲突与线程同步(1) 白话并发冲突与线程同步(2)--Monitor.lock和死锁 白话并发冲突与线程同步(3)--Mutex.EventWaitHandle.AutoResetEvent 和 ManualResetEvent 猴子抬头道:"我有一个梦,我想我飞起时,那天也让开路,我入海时,水也分成两边,众仙诸神,见我也称兄弟,无忧无虑,天下再无可拘我之物,再无可管我之人,再无我到不了之处,再无我做不成之事,再无我战不胜之物." --今何在 摘自<悟空传&

  • 白话并发冲突与线程同步(3)--Mutex.EventWaitHandle.AutoResetEvent 和 Manua 2014-10-16

    系列文章汇总:白话并发冲突与线程同步(1) 白话并发冲突与线程同步(2)--Monitor.lock和死锁 白话并发冲突与线程同步(3)--Mutex.EventWaitHandle.AutoResetEvent 和 ManualResetEvent 不过这热气是从实在的火里发出来的呢,还是从他的爱情里发出来的呢,他完全不知道.他的一切光彩现在都没有了.这是因为他在旅途中失去了呢,还是悲愁的结果,谁也说不出来. --安徒生 摘自<坚定的锡兵> 摘要 1-2-3翻开那<葵花宝典>,只

  • java线程同步原理(lock,synchronized) 2013-10-10

    一. java线程同步原理 java会为每个object对象分配一个monitor,当某个对象的同步方法(synchronized methods )被多个线程调用时,该对象的monitor将负责处理这些访问的并发独占要求. 当一个线程调用一个对象的同步方法时,JVM会检查该对象的monitor.如果monitor没有被占用,那么这个线程就得到了monitor的占有权,可以继续执行该对象的同步方法:如果monitor被其他线程所占用,那么该线程将被挂起,直到monitor被释放. 当线程退出同步

  • c#线程同步使用详解示例 2014-11-02

    这篇文章主要介绍了c#线程同步使用方法,介绍几种常用的C#进行线程同步的方式,需要的朋友可以参考下 在应用程序中使用多个线程的一个好处是每个线程都可以异步执行.对于 Windows 应用程序,耗时的任务可以在后台执行,而使应用程序窗口和控件保持响应.对于服务器应用程序,多线程处理提供了用不同线程处理每个传入请求的能力.否则,在完全满足前一个请求之前,将无法处理每个新请求.然而,线程的异步特性意味着必须协调对资源(如文件句柄.网络连接和内存)的访问.否则,两个或更多的线程可能在同一时间访问相同的资

  • C#线程同步的三类情景分析 2015-02-13

    这篇文章主要介绍了C#线程同步的三类情景分析,较为详细生动的讲述了C#线程同步的三类情况,让大家对C#多线程程序设计有一个深入的了解,需要的朋友可以参考下 本文实例讲述了C#线程同步的三类情景,分享给大家供大家参考.具体分析如下: C# 已经提供了我们几种非常好用的类库如 BackgroundWorker.Thread.Task等,借助它们,我们就能够分分钟编写出一个多线程的应用程序. 比如这样一个需求:有一个 Winform 窗体,点击按钮后,会将窗体中的数据导出到一个 output.pdf

  • Java高级-线程同步机制实现 2012-11-10

    前言 我们可以在计算机上运行各种计算机软件程序.每一个运行的程序可能包括多个独立运行的线程(Thread). 线程(Thread)是一份独立运行的程序,有自己专用的运行栈.线程有可能和其他线程共享一些资源,比如,内存,文件,数据库等. 当多个线程同时读写同一份共享资源的时候,可能会引起冲突.这时候,我们需要引入线程"同步"机制,即各位线程之间要有个先来后到,不能一窝蜂挤上去抢作一团. 同步这个词是从英文synchronize(使同时发生)翻译过来的.我也不明白为什么要用这个很容易引起误

  • c#.net多线程编程教学--线程同步 2014-03-19

    随着对多线程学习的深入,你可能觉得需要了解一些有关线程共享资源的问题. .NET framework提供了很多的类和数据类型来控制对共享资源的访问. 考虑一种我们经常遇到的情况:有一些全局变量和共享的类变量,我们需要从不同的线程来更新它们,可以通过使用System.Threading.Interlocked类完成这样的任务,它提供了原子的,非模块化的整数更新操作. 还有你可以使用System.Threading.Monitor类锁定对象的方法的一段代码,使其暂时不能被别的线程访问. System

  • C#中线程同步对象的方法分析 2014-06-23

    这篇文章主要介绍了C#中线程同步对象的方法,较为详细的分析了线程同步的原理与实现方法,并给出了实例总结,是比较实用的技巧,需要的朋友可以参考下 本文实例讲述了C#中线程同步对象的方法.分享给大家供大家参考.具体分析如下: 在编写多线程程序时无可避免会遇到线程的同步问题.什么是线程的同步呢? 举个例子:如果在一个公司里面有一个变量记录某人T的工资count=100,有两个主管A和B(即工作线程)在早一些时候拿了这个变量的值回去,过了一段时间A主管将T的工资加了5块,并存回count变量,而B主管将

  • .net中线程同步的典型场景和问题剖析 2015-02-04

    在使用多线程进行编程时,有一些经典的线程同步问题,对于这些问题,.net提供了多种不同的类来解决 在使用多线程进行编程时,有一些经典的线程同步问题,对于这些问题,.net提供了多种不同的类来解决.除了要考虑场景本身,一个重要的问题是,这些线程是否在同一个应用程序域中运行.如果线程都在同一应用程序域中运行,则可以使用一些所谓"轻量"级的同步类,否则要使用另一些类,而这些类都是对操作系统所提供的同步原语的包装,相对来说更消耗资源.我在这儿介绍一些典型的应用场景和相关的问题. 多线程争用独占

  • 并发编程基础四--同步代码块,同步方法,lock,死锁 2013-02-07

    老生常谈的一个问题,就bank的那个例子,同一张卡同时取钱,并发2个,每个取800,但卡里只有1000,不能让2个人同时操作.这里就需要线程同步.比如下面的DEMO. 首先定义一个账户类: package org.credo.thread.tongbu; public class Account { private String account; private double money; public Account(String account,double money){ this.acc

  • JAVA线程4 - 线程同步 2014-01-21

    一.线程安全问题 在多线程应用中,多个线程可能共享对同一数据的存取,这样可能会破坏数据.如操作同一个文件. 当一个线程在执行操作共享数据的多条代码过程中,其他线程参与了共享数据运算,就会导致线程安全的问题产生. 要解决这个问题,就需要掌握线程同步相关知识. 二.线程安全解决思路 将多条操作共享数据的代码封装起来,当有线程执行该封装好的代码后,其他线程不能同步执行该代码,只有当前线程执行结束后,其他线程才有执行资格. 三.线程同步-锁 使用synchronized关键字同步方法或代码. 1. 同步

  • Java线程同步之一--AQS 2014-12-04

    一.线程同步 线程同步是指两个并发执行的线程在同一时间不同时执行某一部分的 程序.同步问题在生活中也很常见,就比如在麦当劳点餐,假设只有一个服务员能够提供点餐服务.每个服务员在同一时刻只能接待一个顾客的点餐,那么除了正在 接待的顾客,其他人只能等待排队.当一个点餐服务完成之后,其他顾客就可以上去进行点餐. 从这个例子中可以看到如下几个关注点: 点餐服务为临界区域(critical area),其可同时进行的数量,即为有多少人可进入临界区域. 排队即为对目前暂时无法取得点餐服务的人的一种处理方式.

  • java的线程同步机制synchronized关键字的理解 2015-03-04

    线程同步: 由于同一进程的多个线程共享同一片存储空间,在带来方便的同时,也带来了访问冲突这个严重的问题.Java语言提供了专门机制以解决这种冲突,有效避免了同一个数据对象被多个线程同时访问. 需要明确的几个问题: 1)synchronized关键字可以作为函数的修饰符,也可作为函数内的语句,也就是平时说的同步方法和同步语句块.如果 再细的分类,synchronized可作用于instance变量.object reference(对象引用).static函数和class literals(类名称

  • Java线程同步:synchronized锁住的是代码还是对象 2015-03-26

    在Java中,synchronized关键字是用来控制线程同步的,就是在多线程的环境下,控制synchronized代码段不被多个线程同时执行.synchronized既可以加在一段代码上,也可以加在方法上. 关键是,不要认为给方法或者代码段加上synchronized就万事大吉,看下面一段代码: class Sync { public synchronized void test() { System.out.println("test开始.."); try { Thread.sle

  • CountDownLatch线程同步辅助 2015-04-20

    在上一篇文章中写道用一个静态的变量保存线程的执行状态,并用时间等待的方法后来仔细考虑,其实是 线程不安全的.多个线程同时执行这个类时,这个静态变量的值就不能保证了. 用一个线程同步的Map保存这个值,勉强能实现[每个线程生产一个不重复的map的key] 但是这样很麻烦. java. util. concurrent.CountDownLatch 却能完美的实现.能线程安全的计数,因为每个实现的主线程在并发的情况下java.util.concurrent.CountDownLatch; 是新的实例

  • C++线程同步实例分析 2014-02-06

    这篇文章主要介绍了C++线程同步实例分析,以实例的形式较为深入的分析了C++的线程同步问题,是一个较为经典的线程同步问题,需要的朋友可以参考下 本文实例分析了C++线程同步问题,分享给大家供大家参考.具体分析如下: 该实例设置全局变量g_bContinue,在主线程中设置全局变量g_bContinue,工作线程检测该全局变量,实现主线程控制工作线程的目的. 打印出的g_cnt1与g_cnt2的数值不同,是因为线程调试时时间片的切换. 具体代码如下: // countError.cpp : 定义控

  • java 实现线程同步的方式有哪些 2014-02-09

    当使用多个线程来访问同一个数据时,非常容易出现线程安全问题,所以我们用同步机制来解决这些问题,本文将详细介绍,需要的朋友可以参考下 什么是线程同步? 当使用多个线程来访问同一个数据时,非常容易出现线程安全问题(比如多个线程都在操作同一数据导致数据不一致),所以我们用同步机制来解决这些问题. 实现同步机制有两个方法: 1.同步代码块: synchronized(同一个数据){} 同一个数据:就是N条线程同时访问一个数据. 2. 同步方法: public synchronized 数据返回类型 方法

  • Linux线程同步之信号C语言实例 2014-07-01

    这篇文章主要介绍了Linux线程同步之信号C语言实例,本文直接给出代码实例,需要的朋友可以参考下 linux中向某个线程发送信号,若没有对该信号的处理函数,则会导致程序结束. 如下面的程序,创建一个线程,主线程向其发送一个信号,会导致程序立即结束 #include <stdio.h> #include <pthread.h> pthread_t t; void* run(void* arg) { while(1) { printf("Hello\n"); } }

  • ruby线程实现生产者消费者问题示例(队列Queue实现线程同步) 2014-07-04

    这篇文章主要介绍了ruby线程实现生产者消费者问题示例(队列Queue实现线程同步),需要的朋友可以参考下 Ruby线程实现经典的生产者消费者问题,用ruby中的Queue类实现线程同步问题. require "thread" puts "ProAndCon" queue = Queue.new #用队列Queue实现线程同步 producer = Thread.new do 10.times do |i| sleep rand(i) # 让线程睡眠一段时间 que