CLR 线程同步

CLR 基元线程同步构造

《CLR via C#》到了最后一部分,这一章重点在于线程同步,多个线程同时访问共享数据时,线程同步能防止数据虽坏。之所以要强调同时,是因为线程同步问题其实就是计时问题。为构建可伸缩的、响应灵敏的应用程序,关键在于不要阻塞你拥有的线程,使它们能用于(和重用于)执行其他任务。

不需要线程同步是最理想的情况,因为线程同步存在许多问题:

  • 1 第一个问题是,它比较繁琐,很容易出错。
  • 2 第二个问题是,它们会损坏性能。获取和释放锁是需要时间的,因为要调用一些额外的方法,而且不同的CPU 必须进行协调,以决定哪个线程先取得锁。让机器中的CPU 以这种方式互相通信,会对性能造成影响。

    添加锁后速度会慢下来,具体慢多少要取决于所选的锁的种类。即便是最快的锁,也会造成 方法 数倍地慢于没有任何锁的版本。

  • 3 第三个问题在于,它们一次只允许一个线程访问资源。这是锁的全部意义之所在,但也是问题之所在,因为阻塞一个线程会造成更多的线程被创建。

线程同步如此的不好,应该如何在设计自己的应用时,尽量避免线程同步呢?

  • 具体就是避免使用像静态字段这样的共享数据。可试着使用值类型,因为它们总是被复制,每个线程操作的都是它自己的副本。
  • 多个线程同时共享数据进行只读访问是没有任何问题的。

1 类库和线程安全

Microsoft 的 Framework Class Library (FCL)保证所有静态方法都是线程安全的。另一方面,FCL 不保证实列方法是线程安全的。Jeffery Richter 建议你自己的类库也遵循这个模式。这个模式有一点要注意:如果实例方法的目的是协调线程,则实例方法应该是线程安全的。

注意:使一个方法线程安全,并不是说它一定要在内部获取一个线程同步锁。线程安全的方法意味着在两个线程试图同时访问数据时,数据不会被破坏。例如:System.Math 类的一个静态方法 Max。

2 基元用户模式和 内核模式构造

基元(primitive)是指可以在代码中使用的最简单的构造。有两种基元构造:用户模式(user-mode)和 内核模式(kernel-mode)。尽量使用基元用户模式构造,它们的速度要显著快于内核模式构造。因为它们使用了特殊 CPU 指令来协调线程。这意味着协调是在硬件中发生的(所以才这么快)。

但这意味着 Windows 系统永远检测不到一个线程在基元用户模式的构造上阻塞了。由于在用户模式的基元构造上阻塞的线程池不认为已阻塞,所以线程池不会创建新的线程来替换这种临时阻塞的线程。此外,这些CPU 指令只阻塞线程相当短的时间。

3 用户模式构造

CLR 保证对以下数据类型的变量读写是原子性的:Boolean,Char,(S)Byte,(U)Int16,(U)Int32,(U)IntPtr,Single以及引用类型。

举个列子:

internal static class SomeTyoe{    public static Int32 x = 0;}

如果一个线程执行这一行代码:

SomeType.x = 0x01234567;

x 变量会一次性(原子性)地从0x00000000 变成0x01234567。另一个线程不可能看到处于中间状态的值。假定上述SomeType 类中的x 字段是一个Int64 ,那么当一个线程执行以下代码时:

SomeType.x = 0x0123456789abcdef

另一个线程可能查询x ,并得到0x0123456700000000 或 0x0000000089abcdef 值,因为读取和写入操作不是原子性的。

虽然变量的原子访问可保证读取或写入操作一次性完成,但由于编译器和CPU 的优化,不保证操作什么时候发生。本节讨论的基元用户模式构造,用于规划好这些原子性读取/写入操作的时间。 此外,这些构造还可强制对(U)Int64 和 Double 类型的变量进行原子性的、规划好了时间的访问。

有两种基于用户模式线程同步构造。

  • 1 易变构造:在特定的时间,它在包含一个简单数据类型的变量上 执行 原子性的读 写操作。
  • 2 互锁构造:在特定的时间,它在包含一个简单数据类型的变量上 执行 原子性的读 写操作。

