C# 多线程(二) 线程同步基础(上)

本系列的第一篇简单介绍了线程的概念以及对线程的一些简单的操作,从这一篇开始讲解线程同步,线程同步是多线程技术的难点。线程同步基础由以下几个部分内容组成

1、同步要领(Synchronization Essentials)

2、锁(Locking)

3、线程安全(Thread Safety)

4、事件等待句柄(Signaling with Event Wait Handles)

5、同步上下文(Synchronization Contexts)


同步要领(Synchronization Essentials)

线程同步可以分为四类:

1、简单的阻塞方法(Simple blocking methods,说个不恰当的比喻,所谓同步就是阻塞跑块的人让其停下来等跑慢的人,然后齐步走,这就是所谓的同步,多么和谐的画面 ):主要有三个方法 Thread.Sleep、Join和 Task.Wait。

2、锁(Locking constructs):只允许一个线程进入临界区。常见的锁有 Lock(Monitor.Enter/Moitor.Exit),Mutex(互斥量)、SpinLock(自旋锁)、Semaphore(信号量)、SemaphoreSlim 和 读写锁(reader/writer locks)

3、信号(Signaling constructs):这种机制允许线程在收到外界通知之前处于暂停状态。主要有两种信号机制:事件等待句柄(event wait handles)和 Monitor的Wait和Pluse 方法。.NET 4.0 引入了 CountdownEvent 和 Barrier 等类

4、非阻塞线程同步(Nonblocking synchronization constructs):CLR和C# 提供了以下几种非阻塞同步的方式: Thread.MemoryBarrier、Thread.VolatileRead 、Thread.VolatileWrite、和 volatile 关键字以及 Interlocked 类。

下面一一述说这四种同步方法:

一、简单的阻塞方法

何谓线程阻塞:由于某种原因线程的执行被暂停的现象被称为线程阻塞。

常见的使线程阻塞方式为执行线程主动调用 Thread.Sleep 方法来阻塞自己以及通过 Join 和 EndInvoke 方法阻塞其他线程让其他线程等待本线程执行结束。一个被阻塞的线程会让出CPU资源给其他线程。

当一个线程被阻塞或者被唤醒(blocks or unblocks)时,操作系统完成上下文转换(context switch)的过程。

唤醒发生在以下4种情况:

1、阻塞条件被满足(by the blocking condition being satisfied) 原文这句觉得怪怪的

2、操作超时(如果指定了超时时间timeout)

3、通过 Thread.Interrupt 中断了

4、通过 Thread.Abort 放弃了

当线程通过Suspend方法暂停(该方法不建议使用),不认为是被阻塞了。

阻塞 VS 自旋 (Blocking Versus Spining)

有时我们需要某一个线程在指定条件满足前处于阻塞状态。信号(Signaling)和锁(Locking)的方式很好地满足我们的要求。但是,有一种更简单的实现:一个线程可以通过自旋(spining或翻译为空转)的方式来实现,如:

while(!proceed);  // ; 空语句
或者
while(DateTime.Now < nextStartTime);

上面的空语句的方式非常浪费 CPU 时间,最好改为如下的方式

while(!proceed) Thread.Sleep(10);

线程状态

我们可以通过 ThreadState 属性获取线程的状态,下图显式了线程状态的转换关系

二、锁(Locking)

锁提供了一种互斥访问的机制——只允许一个线程进入特殊的代码片段(临界区)。本节将从 lock 谈起然后讲 Mutex。lock 语句相对较快(消耗资源较Mutex少)而且也更简单,但是Mutex能实现跨程序的互斥(如只允许程序的单开),这点是lock语句没法办到的。

.NET 4.0 引入了 SpinLock 结构用在高并发的情况。

先看个例子:

using System;
using System.Threading;

class App
{
    static void Main()
    {
        AppDomain.CurrentDomain.UnhandledException += new UnhandledExceptionEventHandler(CurrentDomain_UnhandledException); // 全局异常捕获

        for (int i = 0; i < 100*10000; i++)
        {
            var t = new Thread(() => ThreadUnsafe.Go());
            t.Start();
        }
    }

    static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
    {
        Exception error = (Exception)e.ExceptionObject;
        Console.WriteLine("MyHandler caught : " + error.Message);
    }
}

class ThreadUnsafe
{
    static int _val1 = 1, _val2 = 1;
    public static void Go()
    {
        if (_val2 != 0) { Console.WriteLine(_val1 / _val2); }    // 这存在除数为0的异常的风险
        _val2 = 0;
    }
}

运行结果

