系列1 曾经说过:每个线程都有自己的资源,但代码区是共享的,即每个线程都可以执行相同的函数。

       这可能带来的问题就是多个线程同时执行一个函数,并修改同一变量值,这将导致数据的混乱,产生不可预料的结果。看下面的示例:

private void btnThread_Click(object sender, EventArgs e)

{

    Thread t1 = new Thread(ChangeTextBox);

    t1.Start();

    Thread t2 = new Thread(ChangeTextBox);

    t2.Start();

}

 

void ChangeTextBox()

{

    for (int i = 0; i < 10000; i++)

    {

        int num = int.Parse(txtNum.Text);

        num++;

        txtNum.Text = num.ToString();

    }

}

       结果,计数非但不是20000,相差的还很远。这是因为 CPU 在线程切换的过程中,2 个线程多次发生取出相同值进行运算。这显然不是我们想要达到的目的。

 

Lock

       要解决这一问题也非常简单,只需为这段代码加上 Lock 锁定:

private static object obj = new object();

void ChangeTextBox()

{

    for (int i = 0; i < 10000; i++)

    {

        lock(obj)

        {

            int num = int.Parse(txtNum.Text);

            num++;

            txtNum.Text = num.ToString();

        }

    }

}

       C# 提供了一个关键字 lock,它可以把一段代码定义为互斥段(critical section),互斥段在一个时刻内只允许一个线程进入执行,而其他线程必须等待。

   在C#中,关键字 lock 的定义:lock(expression) statement_block

       expression 代表你希望跟踪的对象,通常是对象引用。如果你想保护一个类的实例,你可以使用 this;如果你想保护一个静态变量(如互斥代码段在一个静态方法内部),一般使用类名就可以了。而 statement_block 就是互斥段的代码,这段代码在一个时刻内只可能被一个线程执行。

 

       再看一个 Lock 关键字的示例:

internal class Account

{

    int balance;

    Random r = new Random();

 

    internal Account(int initial)

    {

        balance = initial;

    }

 

    internal void Withdraw(int amount)

    {

        if (balance < 0)

        {

            throw new Exception("Negative Balance");

        }

 

        lock (this)

        {

            // 下面的代码保证在当前线程修改 balance 的值完成之前 

            // 不会有其他线程也执行这段代码来修改 balance 的值 

            // 因此,balance 的值是不可能小于 0 的 

 

            Console.WriteLine("Current Thread:" + Thread.CurrentThread.Name

                + " balance:" + balance.ToString() + " amount:" + amount);

            // 如果没有 lock 关键字的保护,那么可能在执行完 if 的条件判断之后 

            // 另外一个线程却执行了 balance=balance-amount 修改了 balance 的值 

            // 而这个修改对这个线程是不可见的,所以可能导致这时 if 的条件已经不成立了 

            // 但是,这个线程却继续执行 balance=balance-amount,所以导致 balance 可能小于 0 

            // 去除 lock 块可以看出效果,程序会抛出异常

            if (balance >= amount)

            {

                Thread.Sleep(5);

                balance = balance - amount;

            }

            Console.WriteLine("Current Thread:" + Thread.CurrentThread.Name

                + " balance:" + balance.ToString() + " amount:" + amount);

        }

    }

 

    internal void DoTransactions()

    {

        for (int i = 0; i < 100; i++)

        {

            Withdraw(r.Next(-50, 100));

        }

    }

}

 

internal class Test

{

    static internal Thread[] threads = new Thread[10];

    public static void Main()

    {

        Account acc = new Account(0);

        for (int i = 0; i < 10; i++)

        {

            Thread t = new Thread(new ThreadStart(acc.DoTransactions));

            threads[i] = t;

            threads[i].Name = i.ToString();

        }

 

        for (int i = 0; i < 10; i++)

        {

            threads[i].Start();

        }

 

        Console.ReadLine();

    }

}

       Lock 语法简单易用。其本质是针对 Monitor.Enter() 和 Monitor.Exit() 的封装,是一个语法糖!

 

 

