第十一章 线程同步与并发访问共享资源

本文详细介绍了多线程程序中可能出现的死锁与数据存取错误,以及如何通过锁(Monitor)、线程同步(Mutex、Semaphore、EventWaitHandle)和线程池来解决这些问题。此外,还探讨了线程局部存储区的概念和线程相关静态字段的使用,提供了解决并发访问共享资源的多种策略和方法。
摘要由CSDN通过智能技术生成

目录

1 死锁与数据存取错误

1.1 多线程程序中的 “死锁” 现象

1.2 多线程引发的数据存取错误

2 锁

2.1 锁定共享资源 —— Monitor

3 线程同步

3.1 等待句柄

3.2 使用互斥同步对象 Mutex

3.3 管理多个共享资源 —— Semaphore

3.4 线程同步事件类 —— EventWaitHandle

4 线程池

4.1 线程池简介

4.2 使用线程池

5 线程局部存储区

5.1 深入了解线程局部存储区

5.2 线程相关的静态字段


线程同步技术主要包括两个方面的内容:

一、多个线程推进顺序的控制问题

二、访问共享资源的问题


1 死锁与数据存取错误

1.1 多线程程序中的 “死锁” 现象

Join 方法如果使用不当,就会在线程之间形成死锁状态。以下代码为典型的死锁实例:

static Thread mainThread;
static void Main(string[] args)
{
   mainThread = Thread.CurrentThread;
   Thread m_objThread = new Thread(__PlayThread);
   m_objThread.Start();
   m_objThread.Join();
   Console.WriteLine("mainThread");
}

static void __PlayThread()
{
   mainThread.Join();
   Console.WriteLine("__PlayThread");
}

注意突出显示的两句代码:主线程在等待线程 A 运行结束,而线程 A 在运行过程中又等待主线程的运行结束。当死锁发生时,两个或多个线程相互等待,所有线程都无法再前进一步,程序会失去响应。

1. 线程推进循序不当造成线程间的循环等待。

2. 多个线程访问共享的资源。

1.2 多线程引发的数据存取错误

单 CPU 时,多线程访问 Info 类对象,一个线程在访问 Info.Result 属性时,在判断 if (num2 != 0)为true 后,可能在 NO.1 之后和 NO.2 之前处出现中断(线程挂起),此时另一个线程通过 DoSomething 方法修改 num2 的值为 0,中断恢复后,程序报错。双 CPU中,多线程访问 Info 类对象,情况更糟,一个线程访问 Info.Result 属性时,不管在 NO.1 之后和 NO.2 之前会不会中断,另一个线程都有可能通过 DoSomething 方法修改 num2 的值为0,程序报错。

class Info
{
   public int num1;
   public int num2;
   public int Result
   {
       get
       {
           if (num2 != 0)   // NO.1
           {
               return num1 / num2;     // NO.2
           }
           else
           {
               return 0;
           }
       }
   }

   public void DoSomething(int a1,int a2)
   {
       num1 = a1;
       num2 = a2;
   }
}

2 锁

访问共享资源,常用的方法是给资源加 “锁”,加上锁的共享资源一次只允许一个线程访问。从而避免多线程同时存取共享资源所带来的数据存取错误问题,付出的代价是程序性能的下降。

2.1 锁定共享资源 —— Monitor

由于多个线程同时访问共享数据时会造成数据存取错误,解决这一问题的方法是给共享的资源加上一把 “锁”,一次只允许一个线程访问共享资源。

Monitor 类可用于给共享资源加 “锁”。

如下代码所示,定义一个共享资源类:

class SharedResource
{
   public int InstanceCount = 0;
   public static int StaticCount = 0;
}

访问多线程共享的实例字段

private static void __PlayThread(object obj)
{
   // 访问实例字段
   Monitor.Enter(obj);      // 加锁
   int beginNumber = (obj as SharedResource).InstanceCount;
   Monitor.Exit(obj);     // 解锁
}

线程函数可能会被多个线程同时执行,在代码开头,使用 Monitor 类的 Enter 方法申请对共享对象 obj 的 “独家使用权” (即 “对象锁” ),之后就可以放心地访问它,访问完后,再调用 Monitor 类的 Exit 方法放弃对共享对象 obj 的 “独家使用权”。

在调用 Monitor 类的 Enter 方法申请对共享对象 obj 的 “独家使用权” 时,如果此对象已被其它线程所使用,即 “对象锁” 为另一线程所拥有,则申请锁的线程必须等待对方放弃 “对象锁” 之后(通过调用 Monitor 类的 Exit 方法实现),才可以继续运行。

由于共享对象加锁的功能非常常用,所以 C# 甚至在语言级别就提供了于 Monitor 类的 Enter 和 Eixt 方法等价的关键字 —— lock。
其格式为:

lock (obj)
{
   // 访问共享资源的代码
}

它完全等同于以下代码段:

try
{
   Monitor.Enter(obj);
   // 访问共享资源的代码......
}
finally
{
   Monitor.Exit(obj);
}

因此,在开发中推荐使用 lock 关键字而不是直接使用 Monitor 类。
访问多线程共享的静态字段

private static void __PlayThread(object obj)
{
   // 访问静态字段
   Monitor.Enter(typeof(SharedResource));
   int beginNumber = SharedResource.StaticCount;
   Monitor.Exit(typeof(SharedResource));
}

使用 Monitor 控制线程的推进顺序
如果访问共享资源的多个线程间有着顺序关系,比如要求 A 线程先访问,之后 B 线程才可以访问,则可以使用 Monitor 类的 Wait 和 Pulse 方法。其代码框架如下:

// A线程执行的代码
lock(ob
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值