C# 多线程编程:线程锁与无锁并发


前言

多线程编程在现代软件开发中至关重要。本文将讨论 C# 中的多线程技术,重点介绍锁的概念,线程锁与无锁并发。通过学习本篇博文,我们将学会如何正确处理并发问题,提高程序的性能和稳定性。


一、锁的基本概念

在多线程编程中,掌握锁的概念至关重要。本节将介绍什么是锁,为什么我们需要锁以及锁的作用原理。

1.1 什么是锁?

锁是一种同步机制,用于控制多个线程对共享资源的访问。当一个线程获得了锁时,其他线程将被阻塞,直到该线程释放了锁。

1.2 为什么需要锁?

在并发编程中,多个线程同时访问共享资源可能导致数据竞争和不确定的行为。锁可以确保在任意时刻只有一个线程可以访问共享资源,从而避免竞态条件和数据不一致性问题。

1.3 锁的作用原理

锁的作用原理通常涉及到内部的互斥机制。当一个线程获得锁时,它会将锁标记为已被占用,其他线程尝试获取该锁时会被阻塞,直到持有锁的线程释放锁。这种互斥机制可以通过不同的算法和数据结构来实现,如互斥量、自旋锁等。

理解锁的概念是进行多线程编程的基础,它为我们提供了一种可靠的方式来保护共享资源,确保线程安全和程序的正确性。在接下来的章节中,我们将深入探讨不同类型的锁以及它们在 C# 多线程编程中的应用。

二、线程锁的类型

在多线程编程中,锁的实现通常基于互斥机制,确保在任意时刻只有一个线程可以访问共享资源。本节将介绍几种常见的锁类型,包括自旋锁、互斥锁、混合锁和读写锁。

2.1 自旋锁(Spin Lock)

  • 自旋锁是一种基于忙等待的锁,当线程尝试获取锁时,如果发现锁已被其他线程占用,它会循环(自旋)等待,不断地检查锁是否被释放。
  • 自旋锁适用于锁的占用时间短、线程并发度高的情况,因为它避免了线程在等待锁时进入内核态造成的性能损失。
  • 但自旋锁可能会导致线程空转消耗 CPU 资源,因此不适合在锁被占用时间较长或竞争激烈的情况下使用。

2.2 互斥锁(Mutex)

  • 互斥锁是一种阻塞式锁,它通过操作系统提供的原语实现,当线程尝试获取锁时,如果发现锁已被其他线程占用,它会被阻塞,直到锁被释放。
  • 互斥锁适用于锁的占用时间长、线程竞争激烈的情况,因为它可以将等待锁的线程置于休眠状态,避免空转浪费 CPU 资源。
  • 但互斥锁由于涉及系统调用,因此会产生较大的开销,尤其在高并发情况下可能成为性能瓶颈。

2.3 混合锁(Hybrid Lock)

  • 混合锁是结合了自旋锁和互斥锁的优点,根据锁的占用情况动态选择使用自旋等待还是阻塞等待。
  • 在锁的竞争不激烈时,混合锁会采用自旋等待的方式,避免线程进入内核态;而在锁的竞争激烈时,会转为阻塞等待,以减少空转和CPU资源的浪费。
  • 混合锁的实现较为复杂,需要根据具体的场景进行调优,以达到最佳的性能和资源利用率。

2.4 读写锁(Read-Write Lock)

  • 读写锁允许多个线程同时对共享资源进行读取操作,但在进行写入操作时需要互斥。
  • 读写锁适用于读操作远远多于写操作的场景,可以提高程序的并发性能。
  • 读写锁通常包含一个写锁和多个读锁,当写锁被占用时,所有的读锁和写锁都会被阻塞;而当读锁被占用时,其他的读锁仍然可以被获取,但写锁会被阻塞。

三、锁的实现方式

下面是几种常见的锁类型:

3.1 Monitor(互斥体)

Monitor 是 C# 中最基本的锁机制之一,它使用 lock 关键字来实现。lock 关键字在进入代码块时获取锁,在退出代码块时释放锁。这确保了在同一时刻只有一个线程可以执行 lock 块中的代码。

using System;
using System.Threading;

class Program
{
   
   
    private static object _lock = new object();

    static void Main(string[] args)
    {
   
   
        // 启动两个线程访问临界区
        Thread thread1 = new Thread(EnterCriticalSection);
        Thread thread2 = new Thread(EnterCriticalSection);

        thread1.Start();
        thread2.Start();
    }

    static void EnterCriticalSection()
    {
   
   
        // 进入临界区
        Monitor.Enter(_lock);
        try
        {
   
   
            // 在临界区内操作共享资源
            Console.WriteLine($"Thread {
     
     Thread.CurrentThread.ManagedThreadId} entered critical section.");
            Thread.Sleep(2000);
        }
        finally
        {
   
   
            // 退出临界区
            Monitor.Exit(_lock);
            Console.WriteLine($"Thread {
     
     Thread.CurrentThread.ManagedThreadId} exited critical section.");
        }
    }
}