Monitor

       当多个线程公用一个对象时,也会出现和公用代码类似的问题,这就需要用到 System.Threading 中的 Monitor 类,我们可以称之为监视器,Monitor 提供了使线程共享资源的方案。

       Monitor 类可以锁定一个对象,一个线程只有得到这把锁才可以对该对象进行操作。 对象锁机制保证了在可能引起混乱的情况下,一个时刻只有一个线程可以访问这个对象。Monitor 必须和一个具体的对象相关联,但是由于它是一个静态的类,所以不能使用它来定义对象,而且它的所有方法都是静态的,不能使用对象来引用。

    下面代码说明了使用 Monitor 锁定一个对象的情形:

// 表示对象的先进先出集合

Queue oQueue = new Queue();

try

{

    // 现在 oQueue 对象只能被当前线程操纵了

    Monitor.Enter(oQueue);

 

    // do something......

}

catch

{

 

}

finally

{

    // 释放锁 

    Monitor.Exit(oQueue);

}

       如上所示, 当一个线程调用 Monitor.Enter() 方法锁定一个对象时,这个对象就归它所有了,其它线程想要访问这个对象,只有等待它使用 Monitor.Exit() 方法释放锁。为了保证线程最终都能释放锁,你可以把 Monitor.Exit() 方法写在 try-catch-finally 结构中的 finally 代码块里。(Lock 关键字就是这个步骤的语法糖

 

       任何一个被 Monitor 锁定的对象,内存中都保存着与它相关的一些信息:

  1. 现在持有锁的线程的引用
  2. 一个预备队列,队列中保存了已经准备好获取锁的线程
  3. 一个等待队列,队列中保存着当前正在等待这个对象状态改变的队列的引用

       当拥有对象锁的线程准备释放锁时,它使用 Monitor.Pulse() 方法通知等待队列中的第一个线程,于是该线程被转移到预备队列中,当对象锁被释放时,在预备队列中的线程可以立即获得对象锁。

 

生产与消费

       下面是一个展示如何使用 lock 关键字和 Monitor 类来实现线程的同步和通讯的例子,是一个典型的生产者与消费者问题。

       在本例中,生产者线程和消费者线程是交替进行的,生产者写入一个数,消费者立即读取并且显示(注释中介绍了该程序的精要所在)。

/// <summary>

/// 被操作的对象

/// </summary>

public class Cell

{

    /// <summary>

    /// Cell 对象里的内容

    /// </summary>

    int cellContents;

 

    /// <summary>

    /// 状态标志: 为 true 时可以读取,为 false 则正在写入

    /// </summary>

    bool readerFlag = false;

 

    public int ReadFromCell()

    {

        lock (this)

        {

            if (!readerFlag)

            {

                try

                {

                    // 等待 WriteToCell 方法中调用 Monitor.Pulse()方法 

                    Monitor.Wait(this);

                }

                catch (SynchronizationLockException e)

                {

                    Console.WriteLine(e);

                }

                catch (ThreadInterruptedException e)

                {

                    Console.WriteLine(e);

                }

            }

 

            // 开始消费行为

            Console.WriteLine("Consume: {0}", cellContents);

            Console.WriteLine();

 

            // 重置 readerFlag 标志,表示消费行为已经完成 

            readerFlag = false;

 

            // 通知 WriteToCell()方法(该方法在另外一个线程中执行,等待中)

            Monitor.Pulse(this);

        }

        return cellContents;

    }

 

    public void WriteToCell(int n)

    {

        lock (this)

        {

            if (readerFlag)

            {

                try

                {

                    Monitor.Wait(this);

                }

                catch (SynchronizationLockException e)

                {

                    // 当同步方法(指Monitor类除Enter之外的方法)在非同步的代码区被调用 

                    Console.WriteLine(e);

                }

                catch (ThreadInterruptedException e)

                {

                    // 当线程在等待状态的时候中止 

                    Console.WriteLine(e);

                }

            }

            cellContents = n;

            Console.WriteLine("Produce: {0}", cellContents);

            readerFlag = true;

            Monitor.Pulse(this); // 通知另外一个线程中正在等待的 ReadFromCell() 方法 

        }

    }

}

 

/// <summary>

/// 生产者

/// </summary>

public class CellProd

{

    /// <summary>

    /// 被操作的 Cell 对象

    /// </summary>

    Cell cell;

 

    /// <summary>

    /// 生产者生产次数,初始化为 1 

    /// </summary>

    int quantity = 1;

 

    public CellProd(Cell box, int request)

    {

        cell = box;

        quantity = request;

    }

 

    public void ThreadRun()

    {

        for (int looper = 1; looper <= quantity; looper++)

        {

            // 生产者向操作对象写入信息 

            cell.WriteToCell(looper);

        }

    }

}

 

/// <summary>

/// 消费者

/// </summary>

public class CellCons

{

    Cell cell;

    int quantity = 1;

 

    public CellCons(Cell box, int request)

    {

        cell = box;

        quantity = request;

    }

 

    public void ThreadRun()

    {

        int valReturned;

        for (int looper = 1; looper <= quantity; looper++)

        {

            valReturned = cell.ReadFromCell(); // 消费者从操作对象中读取信息 

        }

    }

}

 

/// <summary>

/// 测试类

/// </summary>

public class MonitorSample

{

    public static void Main(String[] args)

    {

        // 一个标志位,如果是 0 表示程序没有出错,如果是 1 表明有错误发生

        int result = 0;

 

        // 下面使用 cell 初始化 CellProd 和 CellCons 两个类,生产和消费次数均为 20 次 

        Cell cell = new Cell();

        CellProd prod = new CellProd(cell, 20);

        CellCons cons = new CellCons(cell, 20);

        Thread producer = new Thread(new ThreadStart(prod.ThreadRun));

        Thread consumer = new Thread(new ThreadStart(cons.ThreadRun));

 

        // 生产者线程和消费者线程都已经被创建,但是没有开始执行 

        try

        {

            producer.Start();

            consumer.Start();

            producer.Join();

            consumer.Join();

            Console.ReadLine();

        }

        catch (ThreadStateException e)

        {

            // 当线程因为所处状态的原因而不能执行被请求的操作 

            Console.WriteLine(e);

            result = 1;

        }

        catch (ThreadInterruptedException e)

        {

            // 当线程在等待状态的时候中止 

            Console.WriteLine(e);

            result = 1;

        }

        // 尽管 Main() 函数没有返回值,但下面这条语句可以向父进程返回执行结果 

        Environment.ExitCode = result;

    }

}

       这个简单的例子解决了多线程应用程序中可能出现的大问题, 只要领悟了解决线程间冲突的基本方法,很容易把它应用到比较复杂的程序中去。 

C# 多线程详解 Part.04(Lock、Monitor、生产与消费)的更多相关文章

  1. 详解synchronized与Lock的区别与使用

    知识点 1.线程与进程 在开始之前先把进程与线程进行区分一下,一个程序最少需要一个进程,而一个进程最少需要一个线程.关系是线程–>进程–>程序的大致组成结构.所以线程是程序执行流的最小单位 ...

  2. python多线程详解

    目录 python多线程详解 一.线程介绍 什么是线程 为什么要使用多线程 二.线程实现 threading模块 自定义线程 守护线程 主线程等待子线程结束 多线程共享全局变量 互斥锁 递归锁 信号量 ...

  3. iOS开发——多线程OC篇&多线程详解

    多线程详解 前面介绍了多线程的各种方式及其使用,这里补一点关于多线程的概念及相关技巧与使用,相信前面不懂的地方看了这里之后你就对多线程基本上没有什么问题了! 1——首先ios开发多线程中必须了解的概念 ...

  4. iOS开发——GCD多线程详解

    GCD多线程详解 1. 什么是GCD Grand Central Dispatch 简称(GCD)是苹果公司开发的技术,简单来说,GCD就是iOS一套解决多线程的机制,使用GCD能够最大限度简化多线程 ...

  5. Java 多线程详解(四)------生产者和消费者

    Java 多线程详解(一)------概念的引入:http://www.cnblogs.com/ysocean/p/6882988.html Java 多线程详解(二)------如何创建进程和线程: ...

  6. java中多线程详解-synchronized

    一.介绍 当多个线程涉及到共享数据的时候,就会设计到线程安全的问题.非线程安全其实会在多个线程对同一个对象中的实例变量进行并发访问时发生,产生的后果就是“脏读”.发生脏读,就是取到的数据已经被其他的线 ...

  7. C#多线程详解(一) Thread.Join()的详解

    bicabo   C#多线程详解(一) Thread.Join()的详解 什么是进程?当一个程序开始运行时,它就是一个进程,进程包括运行中的程序和程序所使用到的内存和系统资源.而一个进程又是由多个线程 ...

  8. Java多线程详解(二)

    评论区留下邮箱可获得<Java多线程设计模式详解> 转载请指明来源 1)后台线程 后台线程是为其他线程服务的一种线程,像JVM的垃圾回收线程就是一种后台线程.后台线程总是等到非后台线程死亡 ...

  9. Delphi多线程详解

    (整理自网络) Delphi多线程处理 1-1多线程的基本概念 WIN 98/NT/2000/XP 是个多任务操作系统,也就是:一个进程可以划分为多个线程,每个线程轮流占用CPU 运行时间和资源,或者 ...