所有易变 和 互锁构造都要求传递对包含简单数据类型的一个变量的引用(内存地址)。

3.1 易变构造 Volatile.Read 和 Volatile.Write

C# 对易变字段的支持

C# 编译器提供了 volatile 关键字,它可应用于以下任何类型的静态 或 实例字段:Boolean,(S)Byte,(U)Int16,(U)Int32,(U)IntPtr,Single和 Char。还可将 volatile 关键字应用于引用类型的字段,以及基础类型为 (S)Byte,(U)Int16,(U)Int32 的任何枚举字段。

JIT 编译器确保对易变字段的所有访问都是易变读取或写入的方式执行,不必显示调用 Volatile 的静态 Read 或 Write 方法。另外,volatile 关键字告诉C# 和 JIT 编译器不将字段缓存到CPU 的寄存器中,确保字段的所有读写操作都在 RAM 中进行。

下面是Volatile.Write 方法和 Volatile.Read 方法的使用。

internal sealed class ThreadsSharingData {    private Int32 m_flag = 0;    private Int32 m_value = 0;    // This method is executed by one thread    public void Thread1() {        // Note: 5 must be written to m_value before 1 is written to m_flag        m_value = 5;        Volatile.Write(ref m_flag, 1);    }    // This method is executed by another thread    public void Thread2() {        // Note: m_value must be read after m_flag is read        if (Volatile.Read(ref m_flag) == 1)            Console.WriteLine(m_value);    }}
  • Volatile.Write 方法强迫location 中的值在调用时写入。此外,按照编码顺序,之前的加载和存储操作必须在调用 Volatile.Write 之前 发生。
  • Volatile.Read 方法强迫location 中的值在调用时读取。此外,按照编码顺序,之后的加载和存储操作必须在调用 Volatile.Read 之后 发生。

C# 对易变字段的支持

为了简化编程,C# 编译器提供了 Volatile 关键字,它可应用于以下任何类型的静态或实例字段:Boolean,(S)Byte,(U)Int16,(U)Int32,(U)IntPtr,Single 和 Char。还可以将 Volatile 关键字应用于引用类型的字段,以及基础类型为(S)Byte,(U)Int16 或 (U)Int32 的任何枚举字段。

volatile 关键字告诉 C# 和 JIT 编译器不将字段缓存到 CPU 的寄存器中,确保字段的所有读写操作都在 RAM 中进行。

用 volatile 引起的不好事情:

  • 如:m_amount = m_amount + m_amount;

    //假定m_amount 是类中定义的一个volatile 字段。编译器必须生成代码将m_amount 读入一个寄存器,再把它读入另一个寄存器,将两个寄存器加到一起,再将结果写回 m_amount 字段。但最简单的方式是将它的所有位都左移1 位。