另一种写法:

object lockObj = new object();
lock (lockObj)
{
   
   
    // 执行需要同步的代码
}

3.2 Mutex(互斥体)

Mutex 是一种操作系统级别的同步原语,与 Monitor 不同,Mutex 可以在进程间共享。Mutex 是一个系统对象,它可以在全局范围内唯一标识一个锁。使用 Mutex 需要在代码中声明一个 Mutex 对象,然后通过 WaitOne 和 ReleaseMutex 方法来获取和释放锁。

using System;
using System.Threading;

class Program
{
   
   
    private static Mutex _mutex = new Mutex();

    static void Main(string[] args)
    {
   
   
        // 启动两个线程访问临界区
        Thread thread1 = new Thread(EnterCriticalSection);
        Thread thread2 = new Thread(EnterCriticalSection);

        thread1.Start();
        thread2.Start();
    }

    static void EnterCriticalSection()
    {
   
   
        // 等待获取 Mutex
        _mutex.WaitOne();
        try
        {
   
   
            // 在临界区内操作共享资源
            Console.WriteLine($"Thread {
     
     Thread.CurrentThread.ManagedThreadId} entered critical section.");
            Thread.Sleep(2000);
        }
        finally
        {
   
   
            // 释放 Mutex
            _mutex.ReleaseMutex();
            Console.WriteLine($"Thread {
     
     Thread.CurrentThread.ManagedThreadId} exited critical section.");
        }
    }
}

3.3 Semaphore(信号量)

Semaphore 是一种允许多个线程同时访问共享资源的同步原语。它通过一个计数器来控制同时访问资源的线程数量。Semaphore 构造函数需要指定初始的计数器值和最大的计数器值。通过 WaitOne 和 Release 方法来获取和释放信号量。

using System;
using System.Threading;

class Program
{
   
   
    private static Semaphore _semaphore = new Semaphore(2, 2); // 允许最多两个线程同时访问

    static void Main(string[] args)
    {
   
   
        // 启动五个线程访问临界区
        for (int i = 0; i < 5; i++)
        {
   
   
            Thread thread = new Thread(EnterCriticalSection);
            thread.Start(i);
        }
    }

    static void EnterCriticalSection(object threadId)
    {
   
   
        // 等待获取 Semaphore
        _semaphore.WaitOne();
        try
        {
   
   
            // 在临界区内操作共享资源
            Console.WriteLine($"Thread {
     
     threadId} entered critical section.");
            Thread.Sleep(2000);
        }
        finally
        {
   
   
            // 释放 Semaphore
            _semaphore.Release();
            Console.WriteLine($"Thread {
     
     threadId} exited critical section.");
        }
    }
}

3.4 ReaderWriterLock(读写锁)

ReaderWriterLock 是一种特殊的锁机制,它允许多个线程同时读取共享资源,但在写入资源时需要互斥。这种锁适用于读操作远远多于写操作的场景,可以提高性能。

using System;
using System.Threading;

class Program
{
   
   
    private static ReaderWriterLockSlim _rwLock = new ReaderWriterLockSlim();

    static void Main(string[] args)
    {
   
   
        // 启动五个读线程和一个写线程访问共享资源
        for (int i = 0; i < 5; i++)
        {
   
   
            Thread readerThread = new Thread(ReadSharedResource);
            readerThread.Start(i);
        }

        Thread writerThread = new Thread(WriteSharedResource);
        writerThread.Start();
    }

    static void ReadSharedResource(object threadId)
    {
   
   
        _rwLock.EnterReadLock();
        try
        {
   
   
            // 读取共享资源
            Console.WriteLine($"Reader {
     
     threadId} read shared resource.");
            Thread.Sleep(2000);
        }
        finally
        {
   
   
            _rwLock.ExitReadLock();
        }
    }

    static void WriteSharedResource()
    {
   
   
        _rwLock.EnterWriteLock();
        try
        {
   
   
            // 写入共享资源
            Console.WriteLine("Writer wrote shared resource.");
            Thread.Sleep(1000);
        }
        finally
        {
   
   
            _rwLock.ExitWriteLock();
        }
    }
}

四、无锁并发编程

在多线程编程中,除了使用锁机制来保护共享资源外,还可以通过无锁并发编程来实现并发控制。本章将介绍无锁并发编程的概念、优势以及常见的无锁算法。

4.1 无锁并发编程的概念