分析下这个程序,考虑有两个线程同时调用 ThreadUnsafe 类的 Go 方法,第一个把 _val2赋为0,第二个确用_val2 做除法,这就会包异常,虽然这个异常出现的概率很低,但在大量重复的情况下就很容易出现,我这例子重复100万次,这就是线程不安全的表现。

现在用 lock 语句解决这个问题

class ThreadUnsafe
{
    static readonly object _locker = new object();    // 用锁解决线程不安全的问题
    static int _val1 =1, _val2 =1;
    public static void Go()
    {
        lock(_locker)
        {
            if(_val2 != 0) {Console.WriteLine(_val1 /_val2);}    // 这就不会包除数为0的异常
            _val2 = 0;
        }
    }
}

一些锁的比较附图

Monitor.Enter 和 Monitor.Exit

C# 的 lock语句实际上用 try/finally 和 Monitor.Enter 和 Monitor.Exit 方法,我们看上面lock代码生成的IL,如下图所示,注意我用红框圈出的部分。

所以用Monitor解决我们刚才的问题也行,代码如下:

class ThreadUnsafe
{
    static readonly object _locker = new object();
    static int _val1 =1, _val2 =1;
    public static void Go()
    {
        Monitor.Enter(_locker);    // 用 try/finally 和 Monitor 方式
        try
        {
            if(_val2 != 0) {Console.WriteLine(_val1 /_val2);}
            _val2 = 0;
        }
        finally {Monitor.Exit(_locker);}
    }
}

其实上面的代码是有缺陷的,考虑一个情况,某一个线程执行了 Monitor.Enter(_locker) 后挂了(比如内存不足引发OutOfMemoryException异常),由于它没进入try/finally 所以它没法释放锁,由于该线程已经挂了,所以它永远不会再释放它占有的锁了,除非程序重启,这就导致了锁泄露(leaked lock)。

对于这个问题的解决办法视乎只要把 Monitor.Enter放入到 try 中就可以了,这样做就真的没有破绽了么?

try
{
    Monitor.Enter(_locker);    // 考虑在这一行之前线程就挂了,还是内存不足异常,Monitor还没来得及获取到锁对象
    if(_val2 != 0) {Console.WriteLine(_val1 /_val2);}
    _val2 = 0;
}
finally {Monitor.Exit(_locker);}

由于程序以及进入了try语句块,那么它也一定会进入 finally 语句块,那么 执行 Monitor.Exit(_locker)的时候会出现 SynchronizationLockException 异常,这异常的含义是当前线程不拥有锁对象。

.NET 4.0为了结局了这个问题,引入方法

public static void Enter(Object obj,ref bool lockTaken)    获取指定对象上的排他锁,并自动设置一个值,指示是否获取了该锁,lockTaken 为true表示已经获取到锁,反之代表没获取到锁

所以最正确的写法是

bool lockTaken = false;
try
{
    Monitor.Enter(_locker,ref lockTaken);
    if(_val2 != 0) {Console.WriteLine(_val1 /_val2);}
    _val2 = 0;
}
finally { if(lockTaken) Monitor.Exit(_locker);}

其实在 .NET 4.0 中的lock语句最终在IL层面是翻译成这样的,现在再去打量下那幅 IL 截图的第二个红框 看到了么

TryEnter

Moniter 也提供了 TryEnter 方法,可以设置超时,看下MSDN截图

个人觉得红框圈出的那个最有用,在看下MSDN给出的例子,代码不言自明了吧

bool acquiredLock = false;

  try
  {
      Monitor.TryEnter(lockObject, 500, ref acquiredLock);
      if (acquiredLock)
      {

          // Code that accesses resources that are protected by the lock.

      }
      else
      {

          // Code to deal with the fact that the lock was not acquired.

      }
  }
  finally
  {
      if (acquiredLock)
      {
          Monitor.Exit(lockObject);
      }
  }