  • 另外,C# 不支持以引用的方式将 volatile 字段传给方法。

3.2 互锁构造

本节将讨论静态System.Threading.Interlocked 类提供的方法。InterLocked 类中的每个方法都执行一次原子读取 以及 写入操作。此外,Interlocked 的所有方法都建立了完整的内存栅栏(memory fence)。也就是说,调用某个 Interlocked 方法之前的任何变量写入都在这个InterLocked 方法调用之前执行。而这个调用之后的任何变量读取都在这个调用之后读取。

作者很喜欢用 Interlocked 的方法,它们相当快,不阻塞任何线程。

AsyncCoordinator 可协调异步操作。作者给了个例子。

internal sealed class MultiWebRequests {    // This helper class coordinates all the asynchronous operations    private AsyncCoordinator m_ac = new AsyncCoordinator();    // Set of web servers we want to query & their responses (Exception or Int32)    // NOTE: Even though multiple could access this dictionary simultaneously,    // there is no need to synchronize access to it because the keys are    // read•only after construction    private Dictionary<String, Object> m_servers = new Dictionary<String, Object> {        { "http://Wintellect.com/", null },        { "http://Microsoft.com/", null },        { "http://1.1.1.1/", null }    };        public MultiWebRequests(Int32 timeout = Timeout.Infinite) {        // Asynchronously initiate all the requests all at once        var httpClient = new HttpClient();        foreach (var server in m_servers.Keys) {            m_ac.AboutToBegin(1);            httpClient.GetByteArrayAsync(server).            ContinueWith(task => ComputeResult(server, task));        }        // Tell AsyncCoordinator that all operations have been initiated and to call        // AllDone when all operations complete, Cancel is called, or the timeout occurs        m_ac.AllBegun(AllDone, timeout);    }        private void ComputeResult(String server, Task<Byte[]> task) {    Object result;    if (task.Exception != null) {        result = task.Exception.InnerException;    } else {        // Process I/O completion here on thread pool thread(s)        // Put your own compute•intensive algorithm here...        result = task.Result.Length; // This example just returns the length    }    // Save result (exception/sum) and indicate that 1 operation completed    m_servers[server] = result;    m_ac.JustEnded();}// Calling this method indicates that the results don‘t matter anymorepublic void Cancel() { m_ac.Cancel(); }// This method is called after all web servers respond,// Cancel is called, or the timeout occursprivate void AllDone(CoordinationStatus status) {    switch (status) {        case CoordinationStatus.Cancel:            Console.WriteLine("Operation canceled.");            break;        case CoordinationStatus.Timeout:            Console.WriteLine("Operation timed•out.");            break;        case CoordinationStatus.AllDone:            Console.WriteLine("Operation completed; results below:");            foreach (var server in m_servers) {                Console.Write("{0} ", server.Key);                Object result = server.Value;                if (result is Exception) {                    Console.WriteLine("failed due to {0}.", result.GetType().Name);                } else {                    Console.WriteLine("returned {0:N0} bytes.", result);                }            }            break;        }    }}
  • 1 调用 AsyncCoordinator 的 AboutToBegin 方法,向它传递要发出的请求数量。
  • 2 然后 调用 HttpClient 的GetByteArrayAsync 来初始化请求。在返回的 Task 上调用 ContinueWith ,确保在服务器上有了响应之后,我的 ComputeResult 方法可通过许多线程池线程并发处理结果。
  • 3 对Web 服务器的所有请求都发出之后,将调用 AsyncCoordinator 的 AllBegun 方法,向它传递要在所有操作完成后,执行的方法(AllDone)以及一个超时值。
  • 4 每收到一个Web 服务器响应,线程池线程都会调用 MultiWebRequests 的 ComputeResult 方法。该方法处理服务器返回的字节(或者发生的任何错误),将结果存到字典集合中。
  • 5 存好每个结果之后,会调用 AsyncCoordinator 的 JustEnded 方法,使AsyncCoordintor 对象只读一个操作已经完成。
  • 6 所有操作完成后,AsyncCoordinator 会调用AllDone 方法处理来自所有Web 服务器的结果。
  • 7 调用 AllDone 方法的是 哪个线程?

    一般情况 执行 AllDone 方法的线程就是获取最后一个 Web服务器响应的哪个线程池线程。

    但如果发生超时或取消,调用 AllDone 的线程就是 AsyncCoordinator 通知超时的 那个线程池线程,或是调用 Cancel 方法的那个线程。也有可能 AllDone 由发出 Web服务器请求的那个线程调用—— 如果最后一个请求在调用AllBegun 之前完成。

  • 8 在调用 AllBegun 方法时 存在竟态条件,因为以下事情可能恰好同时发生:
    • 1 全部操作结束
    • 2 发生超时
    • 3 调用Cancel
    • 4 调用 AllBegun