随机推荐

  1. VS2013+opencv2.4.9(10)配置[zz]

    1.         下载opencv2.4.9,然后解压到一个位置 设置opencv SDK解压目录,点击Extract后解压 我是习惯于解压到这个位置的. 解压过程如上图. 2.          ...

  2. linux-命令-ls

    一.命令介绍: ls命令是linux常用的命令之一.ls用来打印当前目录的文件清单或指定目录的文件清单,也可以查看到文件的基本权限和隐藏文件. 二.命令格式: ls [OPTION]... [FILE ...

  3. Maven构件解析(转)

    文章转自http://gavinwind2000.iteye.com/blog/2290652 谢谢博主的总结! 在Maven中,任何一个依赖.插件或者项目构建的输出,都可以称之为构件. Maven在 ...

  4. Settings

    任何一个构件都有唯一的坐标,根据这个坐标可以定义其在仓库中的唯一存储路径,这是Maven的仓库布局方式. groupId/artifactId/version/artifactId-version(- ...

  5. 纠结attr(),prop()

    刚刚看博客无意中看到attr()和prop()的区别,回头就去翻了一下手册,感觉手册上写的过于简单,不能很清晰的分辨出两者的区别,两者的参数用法都是高度相似. attr():设置或返回被选元素的属性值 ...

  6. $.ajax请求返回数据中status为200,回调的却是error?

    $.ajax({ type:'get',//使用get方法访问后台 dataType:'json',//访问json格式的数据 url:'http://job.hainan.net/api/recru ...

  7. spring简单介绍

    1.spring 的核心技术 IOC(控制翻转)和aop(切面编程) IOC容器是一种设计模式,可以说是工厂模式的升华.它有多种实现方法,其中主要是依赖注入. aop是一种设计思想,通常的功能包括日志 ...

  8. block中如何避免循环引用

    使用 weak–strong dance 技术 block 可以直接引用 self,但是要非常小心地在 block 中引用 self.因为在 block 引用 self,可能会导致循环引用.如下例所示 ...

  9. ScriptableObject本地序列化后重启Unity后报The associated script can not be loaded.Please fix any compile errors and assign a valid script的坑

    踩坑 做编辑器一些设置序列化存在本地的时候,继承自ScriptableObject的类通过 创建的asset文件. 在重启Unity后查看这个asset发现上面的所有序列化属性丢失,报的错就是 在不存 ...

  10. C++ 类继承的对象布局

    C++多重继承下,对象布局与编译器,是否为虚拟继承都有很大关系,下面将逐一分析其中的差别,相同点为都按照类继承的先后顺序布局(类内按照虚表.成员声明先后顺序排列).该类情况为子类按照继承顺序排列,如c ...