本文完(付Windows Live Writer 的代码着色效果真差 -_-#)

时间: 2024-08-02 15:14:02

C# 多线程(二) 线程同步基础(上)的相关文章

C# 多线程(二) 线程同步基础

本系列的第一篇简单介绍了线程的概念以及对线程的一些简单的操作,从这一篇开始讲解线程同步,线程同步是多线程技术的难点.线程同步基础由以下几个部分内容组成 1.同步要领(Synchronization Essentials) 2.锁(Locking) 3.线程安全(Thread Safety) 4.事件等待句柄(Signaling with Event Wait Handles) 5.同步上下文(Synchronization Contexts) 同步要领(Synchronization Essen

C#中的线程(二) 线程同步基础

1.同步要领 下面的表格列展了.NET对协调或同步线程动作的可用的工具:                       简易阻止方法 构成 目的 Sleep 阻止给定的时间周期 Join 等待另一个线程完成                       锁系统 构成 目的 跨进程? 速度 lock 确保只有一个线程访问某个资源或某段代码. 否 快 Mutex 确保只有一个线程访问某个资源或某段代码.可被用于防止一个程序的多个实例同时运行. 是 中等 Semaphore 确保不超过指定数目的线程访问某

mfc小工具开发之定时闹钟之---多线程急线程同步

一.MFC对多线程编程的支持 MFC中有两类线程,分别称之为工作者线程和用户界面线程.二者的主要区别在于工作者线程没有消息循环,而用户界面线程有自己的消息队列和消息循环. 工作者线程没有消息机制,通常用来执行后台计算和维护任务,如冗长的计算过程,打印机的后台打印等.用户界面线程一般用于处理独立于其他线程执行之外的用户输入,响应用户及系统所产生的事件和消息等.但对于Win32的API编程而言,这两种线程是没有区别的,它们都只需线程的启动地址即可启动线程来执行任务. 在MFC中,一般用全局函数Afx

第二章线程同步基础

Java 7 并发编程实战手册目录 代码下载(https://github.com/Wang-Jun-Chao/java-concurrency) 第二章线程同步基础 2.1简介 多个执行线程共享一个资源的情景,是最常见的并发编程情景之一.在并发应用中常常遇到这样的情景:多个线程读或者写相同的数据,或者访问相同的文件或数据库连接. 为了防止这些共享资源可能出现的错误或数据不一致,我们必须实现一些机制来防止这些错误的发生. 为了解决这些问题,引入了临界区(Critical Section)概念,临

关于Java多线程的线程同步和线程通信的一些小问题(顺便分享几篇质量高的博文)

Java多线程的线程同步和线程通信的一些小问题(顺便分享几篇质量高的博文) 前言:在学习多线程时,遇到了一些问题,这里我将这些问题都分享出来,同时也分享了几篇其他博客主的博客,并且将我个人的理解也分享给大家. 一.对于线程同步和同步锁的理解(注:分享了三篇高质量的博客) 以下我精心的挑选了几篇博文,分别是关于对线程同步的理解和如何选择线程锁以及了解线程锁的作用范围. <一>线程同步锁的选择 1. 这里我推荐下Java代码质量改进之:同步对象的选择这篇博文. 2. 以上推荐的博文是以卖火车票为例

MFC——9.多线程与线程同步

Lesson9:多线程与线程同步 程序.进程和线程是操作系统的重点,在计算机编程中,多线程技术是提高程序性能的重要手段.本文主要讲解操作系统中程序.进程和线程之间的关系,并通过互斥对象和事件对象实例说明多线程和线程同步技术. 1.      程序.进程和线程 1.1  程序和进程 程序是计算机指令的集合,它以文件的形式存储在磁盘上.进程通常被定义为一个正在运行的程序的实例,是一个程序在其自身的地址空间中的一次执行活动.进程是资源申请.调度和独立运行的单位,因此,它使用系统中的运行资源:而程序不能

Linux程序设计学习笔记----多线程编程线程同步机制之互斥量(锁)与读写锁

互斥锁通信机制 基本原理 互斥锁以排他方式防止共享数据被并发访问,互斥锁是一个二元变量,状态为开(0)和关(1),将某个共享资源与某个互斥锁逻辑上绑定之后,对该资源的访问操作如下: (1)在访问该资源之前需要首先申请互斥锁,如果锁处于开状态,则申请得到锁并立即上锁(关),防止其他进程访问资源,如果锁处于关,则默认阻塞等待. (2)只有锁定该互斥锁的进程才能释放该互斥锁. 互斥量类型声明为pthread_mutex_t数据类型,在<bits/pthreadtypes.h>中有具体的定义. 互斥量

C#多线程之线程同步3

在上一篇C#多线程之线程同步2中,我们主要学习了AutoResetEvent构造.ManualResetEventSlim构造和CountdownEvent构造,在这一篇中,我们将学习Barrier构造.ReaderWriterLockSlim构造和SpinWait构造. 七.使用Barrier构造 在这一小节中,我们将学习一个比较有意思的同步构造:Barrier.Barrier构造可以帮助我们控制多个等待线程达到指定数量后,才发送通知信号,然后所有等待线程才能继续执行,并且在每次等待线程达到指

java多线程之 ---- 线程同步

java多线程之线程同步 线程同步 定义:同步是指在同一时间段内只能运行一个线程. 分类:同步方法.同步块. 作用:安全解决共享问题. 同步块: 语法: synchronized (同步对象) { 需要同步的代码; } 例子: public class ThreadDemo implements Runnable{ private int ticket = 5; public void run(){ for(int i=1;i<=5;i++){ synchronized (this){ if(t