    这时 AsyncCoordinator 会选择1 个赢家和 3 个输家,确保AllDone 方法不被多次调用。赢家是通过 传给 AllDone 的 status 实参来识别的。

我们来看一看 AsyncCoordinator 类的具体工作原理。AsyncCoordinator 类封装了所有线程协调(合作)逻辑。它用 Interlocked 提供的方法来操作一切,确保代码以极快的速度允许,同时没有线程会被阻塞。

internal sealed class AsyncCoordinator {    private Int32 m_opCount = 1; // Decremented when AllBegun calls JustEnded    private Int32 m_statusReported = 0; // 0=false, 1=true    private Action<CoordinationStatus> m_callback;    private Timer m_timer;        // This method MUST be called BEFORE initiating an operation    public void AboutToBegin(Int32 opsToAdd = 1) {        Interlocked.Add(ref m_opCount, opsToAdd);    }        // This method MUST be called AFTER an operation’s result has been processed    public void JustEnded() {        if (Interlocked.Decrement(ref m_opCount) == 0)            ReportStatus(CoordinationStatus.AllDone);    }        // This method MUST be called AFTER initiating ALL operations    public void AllBegun(Action<CoordinationStatus> callback,        Int32 timeout = Timeout.Infinite) {        m_callback = callback;        if (timeout != Timeout.Infinite)            m_timer = new Timer(TimeExpired, null, timeout, Timeout.Infinite);        JustEnded();    }        private void TimeExpired(Object o) { ReportStatus(CoordinationStatus.Timeout); }    public void Cancel() { ReportStatus(CoordinationStatus.Cancel); }        private void ReportStatus(CoordinationStatus status) {        // If status has never been reported, report it; else ignore it        if (Interlocked.Exchange(ref m_statusReported, 1) == 0)            m_callback(status);    }}

这个类最重要的字段就是 m_opCount 字段,用于跟踪仍在进行的异步操作的数量。每个异步操作开始前都会调用 AboutToBegin。该方法调用 Interlocked.Add,以原子方式将传给它的数字加到 m_opCount 字段上。处理好Web 服务器的响应后会调用 JustEnded 。该方法调用Interlocked.Decrement,以原子方式从m_opCount 上减1。无论哪个线程恰好将 m_opCount 设为0,都由它调用ReportStatus。

ReportStatus 方法对全部操作结束、发生超时和调用Cancel 时可能发生的竟态条件进行仲裁。ReportStatus 必须确保其中只有一个条件胜出,确保 m_callback 方法只被调用一次。

3.3 实现简单的自旋锁

在多线程处理中,它意味着让一个线程暂时“原地打转”,以免它跑去跟另一个线程竞争资源。它会占用CPU 资源

Interlocked 的方法很好用,但主要用于操作 Int32 值。如果需要原子性地操作类对象中的一组字段,又该怎么办? 这需要采取一个办法阻止所有线程,只允许其中一个进入对字段进行操作的代码区域,可以使用 Interlocked 的方法构造一个线程同步块:

internal struct SimpleSpinLock {    private Int32 m_ResourceInUse; // 0=false (default), 1=true    public void Enter() {        while (true) {            // Always set resource to in•use            // When this thread changes it from not in•use, return            if (Interlocked.Exchange(ref m_ResourceInUse, 1) == 0) return;            // Black magic goes here...        }    }    public void Leave() {        // Set resource to not in-use        Volatile.Write(ref m_ResourceInUse, 0);    }}

下面的类展示了如何使用 SimpleSpinLock.

public sealed class SomeResource {    private SimpleSpinLock m_sl = new SimpleSpinLock();    public void AccessResource() {        m_sl.Enter();        // Only one thread at a time can get in here to access the resource...        m_sl.Leave();    }}

这种锁的最大问题在于,在存在对锁的竞争的前提下,会造成线程“自旋”。这个“自旋”会浪费宝贵的CPU 时间,阻止CPU 做其他更有用的工作。因此自旋锁只应该保护那些会执行得非常快的代码区域。这种锁一般不要在单 CPU 机器上使用。

为了解决线程“自旋” 问题,许多自旋锁内部有一些额外的逻辑。FCL 提供了一个名为 System.Threading.SpinWait 的结构,封装了人们关于这种 黑科技 的最新研究。

FCL 还包含一个 System.Threading.SpinLock 结构,它和 SimpleSpinLock 类似,只是使用了 SpinWait 结构来增强性能。 SpinLock 提供了超时支持。它们都是值类型。

3.4 Interlocked Anything 模式

使用 Interlocked.CompareExchagne 方法以原子方式在 Int32 上执行任何操作。 事实上,由于 Interlocked.CompareExchange 提供了其他重载版本,能操作 Int64 , Single, Double ,Object 和 泛型引用类型,所以该模式适合所有这些类型。

public static Int32 Maximum(ref Int32 target, Int32 value) {    Int32 currentVal = target, startVal, desiredVal;    // Don‘t access target in the loop except in an attempt    // to change it because another thread may be touching it    do {        // Record this iteration‘s starting value        startVal = currentVal;        // Calculate the desired value in terms of startVal and value        desiredVal = Math.Max(startVal, value);        // NOTE: the thread could be preempted here!        // if (target == startVal) target = desiredVal        // Value prior to potential change is returned        currentVal = Interlocked.CompareExchange(ref target, desiredVal, startVal);        // If the starting value changed during this iteration, repeat    } while (startVal != currentVal);        // Return the maximum value when this thread tried to set it    return desiredVal;}

当这个操作进行时,其他线程可能更改 target。虽然几率很小,但仍是有可能发生的。如果真的发生,desiredVal 的值就是基于存储在 startVal 中的旧值而获得的,而非基于 target 的新值。这时就不应该更改 target 。我们用 interlocked.CompareExchange 方法确保没有其他线程更改 target 的前提下 将target 的值改为 desiredVal。

4 内核模式

内核模式的构造更慢,有两个原因:

  • 1 它们要求 Windows 操作系统自身的配合
  • 2 在内核对象上调用的每个方法都造成调用线程从托管代码转换为 本机用户模式代码。再转换为本机内核模式代码。

但内核模式的构造具备基元用户模式构造不具备的优点。

  • 1 内核模式的构造检测到一个资源上的竞争时,Windows 会阻塞输掉的线程,使它不占着一个 CPU “自旋”,无畏地浪费处理器资源。
  • 2 内核模式的构造可实现本机(native)和托管(managed)线程相互之间的同步。
  • 3 内核模式的构造可同步在同一台机器的不同进程中运行的线程。
  • 4 内核模式的构造可应用安全性设置,为防止未经授权的账户访问它们。
  • 5 在内核模式的构造上阻塞的线程可指定超时值。指定时间内访问不到希望的资源,线程就可以解除阻塞并执行其他任务。

内核模式基元构造一共两种:事件 和 信号量。至于其他内核模式构造,比如 互斥体,则是在两个基元构造上构建的。

System.Threading 命名空间提供了一个名为 WaitHandle 抽象基类,它包装了一个 Windows 内核对象句柄。在一个内核模式 的构造上调用的每个方法都代表一个完整的内存栅栏。WaitHandle 基类内部有一个 SafeWaitHandle 字段,它容纳了一个 Win32 内核对象句柄。这个字段是在构造一个具体的WaitHandle 派生类时初始化的。

AutoResetEvent , ManualResetEvent,Semaphore 和 Mutex 类 都派生自 WaitHandle ,它们继承了 WaitHandle 的方法和行为。

using System;using System.Threading;?public static class Program {    public static void Main() {        Boolean createdNew;        // Try to create a kernel object with the specified name        using (new Semaphore(0, 1, "SomeUniqueStringIdentifyingMyApp", out createdNew)) {            if (createdNew) {                // This thread created the kernel object so no other instance of this                // application must be running. Run the rest of the application here...            } else {                // This thread opened an existing kernel object with the same string name;                // another instance of this application must be running now.                // There is nothing to do in here, let‘s just return from Main to terminate                // this second instance of the application.            }        }    }}

上述代码使用的是 Semaphore,但换成EventWaitHandle 或 Mutex 一样也可以,因为我并没有真正使用对象提供的线程同步行为。但我利用了在创建任何种类的内核对象时由Windows 内核提供的一些线程同步行为。当两个进程中的线程都尝试创建具有相同字符串名称的一个Semaphore,Windows 内核确保只有一个线程实际地创建具有指定名称的内核对象。创建对象的线程会将它的 createdNew 变量设为true。

4.1 Event 构造

事件(event)其实只是由内核维护的 Boolen 变量。事件为 false, 在事件上等待的线程就阻塞;事件为 true ,就解除阻塞。有两种事件,即自动重置事件和 手动重置事件。自动重置事件为 true 时,它只唤醒一个阻塞的线程。手动重置事件为 true时,它解除正在等待它的所有线程的阻塞,因为内核不将事件自动重置回false。必须手动重置回false。

public class EventWaitHandle : WaitHandle {    public Boolean Set(); // Sets Boolean to true; always returns true    public Boolean Reset(); // Sets Boolean to false; always returns true}?public sealed class AutoResetEvent : EventWaitHandle {    public AutoResetEvent(Boolean initialState);}?public sealed class ManualResetEvent : EventWaitHandle {    public ManualResetEvent(Boolean initialState);}

可用自动重置事件轻松创建线程同步锁,它的行为和前面展示的 SimpleSpinLock 类似:

internal sealed class SimpleWaitLock : IDisposable {    private readonly AutoResetEvent m_available;        public SimpleWaitLock() {        m_available = new AutoResetEvent(true); // Initially free    }        public void Enter() {        // Block in kernel until resource available        m_available.WaitOne();    }        public void Leave() {        // Let another thread access the resource        m_available.Set();    }        public void Dispose() { m_available.Dispose(); }}

和使用 SimlpeSpinLock 时完全一样的方式使用 SimpleWaitLock,表面上完全相同,但是两个锁的性质截然不同。锁上面没有竞争的时候, SimpleWaitLock 比 SimpleSpinLock 慢得多,因为对 SimpleWaitLock 的 Enter 和 Leave 方法的每个调用都强迫线程从托管代码转换为内核代码。再转换回来。但在存在竞争的时候,输掉的线程会被内核阻塞,不会在那里自旋,从而不浪费CPU 事件。

Semaphore 构造

信号量(semaphore)其实就是内核维护的Int32 变量。信号量为 0 时,在信号量上等待的线程会阻塞;信号量大于 0 时解除阻塞。在信号量上等待的线程解除阻塞时,内核自动从信号量 的计数中减 1。信号量还关联了一个最大 Int32 值,当前计数绝不允许超过最大计数。下面展示了 Semaphore 类的样子:

public sealed class Semaphore : WaitHandle {    public Semaphore(Int32 initialCount, Int32 maximumCount);    public Int32 Release(); // Calls Release(1); returns previous count    public Int32 Release(Int32 releaseCount); // Returns previous count}

总结一下这三种内核构造基元的行为:

  • 多个线程在一个自动重置事件上等待时,设置事件只导致一个线程被解除阻塞。
  • 多个线程在一个手动重置事件上等待时,设置事件导致所有线程被解除阻塞。
  • 多个线程在一个信号量上等待时,释放信号量导致 releaseCount 个线程被解除阻塞(

    releaseCount 是传给 Semaphore 的 Release 方法的实参)。

自动重置事件和信号量的区别是:

可以在一个自动重置事件上连续多次调用 Set,同时仍然只有一个线程解除阻塞。相反,在一个信号量上连续多次调用Release ,会使它的内部计数一直递增,这可能解除大量线程的阻塞。顺便说一句,如果在一个信号量上多次调用Release ,会导致它的计数超过最大计数,这时Release 会抛出一个 SemaphoreFullException。

可像下面这样用信号量重新实现 SimpleWaitLock,允许多个线程并发访问一个资源。

public sealed class SimpleWaitLock : IDisposable {    private readonly Semaphore m_available;        public SimpleWaitLock(Int32 maxConcurrent) {        m_available = new Semaphore(maxConcurrent, maxConcurrent);    }        public void Enter() {        // Block in kernel until resource available        m_available.WaitOne();    }        public void Leave() {        // Let another thread access the resource        m_available.Release(1);    }    public void Dispose() { m_available.Close(); }}

Mutex Constructs

互斥体(mutex)代表一个互斥的锁。它的工作方式和 AutoResetEvent (或者技术为1 的 Semaphore )相似,三者都是一次只释放一个正在等待的线程。下面是 Mutex 类的样子:

public sealed class Mutex : WaitHandle {    public Mutex();    public void ReleaseMutex();}

互斥体有一些额外的逻辑,这造成它们比其他构造更复杂。一个是记录被哪个线程ID记录了,一个是记录被线程调用的次数。

1 Mutex 对象会查询调用线程的 Int32 ID ,记录是哪个线程获得了它。一个线程调用 ReleaseMutex 时,Mutex 确保调用线程就是获取 Mutex 的那个线程。如诺不然,Mutex 对象的状态就不会改变,而ReleaseMutex 会抛出一个 System.ApplicationException。另外,拥有Mutex 的线程因为任何原因而终止,在Mutex 上等待的某个线程会因为抛出 System.Threading.AbandonedMutexException 异常而被唤醒。该异常通常会成为未处理的异常,从而终止整个进程。

2 Mutex 对象维护着一个递归计数,指出拥有该 Mutex 的线程拥有了它多少次。如果一个线程当前拥有一个 Mutex,而后线程再次在 Mutex 上等待,计数就会递增,这个线程允许继续运行。线程调用 ReleaseMutex 将导致计数递减。只有计数变成 0,另一个线程才能为该 Mutex 的所有者。

Mutex 对象需要更多的内存来容纳额外的线程 ID 和计数信息。Mutex 必须维护这些信息,使锁变得更慢。

通常当一个方法获取了一个锁,然后调用也需要这个锁的另一个方法,就需要一个递归锁。下面的代码要释放两次,其他线程才能获得该锁。代码如下所示。

internal class SomeClass : IDisposable {    private readonly Mutex m_lock = new Mutex();        public void Method1() {        m_lock.WaitOne();        // Do whatever...        Method2(); // Method2 recursively acquires the lock        m_lock.ReleaseMutex();    }        public void Method2() {        m_lock.WaitOne();        // Do whatever...        m_lock.ReleaseMutex();    }

    public void Dispose() { m_lock.Dispose(); }}

如果SomeClass 使用一个 AutoResetEvent 而不是 Mutex,线程在调用Method2 的WaitOne 方法时会阻塞。

如果需要递归锁,可以使用一个 AutoResetEvent 来简单创建一个:

internal sealed class RecursiveAutoResetEvent : IDisposable {    private AutoResetEvent m_lock = new AutoResetEvent(true);    private Int32 m_owningThreadId = 0;    private Int32 m_recursionCount = 0;        public void Enter() {        // Obtain the calling thread‘s unique Int32 ID        Int32 currentThreadId = Thread.CurrentThread.ManagedThreadId;        // If the calling thread owns the lock, increment the recursion count        if (m_owningThreadId == currentThreadId) {            m_recursionCount++;            return;        }        // The calling thread doesn‘t own the lock, wait for it        m_lock.WaitOne();        // The calling now owns the lock, initialize the owning thread ID & recursion count        m_owningThreadId = currentThreadId;        m_recursionCount = 1;    }        public void Leave() {        // If the calling thread doesn‘t own the lock, we have an error        if (m_owningThreadId != Thread.CurrentThread.ManagedThreadId)            throw new InvalidOperationException();        // Subtract 1 from the recursion count        if (--m_recursionCount == 0) {            // If the recursion count is 0, then no thread owns the lock            m_owningThreadId = 0;            m_lock.Set(); // Wake up 1 waiting thread (if any)        }    }        public void Dispose() { m_lock.Dispose(); }}

虽然 RecursiveAutoResetEvent 类的行为和 Mutex 类完全一样,但在一个线程试图递归取锁时,它大的性能会好很多,因为现在跟踪线程所有权和递归的都是托管代码。只有在一次获取AutoResetEvent,或者最后把它放弃给其他线程时,线程才需要从托管代码转为内核代码。

原文地址:https://www.cnblogs.com/mingjie-c/p/11761390.html

时间: 2024-10-19 22:57:50

CLR 线程同步的相关文章

.net中的线程同步基础(搬运自CLR via C#)

线程安全 此类型的所有公共静态(Visual Basic 中为 Shared)成员对多线程操作而言都是安全的.但不保证任何实例成员是线程安全的. 在MSDN上经常会看到这样一句话.表示如果程序中有n个线程调用这个方法,那么这n个线程都是安全的, 但是实例成员就不能保证了. 比如Math.Max方法,不管有多少个线程调用,都不会出现线程不安全的情况. 列举一个由于多线程引起的数据不安全. static void Main(string[] args) { Stopwatch watch = new

Clr Via C#读书笔记----基元线程同步构造

重点在于多个线程同时访问,保持线程的同步. 线程同步的问题: 1,线程同步比较繁琐,而且容易写错. 2,线程同步会损害性能,获取和释放一个锁是需要时间. 3,线程同步一次只允许一个线程访问资源. 类库和线程安全, 一个线程安全的发那个发意味着两个线程试图同时访问数据时,数据不会被破坏. 基元用户模式和内核模式构造 基元:指代码中最简单的构造,有两种基元构造:用户模式和内核模式. 1,基元用户模式比基元内核模式速度要快,因为直接使用特殊的cpu指令来协调线程,在硬件中发生的. 2,基元用户模式构造

【C#进阶系列】29 混合线程同步构造

上一章讲了基元线程同步构造,而其它的线程同步构造都是基于这些基元线程同步构造的,并且一般都合并了用户模式和内核模式构造,我们称之为混合线程同步构造. 在没有线程竞争时,混合线程提供了基于用户模式构造所具备的性能优势,而多个线程竞争一个构造时,混合线程通过基元内核模式的构造来提供不“自旋”的优势. 那么接下来就是个简单的混合线程同步构造的例子,可与上一章最后的那些例子相比较: public class SimpleHybridLock : IDisposable { private Int32 m

C#编程总结(三)线程同步

C#编程总结(三)线程同步 在应用程序中使用多个线程的一个好处是每个线程都可以异步执行.对于 Windows 应用程序,耗时的任务可以在后台执行,而使应用程序窗口和控件保持响应.对于服务器应用程序,多线程处理提供了用不同线程处理每个传入请求的能力.否则,在完全满足前一个请求之前,将无法处理每个新请求.然而,线程的异步特性意味着必须协调对资源(如文件句柄.网络连接和内存)的访问.否则,两个或更多的线程可能在同一时间访问相同的资源,而每个线程都不知道其他线程的操作. "如果觉得有用,请帮顶! 如果有

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

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

线程同步之临界区

临界区:当两个线程竞争同一资源时,如果对资源的访问顺序敏感,就称存在竞态条件.导致竞态条件发生的代码区称作临界区.临界区线程同步适用范围:它只能同步一个进程中的线程,不能跨进程同步.一般用它来做单个进程内的代码快同步,效率比较高. 在.Net中有Monitor.Lock等方式是以临界区的方式来实现线程同步的,我们看一下两者的具体示例. 1.Lock  Lock关键字将代码块标记为临界区,方法是获取指定对象的互斥锁,执行语句,然后释放锁,这样其它线程就可以接着获取锁来进入临界区. Lock关键字保

CLR线程概览(下)

作者:施懿民链接:https://zhuanlan.zhihu.com/p/20866017来源:知乎著作权归作者所有.商业转载请联系作者获得授权,非商业转载请注明出处. 同步: 托管代码 托管代码可以访问很多在System.Threading里定义的同步原语.包括操作系统原语的简单封装如:互斥(Mutex),事件(Event)和旗标(Semaphore)对象,也包括类似的栅栏(Barrier)和自旋锁(SpinLock)等抽象.但托管代码用的最多的同步机制是System.Threading.M

多线程之线程同步

多线程内容大致分两部分,其一是异步操作,可通过专用,线程池,Task,Parallel,PLINQ等,而这里又涉及工作线程与IO线程:其二是线程同步问题,鄙人现在学习与探究的是线程同步问题. 通过学习<CLR via C#>里面的内容,对线程同步形成了脉络较清晰的体系结构,在多线程中实现线程同步的是线程同步构造,这个构造分两大类,一个是基元构造,一个是混合构造.所谓基元则是在代码中使用最简单的构造.基元构造又分成两类,一个是用户模式,另一个是内核模式.而混合构造则是在内部会使用基元构造的用户模

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

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