无锁并发编程是一种基于原子操作的并发控制方式,它不需要使用传统的锁机制来保护共享资源,而是通过原子性操作来确保线程安全。无锁并发编程通常比锁机制具有更低的开销和更高的性能。

4.2 无锁算法

4.2.1 CAS(Compare And Swap)

CAS 是一种原子操作,通常由处理器提供支持。它涉及三个操作数:内存位置(通常是一个地址)、旧的预期值和新的值。如果内存位置的值与预期值相等,则将新值写入该位置;否则,操作失败。

using System;
using System.Threading;

class Program
{
   
   
    static int sharedValue = 0;

    static void Main(string[] args)
    {
   
   
        // 使用 CAS 算法更新共享变量
        int expectedValue = 0;
        int newValue = 1;
        if (Interlocked.CompareExchange(ref sharedValue, newValue, expectedValue) == expectedValue)
        {
   
   
            Console.WriteLine("Value updated successfully."
1.几种同步方法的区别 lock和Monitor是.NET用一个特殊结构实现的,Monitor对象是完全托管的、完全可移植的,并且在操作系统资源要求方 面可能更为有效,同步速度较快,但不能跨进程同步。lock(Monitor.Enter和Monitor.Exit方法的封装),主要作用是定临界区,使临 界区代码只能被获得的线程执行。Monitor.Wait和Monitor.Pulse用于线程同步,类似信号操作,个人感觉使用比较复杂,容易造成死 。 互斥体Mutex和事件对象EventWaitHandler属于内核对象,利用内核对象进行线程同步,线程必须要在用户模式和内核模 式间切换,所以一般效率很低,但利用互斥对象和事件对象这样的内核对象,可以在多个进程中的各个线程间进行同步。 互斥体Mutex类似于一个接力棒,拿到接力棒的线程才可以开始跑,当然接力棒一次只属于一个线程(Thread Affinity),如果这个线程不释放接力棒(Mutex.ReleaseMutex),那么没办法,其他所有需要接力棒运行的线程都知道能等着看热 闹。 EventWaitHandle 类允许线程通过发信号互相通信。 通常,一个或多个线程在 EventWaitHandle 上阻止,直到一个未阻止的线程调用 Set 方法,以释放一个或多个被阻止的线程。 2.什么时候需要定 首先要理解定是解决竞争条件的,也就是多个线程同时访问某个资源,造成意想不到的结果。比如,最简单的情况是,一个计数器,两个线程 同时加一,后果就是损失了一个计数,但相当频繁的定又可能带来性能上的消耗,还有最可怕的情况死。那么什么情况下我们需要使用,什么情况下不需要 呢? 1)只有共享资源才需要定 只有可以被多线程访问的共享资源才需要考虑定,比如静态变量,再比如某些缓存中的值,而属于线程内部的变量不需要定。 2)多使用lock,少用Mutex 如果你一定要使用定,请尽量不要使用内核模块的定机制,比如.NET的Mutex,Semaphore,AutoResetEvent和 ManuResetEvent,使用这样的机制涉及到了系统在用户模式和内核模式间的切换,性能差很多,但是他们的优点是可以跨进程同步线程,所以应该清 楚的了解到他们的不同和适用范围。 3)了解你的程序是怎么运行的 实际上在web开发中大多数逻辑都是在单个线程中展开的,一个请求都会在一个单独的线程中处理,其中的大部分变量都是属于这个线程的,根本没有必要考虑 定,当然对于ASP.NET中的Application对象中的数据,我们就要考虑加了。 4)把定交给数据库 数 据库除了存储数据之外,还有一个重要的用途就是同步,数据库本身用了一套复杂的机制来保证数据的可靠和一致性,这就为我们节省了很多的精力。保证了数据源 头上的同步,我们多数的精力就可以集中在缓存等其他一些资源的同步访问上了。通常,只有涉及到多个线程修改数据库中同一条记录时,我们才考虑加。 5)业务逻辑对事务和线程安全的要求 这 条是最根本的东西,开发完全线程安全的程序是件很费时费力的事情,在电子商务等涉及金融系统的案例中,许多逻辑都必须严格的线程安全,所以我们不得不牺牲 一些性能,和很多的开发时间来做这方面的工作。而一般的应用中,许多情况下虽然程序有竞争的危险,我们还是可以不使用定,比如有的时候计数器少一多一, 对结果无伤大雅的情况下,我们就可以不用去管它。 3.InterLocked类 Interlocked 类提供了同步对多个线程共享的变量的访问的方法。如果该变量位于共享内存中,则不同进程的线程就可以使用该机制。互操作是原子的,即整个操作是不能由相 同变量上的另一个互操作所中断的单元。这在抢先多线程操作系统中是很重要的,在这样的操作系统中,线程可以在从某个内存地址加载值之后但是在有机会更改 和存储该值之前被挂起。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值