多线程下的进程同步(线程同步问题总结篇)

简介:

之前写过两篇关于线程同步问题的文章(),这篇中将对相关话题进行总结,本文中也对.NET 4.0中新增的一些同步机制进行了介绍。

  首先需要说明的是为什么需要线程功能同步。MSDN中有这样一段话很好的解释了这个问题:

当多个线程可以调用单个对象的属性和方法时,对这些调用进行同步处理是非常重要的。否则,一个线程可能会中断另一个线程正在执行的任务,使该对象处于一种无效状态。

也就说在默认无同步的情况下,任何线程都可以随时访问任何方法或字段,但一次只能有一个线程访问这些对象。另外,MSDN中也给出定义,成员不受多线程调用中断影响的类即线程安全类。

       CLI提供了几种可用来同步对实例和静态成员的访问的策略(前面两边文章介绍了这其中大部分机制):

  • 同步代码区域

可以使用Monitor类或(编译器支持的语法,如C#中的lock关键字)来同步需要安全的接受并发请求的代码段,这种方式比其他等效的同步方法有更好的性能。

lock语句通过Monitor的Enter和Exit方法实现代码段同步,使用try catch finally结构确保锁被释放。当线程执行该代码时,会尝试获取锁。如果该锁已由其他线程获取,则在锁变为可用状态之前,该线程一直处于阻塞状态。当线 程退出同步代码块时,锁就会被释放,这与线程的退出方式无关。通常情况下同步一小代码块并且不跨越多个方法的最佳选择是lock 语句,Monitor类 功能强大但使用不当容易出现孤立锁与死锁,而由于lock是通过Monitor的Enter和Exit实现的,因此在临界区中可以结合Monitor的其 它方法一起使用。

另外可以通过[MethodImpl(MethodImplOptions.Synchronized)]特性标 记一个方法是需要被同步的,方法可以是实例方法也可以是静态方法。最终实现的效果与使用lock关键字或Monitor相关方法相同。注意不要在此特性标 记的方法内使用lock(this)/lock(typeof(this))(注意,单独使用lock时也不应用对象本身或类型作为锁(应为类型或实例可 能被其它机制锁定,如被[MethodImpl(MethodImplOptions.Synchronized)]标记),对于实例方法与静态方法最好 分别使用声明新的私有成员或静态私有成员作为锁,避免使用公有成员作为锁)。另外不能对字符串加锁。

  • 手动同步:

.NET Framework中提供一些类用于手动进行线程间的访问同步。这些类主要分为3大类别(但正如下文中会看到的这些类别划分并非绝对,某些同步机制在多个类别之间有交叉):

ü  锁定

ü  通知

ü  连锁操作

  1. 锁定

排他锁

独占锁

最常见的形式就是C#的lock语句,该语句控制对一个代码块的访问,这个代码块被称作临界区。详见前文xx中对lock的介绍。

Monitor类

Monitor类提供了许多附加功能,这些功能可以与lock关键字结合使用(在lock的临界区中调用Monitor类的方法)。更多细节见线程同步问题1方法二中的介绍。

Mutex类

Mutex的作用也是创建一个临界区以同步对其中对象的访问,方式类似Monitor类,但最大的不同是Mutex支持跨进程的同步。当然其效率也不如Monitor类,在同一进程内通信应首先考虑使用Monitor。Mutex的介绍详见线程同步问题2方法五中的介绍。

SpinLock类

.NET4.0中新增

当 Monitor 所需的开销会造成性能下降时,可以使用 SpinLock 类。当SpinLock请求进入 临界区时,会反复地旋转(执行空循环),直至锁变为可用的。如果请求锁所需时间非常短,则空转可比阻塞提供更好的性能。但是,如果锁保留数十个周期以上, 则SpinLock的表现会和Monitor一样,而且将使用更多的CPU周期,降低其他线程或进程的性能。

其它锁

有些时候锁不必独占,可以允许一定数目的线程并发访问某个资源。下面列举的锁即用于这个目的。

ReaderWriterLock类

允许多个线程同时读取一个资源,但在向该资源写入时要求线程等待以获得独占锁。更多细节见线程同步问题1方法三中的介绍。

Semaphore类

Semaphore类允许指定数目的线程访问某个资源。超过这个数目时,请求该资源的其他线程会一直阻塞,直到某个线程释放信号量。更多细节见线程同步问题2方法七中的介绍。

ReaderWriterLockSlim类

.NET4.0中新增

这个类的作用与ReaderWriterLock类完全一致,其拥有更好的性能,在新开发的程序中应当使用ReaderWriterLockSlim而不是ReaderWriterLock。ReaderWriterLockSlim 具有线程关联。

SemaphoreSlim类

.NET4.0中新增

SemaphoreSlim类是用于在单一进程边界内进行同步的轻量信号量。使用方式上与Semaphore一致。

  1. 通知

通知机制是等待另一个线程的信号的所有方法的统称。

Join方法

这是等待来自另一个线程信号最简单的方法,解释Join方法最好有一个场景,假如我们有ThreadA,ThreadB两个线程,假如我们在ThreadB执行的方法中调用ThreadA.Join() 方法。这将阻塞B线程的执行直到A线程完成。场景中ThreadB可以是主线程也可以是其它子线程。其中也可以调用多个子线程的Join方法。这样 ThreadB将阻塞并等待所有这些线程执行完毕后才继续执行。另外如果ThreadA的方法中调用了其它线程的Join方法,这将形成一个队列形式的线 程调用,所有这些线程将一个个排队执行。

    Join也具有两个接受时间间隔的重载,用于设置阻塞线程等待的最长时间。依然用上面的例子来说,我们在 B线程方法中调用ThreadA.Join(5000),当在5秒钟内线程A执行完毕了,则Join方法会立刻返回true,ThreadB继续执行,如 果5秒钟线程A未完成,则Join方法在5秒钟到时返回false,ThreadA与ThreadB进入并行交替执行状态。

等待句柄

等待句柄派生自WaitHandle类,后者又派生自 MarshalByRefObject。从而等待句柄可用于跨应用程序域边界的线程同步。WaitHandle类封装了Win32的同步句柄,用于表示所有允许多个等待操作的同步对象。

通过调用WaitOne实例方法或WaitAll、WaitAny及SignalAndWait中任一个静态方法方法,可以阻塞当前线程以等待WaitHandle发出信号。

WaitHandle的派生类具有不同的线程关联。事件等待句柄(EventWaitHandle、 AutoResetEvent 和 ManualResetEvent)以及信号量没有线程关联。任何线程都可以发送事件等待句柄或信号量的信号。另一方 面,mutex有线程关联。拥有mutex的线程必须将其释放;而如果在不拥有mutex的线程上调用ReleaseMutex方法,则将引发异常。

事件等待句柄

事件等待句柄包括EventWaitHandle类及其派生类AutoResetEvent和 ManualResetEvent,这些类允许线程通过彼此发送信号和等待彼此的信号来同步活动。当通过调用Set方法或使用SignalAndWait 方法通知事件等待句柄时,阻塞线程会从事件等待句柄中释放。

事件等待句柄要么自动重置自身(类似于每次得到信号时只允许一个线程通过的旋转门),要么必须手动重置(类似于一 道门,在得到信号前一直关闭,得到信号打开后到其关闭前一直打开)。顾名思义,AutoResetEvent和ManualResetEvent分别表示 前者和后者。

AutoResetEvent

派生自EventWaitHandle,表示自动重置的本地事件。详见线程同步问题2方法六的介绍。

ManualResetEvent

派生自EventWaitHandle,表示手动重置的本地事件。详见线程同步问题2方法六的介绍

ManualResetEventSlim

.NET4.0中新增

ManualResetEventSlim类提供了ManualResetEvent的简化版本。其模型与使用方式上与ManualResetEvent一致,主要用于同一进程内线程间的同步。

CountdownEvent

.NET4.0中新增

CountdownEvent的作用与Semaphore相反,Semaphore中设置了最大可用槽数,当计数 为0时(即资源不够用时)则阻塞线程。而CountdownEvent用来统计其它线程结束工作的情况,当监听数变为0时,触发信号。本篇文章的最后部分 我们详细介绍CountdownEvent类。

Mutex类/ Semaphore类

这两个类均派生自WaitHandle,所以它们均可与WaitHandle的静态方法一起使用。例如,线程可以 使用WaitAll方法/WaitAny方法等待,而以下三个条件均可以使这个线程解除阻塞:EventWaitHandle接收到信号,Mutex被释 放,Semaphore被释放。

Barrier类

.NET4.0中新增

利用 Barrier 类,可以对多个线程进行循环同步,以便它们都在同一个点上阻塞来等待其他线程完成。后文将对这个类进行详细介绍。

         
  1. 连锁操作

联锁操作是由 Interlocked 类的静态方法对某个内存位置执行的简单原子操作。这些原子操作包括添加、递增和递减、交换、依赖于比较的条件交换,以及 32 位平台上的 64 位值的读取操作。关于Interlocked类详见线程同步问题1方法一。

特别注意,原子性的保证仅限于单个操作;如果必须将多个操作作为一个单元执行,则必须使用更粗粒度的同步机制。

尽管这些操作中没有一个是锁或信号,但它们可用于构造锁和信号。因为它们是Windows操作系统固有的,因此联锁操作的执行速度非常快。如CountdownEvent的实现中就使用了Interlocked类。

最后注意,只要有一个线程避开同步机制直接访问需要同步访问的资源,这种同步机制就是无效的。

  • 同步上下文:

可以使用SynchronizationAttribute为ContextBoundObject对象(上下文绑定对象)启用简单的自动同步。介绍详见线程同步问题1方法四中的介绍。

  • 线程安全集合:

.NET4.0中新引入的命名空间System.Collections.Concurrent中提供的集合类内 置对添加和移除操作的同步机制。多个线程可以在这些集合中安全高效地添加或移除项,而无需用户执行其他同步操作。在编写新代码时,如果遇到多个线程同时写 入集合的情况,就应使用并发集合类。如果仅从集合进行(并发)读取,则可使用System.Collections.Generic命名空间中的类。

从.NET发展来看,.NET1.0中提供的集合类(Aarry,Hashtable)通过 Synchronized属性支持同步,但不支持泛型,NET2.0种提供了泛型类的集合,但没有内置任何同步机制。.NET4.0开始提供的并发集合类 把线程安全与类型安全集合起来。为了提高效率这些并发集合的一部分使用了.NET4.0新增的轻量同步机制,如SpinLock、SpinWait、 SemaphoreSlim 和 CountdownEvent,另外ConcurrentQueue<T>和 ConcurrentStack<T>类没有使用这些同步机制,而是依赖Interlocked操作来实现线程安全性。

这个新增的命名空间下包含如下类型:

类型

说明

BlockingCollection<T>

通过实现IProducerConsumerCollection<T>接口,实现了一个支持生产者消费者模型的数据结构。

ConcurrentDictionary<TKey, TValue>

键/值对字典的线程安全实现。

ConcurrentQueue<T>

线程安全的队列实现。

ConcurrentStack<T>

线程安全的堆栈实现。

ConcurrentBag<T>

无序的元素集合的线程安全实现。

IProducerConsumerCollection<T>

BlockingCollection实现的接口。

 

CLR中不同类别可以根据要求以不同的方式进行同步。下表显示了上面列出的几类同步策略为不同类别的字段和方法提供的同步支持。

类别

全局字段

静态字段

静态方法

实例字段

实例方法

特定代码块

无同步

不同步

不同步

不同步

不同步

不同步

不同步

同步上下文

不同步

不同步

不同步

可以同步

可以同步

不同步

同步代码区域

不同步

不同步

当标记时同步

不同步

当标记时同步

当标记时同步

手动同步

手动

手动

手动

手动

手动

手动

 

到这,可以发现.NET4.0添加了很多新的同步类(轻量类型),这些类尽可能避免依赖高开销的Win32内核对 象(例如等待句柄)来提高性能。通常,当等待时间较短并且只有在尝试了原始同步类型并发现它们并不令人满意时,才应使用这些类型。另外,在需要跨进程通信 的方案中不能使用轻量类型。

 

以下内容来源这篇文章

CountdownEvent

CountdownEvent,前文中我们提及了CountdownEvent实现的同步效果。这里我们将给出一 个CountdownEvent适用的场景及示例代码。如我们可以在主线程中模拟一个线程池,通过CountdownEvent使得主线程可以等待线程池 中所有线程结束后才能继续执行(对所有子线程的执行顺序没有要求)。在给出代码之前先介绍一些CountdownEvent中一些主要的属性与方法:

重载的构造函数:CountdownEvent的构造函数接受一个整型值,表示事件句柄最初必须的信号数。

InitialCount属性:这个属性正是构造函数接收的参数所设置的值。

CurrentCount属性:事件解除阻塞所必需的剩余信号数。

AddCount方法:将CurrentCount属性的值加1。

Single方法:给出一个信号,这将是CurrentCount的值减1。

class Program

{

    static void Main()

    {

        var customers = Enumerable.Range(1, 20);

 

        using (var countdown = new CountdownEvent(customers.Count()))

        {

            foreach (var customer in customers)

            {

                int currentCustomer = customer;

                ThreadPool.QueueUserWorkItem(delegate

                {

                    BuySomeStuff(currentCustomer);

                    countdown.Signal();

                    //for test

                    Console.WriteLine(" CountdownEvent:" + countdown.CurrentCount);

                });

            }

            countdown.Wait();

        }

        //主线程继续执行

        Console.WriteLine("All Customers finished shopping...");

        Console.ReadKey();

    }

 

    static void BuySomeStuff(int customer)

    {

        // Fake work

        Thread.SpinWait(200000000);

        Console.Write("Customer {0} finished", customer);

    }

}

代码输出(每次运行子线程执行顺序可能不同):

Customer 1 finished CountdownEvent:19

Customer 2 finished CountdownEvent:18

Customer 3 finished CountdownEvent:17

Customer 4 finished CountdownEvent:16

Customer 5 finished CountdownEvent:15

Customer 6 finished CountdownEvent:14

Customer 7 finished CountdownEvent:13

Customer 8 finished CountdownEvent:12

Customer 9 finished CountdownEvent:11

Customer 10 finished CountdownEvent:10

Customer 11 finished CountdownEvent:9

Customer 12 finished CountdownEvent:8

Customer 13 finished CountdownEvent:7

Customer 14 finished CountdownEvent:6

Customer 15 finished CountdownEvent:5

Customer 16 finished CountdownEvent:4

Customer 17 finished CountdownEvent:3

Customer 18 finished CountdownEvent:2

Customer 20 finished CountdownEvent:1

Customer 19 finished CountdownEvent:0

All Customers finished shopping...

代码中主线程中调用Wait方法来等待子线程完成(即CountdownEvent的CurrentCount属性变为0)。

CountdownEvent内部通过ManualResetEventSlim与Interlocked实现,ManualResetEventSlim用于实现事件等待句柄,而Interlocked用于线程计数。

Barrier

       这个类的作用很明确,使用很简单,首先介绍其中几个比较重要的属性与方法,之后直接进入示例:

构造函数:两个重载共同的参数是需要被同步的线程的数量,参数较多的一个重载第二个参数接收一个Action<Barrier>类型对象,表示所有线程达到同一阶段后执行的方法。

ParticipantCount属性:即构造函数中设置的需要被同步的线程的数量。

SignalAndWait方法:发出参与者已达到Barrier的信号,等待所有其他参与者也达到Barrier。

场景如下:Charlie、Mac、Dennis三个人相约在途中的加油站会合后一同前往西雅图。我们用Barrier来模拟这个场景,重要的是在加油站会和这一点进行同步。

代码:

class Program

{

    static Barrier sync;

    static CancellationToken token;

 

    static void Main(string[] args)

    {

        var source = new CancellationTokenSource();

        token = source.Token;

        sync = new Barrier(3);

 

        var charlie = new Thread(() => DriveToBoston("Charlie", TimeSpan.FromSeconds(1)));

        charlie.Start();

        var mac = new Thread(() => DriveToBoston("Mac", TimeSpan.FromSeconds(2)));

        mac.Start();

        var dennis = new Thread(() => DriveToBoston("Dennis", TimeSpan.FromSeconds(3)));

        dennis.Start();

 

        //source.Cancel();

 

        charlie.Join();

        mac.Join();

        dennis.Join();

 

        Console.ReadKey();

    }

 

    static void DriveToBoston(string name, TimeSpan timeToGasStation)

    {

        try

        {

            Console.WriteLine("[{0}] Leaving House", name);

 

            // Perform some work

            Thread.Sleep(timeToGasStation);

            Console.WriteLine("[{0}] Arrived at Gas Station", name);

 

            // Need to sync here

            sync.SignalAndWait(token);

 

            // Perform some more work

            Console.WriteLine("[{0}] Leaving for Boston", name);

        }

        catch (OperationCanceledException)

        {

            Console.WriteLine("[{0}] Caravan was cancelled! Going home!", name);

        }

    }

}

执行结果(同样每次运行子线程执行顺序可能不同):

[Charlie] Leaving House

[Mac] Leaving House

[Dennis] Leaving House

[Charlie] Arrived at Gas Station

[Mac] Arrived at Gas Station

[Dennis] Arrived at Gas Station

[Dennis] Leaving for Boston

[Mac] Leaving for Boston

[Charlie] Leaving for Boston

另外可以取消代码中的注释,观察多线程取消的效果。

其它.NET4.0新增的线程类

SpinWait

从.NET Framework 4开始,当线程必须等待发生某个事件发出信号时或需要满足某个条件时,可以使用System.Threading.SpinWait结构,前提是实际等待 时间预计会少于通过使用等待句柄或通过其他方式阻塞当前线程所需要的等待时间,否则SpinWait空转导致的CPU开销会影响其它进程。通过使 用 SpinWait,可以指定在一个较短的时段内边等待边旋转,然后只有在相应的条件在指定时间内无法得到满足的情况下放弃旋转。

其它小话题:

Thread.Interrupt方法可用于使线程跳出阻塞状态(如等待访问同步代码区域)。Thread.Interrupt 还可用于使线程跳出 Thread.Sleep 等操作。

分类:  基础知识

本文转自快乐就好博客园博客,原文链接:http://www.cnblogs.com/happyday56/p/3823078.html,如需转载请自行联系原作者
相关文章
|
16天前
|
数据采集 Java 数据处理
Python实用技巧:轻松驾驭多线程与多进程,加速任务执行
在Python编程中,多线程和多进程是提升程序效率的关键工具。多线程适用于I/O密集型任务,如文件读写、网络请求;多进程则适合CPU密集型任务,如科学计算、图像处理。本文详细介绍这两种并发编程方式的基本用法及应用场景,并通过实例代码展示如何使用threading、multiprocessing模块及线程池、进程池来优化程序性能。结合实际案例,帮助读者掌握并发编程技巧,提高程序执行速度和资源利用率。
22 0
|
17天前
|
存储 Linux API
【Linux进程概念】—— 操作系统中的“生命体”,计算机里的“多线程”
在计算机系统的底层架构中,操作系统肩负着资源管理与任务调度的重任。当我们启动各类应用程序时,其背后复杂的运作机制便悄然展开。程序,作为静态的指令集合,如何在系统中实现动态执行?本文带你一探究竟!
【Linux进程概念】—— 操作系统中的“生命体”,计算机里的“多线程”
|
1月前
|
Python
python3多线程中使用线程睡眠
本文详细介绍了Python3多线程编程中使用线程睡眠的基本方法和应用场景。通过 `time.sleep()`函数,可以使线程暂停执行一段指定的时间,从而控制线程的执行节奏。通过实际示例演示了如何在多线程中使用线程睡眠来实现计数器和下载器功能。希望本文能帮助您更好地理解和应用Python多线程编程,提高程序的并发能力和执行效率。
54 20
|
1月前
|
安全 Java C#
Unity多线程使用(线程池)
在C#中使用线程池需引用`System.Threading`。创建单个线程时,务必在Unity程序停止前关闭线程(如使用`Thread.Abort()`),否则可能导致崩溃。示例代码展示了如何创建和管理线程,确保在线程中执行任务并在主线程中处理结果。完整代码包括线程池队列、主线程检查及线程安全的操作队列管理,确保多线程操作的稳定性和安全性。
|
2月前
|
Java Linux 调度
硬核揭秘:线程与进程的底层原理,面试高分必备!
嘿,大家好!我是小米,29岁的技术爱好者。今天来聊聊线程和进程的区别。进程是操作系统中运行的程序实例,有独立内存空间;线程是进程内的最小执行单元,共享内存。创建进程开销大但更安全,线程轻量高效但易引发数据竞争。面试时可强调:进程是资源分配单位,线程是CPU调度单位。根据不同场景选择合适的并发模型,如高并发用线程池。希望这篇文章能帮你更好地理解并回答面试中的相关问题,祝你早日拿下心仪的offer!
52 6
|
2月前
|
消息中间件 调度
如何区分进程、线程和协程?看这篇就够了!
本课程主要探讨操作系统中的进程、线程和协程的区别。进程是资源分配的基本单位,具有独立性和隔离性;线程是CPU调度的基本单位,轻量且共享资源,适合并发执行;协程更轻量,由程序自身调度,适合I/O密集型任务。通过学习这些概念,可以更好地理解和应用它们,以实现最优的性能和资源利用。
86 11
|
3月前
|
NoSQL Redis
单线程传奇Redis,为何引入多线程?
Redis 4.0 引入多线程支持,主要用于后台对象删除、处理阻塞命令和网络 I/O 等操作,以提高并发性和性能。尽管如此,Redis 仍保留单线程执行模型处理客户端请求,确保高效性和简单性。多线程仅用于优化后台任务,如异步删除过期对象和分担读写操作,从而提升整体性能。
89 1
|
3月前
|
算法 调度 开发者
深入理解操作系统:进程与线程的管理
在数字世界的复杂编织中,操作系统如同一位精明的指挥家,协调着每一个音符的奏响。本篇文章将带领读者穿越操作系统的幕后,探索进程与线程管理的奥秘。从进程的诞生到线程的舞蹈,我们将一起见证这场微观世界的华丽变奏。通过深入浅出的解释和生动的比喻,本文旨在揭示操作系统如何高效地处理多任务,确保系统的稳定性和效率。让我们一起跟随代码的步伐,走进操作系统的内心世界。
|
3月前
|
消息中间件 Unix Linux
【C语言】进程和线程详解
在现代操作系统中,进程和线程是实现并发执行的两种主要方式。理解它们的区别和各自的应用场景对于编写高效的并发程序至关重要。
106 6
|
1月前
|
Linux
Linux编程: 在业务线程中注册和处理Linux信号
本文详细介绍了如何在Linux中通过在业务线程中注册和处理信号。我们讨论了信号的基本概念,并通过完整的代码示例展示了在业务线程中注册和处理信号的方法。通过正确地使用信号处理机制,可以提高程序的健壮性和响应能力。希望本文能帮助您更好地理解和应用Linux信号处理,提高开发效率和代码质量。
49 17

相关实验场景

更多