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

文章目录

  • 前言
  • 一、锁的基本概念
    • [1.1 什么是锁?](#1.1 什么是锁?)
    • [1.2 为什么需要锁?](#1.2 为什么需要锁?)
    • [1.3 锁的作用原理](#1.3 锁的作用原理)
  • 二、线程锁的类型
    • [2.1 自旋锁(Spin Lock)](#2.1 自旋锁(Spin Lock))
    • [2.2 互斥锁(Mutex)](#2.2 互斥锁(Mutex))
    • [2.3 混合锁(Hybrid Lock)](#2.3 混合锁(Hybrid Lock))
    • [2.4 读写锁(Read-Write Lock)](#2.4 读写锁(Read-Write Lock))
  • 三、锁的实现方式
    • [3.1 Monitor(互斥体)](#3.1 Monitor(互斥体))
    • [3.2 Mutex(互斥体)](#3.2 Mutex(互斥体))
    • [3.3 Semaphore(信号量)](#3.3 Semaphore(信号量))
    • [3.4 ReaderWriterLock(读写锁)](#3.4 ReaderWriterLock(读写锁))
  • 四、无锁并发编程
    • [4.1 无锁并发编程的概念](#4.1 无锁并发编程的概念)
    • [4.2 无锁算法](#4.2 无锁算法)
      • [4.2.1 CAS(Compare And Swap)](#4.2.1 CAS(Compare And Swap))
      • [4.2.2 Volatile 关键字](#4.2.2 Volatile 关键字)
    • [4.3 无锁并发编程的优势](#4.3 无锁并发编程的优势)
    • [4.4 无锁并发编程的局限性](#4.4 无锁并发编程的局限性)
  • 五、并发集合类
    • [5.1 ConcurrentBag](#5.1 ConcurrentBag)
    • [5.2 ConcurrentDictionary](#5.2 ConcurrentDictionary)
    • [5.3 ConcurrentQueue](#5.3 ConcurrentQueue)
    • [5.4 ConcurrentStack](#5.4 ConcurrentStack)
  • 六、经典并发同步问题
    • [6.1 生产者-消费者问题(Producer-Consumer Problem)](#6.1 生产者-消费者问题(Producer-Consumer Problem))
      • [6.1.1 使用 `Monitor` 类实现生产者-消费者问题](#6.1.1 使用 Monitor 类实现生产者-消费者问题)
      • [6.1.2 使用 `Semaphore` 类实现生产者-消费者问题](#6.1.2 使用 Semaphore 类实现生产者-消费者问题)
      • [6.1.3 使用 `BlockingCollection` 类实现生产者-消费者问题](#6.1.3 使用 BlockingCollection 类实现生产者-消费者问题)
    • [6.2 读者-写者问题(Reader-Writer Problem)](#6.2 读者-写者问题(Reader-Writer Problem))
      • [6.2.1 使用 `ReaderWriterLockSlim` 类实现读者-写者问题](#6.2.1 使用 ReaderWriterLockSlim 类实现读者-写者问题)
      • [6.2.2 使用 `SemaphoreSlim` 类实现读者-写者问题](#6.2.2 使用 SemaphoreSlim 类实现读者-写者问题)
      • [6.2.3 使用 `Monitor` 类实现读者-写者问题](#6.2.3 使用 Monitor 类实现读者-写者问题)
    • [6.3 哲学家就餐问题(Dining Philosophers Problem)](#6.3 哲学家就餐问题(Dining Philosophers Problem))
      • [6.3.1 使用`Semaphore`实现哲学家就餐问题](#6.3.1 使用Semaphore实现哲学家就餐问题)
      • [6.3.2 使用`Mutex`实现哲学家就餐问题](#6.3.2 使用Mutex实现哲学家就餐问题)
      • [6.3.3 使用`Monitor`实现哲学家就餐问题](#6.3.3 使用Monitor实现哲学家就餐问题)
  • 总结

前言

多线程编程在现代软件开发中至关重要。本文将讨论 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 块中的代码。

csharp 复制代码
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.");
        }
    }
}

另一种写法:

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

3.2 Mutex(互斥体)

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

csharp 复制代码
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 方法来获取和释放信号量。

csharp 复制代码
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 是一种特殊的锁机制,它允许多个线程同时读取共享资源,但在写入资源时需要互斥。这种锁适用于读操作远远多于写操作的场景,可以提高性能。

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

csharp 复制代码
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.");
        }
        else
        {
            Console.WriteLine("Value update failed.");
        }
    }
}

在代码中,Interlocked.CompareExchange 方法用于比较并交换操作,它原子性地比较 sharedValue

的值是否等于 expectedValue,如果相等则将 newValue 写入
sharedValue,并返回原来的值;否则不做任何操作。通过这种方式,我们可以实现无锁的并发控制,避免了锁带来的开销和竞争。

CAS 算法通常用于实现无锁的数据结构,例如无锁队列、无锁栈等。虽然 CAS 算法能够提供较好的并发性能,但在某些场景下可能会存在ABA问题等限制,需要特殊处理。

4.2.2 Volatile 关键字

Volatile 关键字用于声明字段是易变的,即可能被多个线程同时访问。它可以确保变量的读取和写入操作都是原子性的,并且不会被编译器或者 CPU 优化掉,从而避免了线程间的数据不一致性问题。

csharp 复制代码
using System;
using System.Threading;

class Program
{
    private static volatile bool _flag = false;

    static void Main(string[] args)
    {
        // 启动一个线程不断修改 _flag 的值
        Thread writerThread = new Thread(WriteFlag);
        writerThread.Start();

        // 主线程读取 _flag 的值
        while (true)
        {
            if (_flag)
            {
                Console.WriteLine("Flag is true.");
                break;
            }
            else
            {
                Console.WriteLine("Flag is false.");
                Thread.Sleep(1000);
            }
        }
    }

    static void WriteFlag()
    {
        // 在另一个线程中修改 _flag 的值
        Thread.Sleep(2000);
        _flag = true;
        Console.WriteLine("Flag has been set to true.");
    }
}

在代码中,使用了 volatile 关键字来声明 _flag 字段,确保了其在多线程环境下的可见性和原子性。主线程不断读取 _flag 的值,而另一个线程在一段时间后将其设置为 true。由于使用了 volatile 关键字,主线程能够正确地读取到 _flag 字段的最新值,从而实现了线程间的正确通信。

4.3 无锁并发编程的优势

  • 减少线程切换开销:无锁并发编程不涉及线程的阻塞和唤醒,可以减少线程切换的开销,提高程序性能。
  • 没有死锁风险:由于无锁并发编程不需要使用锁机制,因此不存在死锁等与锁相关的问题。

4.4 无锁并发编程的局限性

  • 实现复杂度较高:无锁并发编程通常需要仔细设计和实现,因此可能比使用锁机制更复杂。
  • 适用场景有限:无锁并发编程适用于某些特定的场景,例如高并发读操作、轻量级状态同步等。

无锁并发编程是一种重要的并发控制方式,可以提高程序的性能和可伸缩性。但在实际应用中,我们需要根据具体情况选择合适的并发控制方式,以确保程序的正确性和性能。

五、并发集合类

在 C# 中,.NET Framework 提供了许多线程安全的并发集合类,包括 ConcurrentBag、ConcurrentDictionary、ConcurrentQueue 和 ConcurrentStack。本章将介绍这些并发集合类的特点、用途以及示例代码。

5.1 ConcurrentBag

ConcurrentBag 是一个无序的、线程安全的集合类,用于存储对象。它允许多个线程同时添加、移除和遍历元素,适用于需要高度并发性的场景。

csharp 复制代码
using System;
using System.Collections.Concurrent;
using System.Threading.Tasks;

class Program
{
    static void Main(string[] args)
    {
        ConcurrentBag<int> bag = new ConcurrentBag<int>();

        // 使用多个线程添加元素到 ConcurrentBag
        Parallel.For(0, 10, i =>
        {
            bag.Add(i);
            Console.WriteLine($"Added {i} to bag.");
        });

        // 遍历 ConcurrentBag 中的元素
        foreach (var item in bag)
        {
            Console.WriteLine($"Item in bag: {item}");
        }
    }
}

5.2 ConcurrentDictionary

ConcurrentDictionary 是一个线程安全的字典集合类,用于存储键值对。它允许多个线程同时对字典进行读取、写入和修改操作,提供了高效的并发性能。

csharp 复制代码
using System;
using System.Collections.Concurrent;

class Program
{
    static void Main(string[] args)
    {
        ConcurrentDictionary<int, string> dictionary = new ConcurrentDictionary<int, string>();

        // 使用多个线程添加元素到 ConcurrentDictionary
        Parallel.For(0, 10, i =>
        {
            dictionary.TryAdd(i, i);
            Console.WriteLine($"{i} Added");
        });

        // 读取 ConcurrentDictionary 中的键值对
        foreach (var kvp in dictionary)
        {
            Console.WriteLine($"Key: {kvp.Key}, Value: {kvp.Value}");
        }
    }
}

5.3 ConcurrentQueue

ConcurrentQueue 是一个线程安全的队列集合类,用于存储对象。它支持多个线程同时对队列进行入队和出队操作,并提供了高效的并发性能。

csharp 复制代码
using System;
using System.Collections.Concurrent;
using System.Threading.Tasks;

class Program
{
    static void Main(string[] args)
    {
        ConcurrentQueue<int> queue = new ConcurrentQueue<int>();

        // 使用多个线程入队
        Parallel.For(0, 10, i =>
        {
            queue.Enqueue(i);
            Console.WriteLine($"Enqueued {i} to queue.");
        });

        // 多个线程出队
        int item;
        while (queue.TryDequeue(out item))
        {
            Console.WriteLine($"Dequeued {item} from queue.");
        }
    }
}

5.4 ConcurrentStack

ConcurrentStack 是一个线程安全的栈集合类,用于存储对象。它支持多个线程同时对栈进行入栈和出栈操作,并提供了高效的并发性能。

csharp 复制代码
using System;
using System.Collections.Concurrent;
using System.Threading.Tasks;

class Program
{
    static void Main(string[] args)
    {
        ConcurrentStack<int> stack = new ConcurrentStack<int>();

        // 使用多个线程入栈
        Parallel.For(0, 10, i =>
        {
            stack.Push(i);
            Console.WriteLine($"Pushed {i} to stack.");
        });

        // 多个线程出栈
        int item;
        while (stack.TryPop(out item))
        {
            Console.WriteLine($"Popped {item} from stack.");
        }
    }
}

六、经典并发同步问题

以下是几个经典的多线程并发同步问题

6.1 生产者-消费者问题(Producer-Consumer Problem)

生产者线程生成数据并放入共享缓冲区,消费者线程从缓冲区中取出数据进行消费。需要确保在生产者线程生产数据时,消费者线程不会访问空缓冲区,并且在消费者线程消费数据时,生产者线程不会访问满缓冲区。

6.1.1 使用 Monitor 类实现生产者-消费者问题

csharp 复制代码
using System;
using System.Threading;

class Program
{
    static int[] buffer = new int[10];
    static int count = 0;
    static object locker = new object();

    static void Main(string[] args)
    {
        Thread producerThread = new Thread(Producer);
        Thread consumerThread = new Thread(Consumer);

        producerThread.Start();
        consumerThread.Start();

        producerThread.Join();
        consumerThread.Join();
    }

    static void Producer()
    {
        for (int i = 0; i < 20; i++)
        {
            lock (locker)
            {
                while (count == buffer.Length)
                    Monitor.Wait(locker);

                buffer[count++] = i;
                Console.WriteLine("Produced: " + i);
                Monitor.PulseAll(locker);
            }
        }
    }

    static void Consumer()
    {
        for (int i = 0; i < 20; i++)
        {
            lock (locker)
            {
                while (count == 0)
                    Monitor.Wait(locker);

                int consumed = buffer[--count];
                Console.WriteLine("Consumed: " + consumed);
                Monitor.PulseAll(locker);
            }
        }
    }
}

6.1.2 使用 Semaphore 类实现生产者-消费者问题

csharp 复制代码
using System;
using System.Threading;

class Program
{
    static int[] buffer = new int[10];
    static SemaphoreSlim empty = new SemaphoreSlim(10);
    static SemaphoreSlim full = new SemaphoreSlim(0);
    static object locker = new object();

    static void Main(string[] args)
    {
        Thread producerThread = new Thread(Producer);
        Thread consumerThread = new Thread(Consumer);

        producerThread.Start();
        consumerThread.Start();

        producerThread.Join();
        consumerThread.Join();
    }

    static void Producer()
    {
        for (int i = 0; i < 20; i++)
        {
            empty.Wait();
            lock (locker)
            {
                buffer[i % buffer.Length] = i;
                Console.WriteLine("Produced: " + i);
            }
            full.Release();
        }
    }

    static void Consumer()
    {
        for (int i = 0; i < 20; i++)
        {
            full.Wait();
            lock (locker)
            {
                int consumed = buffer[i % buffer.Length];
                Console.WriteLine("Consumed: " + consumed);
            }
            empty.Release();
        }
    }
}

6.1.3 使用 BlockingCollection 类实现生产者-消费者问题

csharp 复制代码
using System;
using System.Collections.Concurrent;
using System.Threading;

class Program
{
    static BlockingCollection<int> buffer = new BlockingCollection<int>(10);

    static void Main(string[] args)
    {
        Thread producerThread = new Thread(Producer);
        Thread consumerThread = new Thread(Consumer);

        producerThread.Start();
        consumerThread.Start();

        producerThread.Join();
        consumerThread.Join();
    }

    static void Producer()
    {
        for (int i = 0; i < 20; i++)
        {
            buffer.Add(i);
            Console.WriteLine("Produced: " + i);
        }
        buffer.CompleteAdding();
    }

    static void Consumer()
    {
        foreach (var item in buffer.GetConsumingEnumerable())
        {
            Console.WriteLine("Consumed: " + item);
        }
    }
}

这些示例分别使用了 MonitorSemaphoreBlockingCollection 来解决生产者-消费者问题。每个示例都实现了在生产者线程生成数据时,消费者线程不会访问空缓冲区,并且在消费者线程消费数据时,生产者线程不会访问满缓冲区。

6.2 读者-写者问题(Reader-Writer Problem)

多个读者线程可以同时读取共享资源,但写者线程在写入共享资源时需要独占访问。需要确保在有写者写入时,不允许读者读取,以保证数据的一致性。

6.2.1 使用 ReaderWriterLockSlim 类实现读者-写者问题

csharp 复制代码
using System;
using System.Threading;

class Program
{
    static ReaderWriterLockSlim rwLock = new ReaderWriterLockSlim();
    static int resource = 0;

    static void Main(string[] args)
    {
        Thread[] readers = new Thread[5];
        Thread[] writers = new Thread[2];

        for (int i = 0; i < 5; i++)
        {
            readers[i] = new Thread(new ThreadStart(Reader));
            readers[i].Start();
        }

        for (int i = 0; i < 2; i++)
        {
            writers[i] = new Thread(new ThreadStart(Writer));
            writers[i].Start();
        }

        for (int i = 0; i < 5; i++)
        {
            readers[i].Join();
        }

        for (int i = 0; i < 2; i++)
        {
            writers[i].Join();
        }
    }

    static void Reader()
    {
        while (true)
        {
            rwLock.EnterReadLock();
            Console.WriteLine("Reader " + Thread.CurrentThread.ManagedThreadId + " reads: " + resource);
            rwLock.ExitReadLock();
            Thread.Sleep(1000);
        }
    }

    static void Writer()
    {
        while (true)
        {
            rwLock.EnterWriteLock();
            resource++;
            Console.WriteLine("Writer " + Thread.CurrentThread.ManagedThreadId + " writes: " + resource);
            rwLock.ExitWriteLock();
            Thread.Sleep(2000);
        }
    }
}

6.2.2 使用 SemaphoreSlim 类实现读者-写者问题

csharp 复制代码
using System;
using System.Threading;

class Program
{
    static SemaphoreSlim readLock = new SemaphoreSlim(1);
    static SemaphoreSlim writeLock = new SemaphoreSlim(1);
    static int readersCount = 0;
    static int resource = 0;

    static void Main(string[] args)
    {
        Thread[] readers = new Thread[5];
        Thread[] writers = new Thread[2];

        for (int i = 0; i < 5; i++)
        {
            readers[i] = new Thread(new ThreadStart(Reader));
            readers[i].Start();
        }

        for (int i = 0; i < 2; i++)
        {
            writers[i] = new Thread(new ThreadStart(Writer));
            writers[i].Start();
        }

        for (int i = 0; i < 5; i++)
        {
            readers[i].Join();
        }

        for (int i = 0; i < 2; i++)
        {
            writers[i].Join();
        }
    }

    static void Reader()
    {
        while (true)
        {
            readLock.Wait();
            readersCount++;
            if (readersCount == 1)
                writeLock.Wait();
            readLock.Release();

            Console.WriteLine("Reader " + Thread.CurrentThread.ManagedThreadId + " reads: " + resource);

            readLock.Wait();
            readersCount--;
            if (readersCount == 0)
                writeLock.Release();
            readLock.Release();

            Thread.Sleep(1000);
        }
    }

    static void Writer()
    {
        while (true)
        {
            writeLock.Wait();
            resource++;
            Console.WriteLine("Writer " + Thread.CurrentThread.ManagedThreadId + " writes: " + resource);
            writeLock.Release();
            Thread.Sleep(2000);
        }
    }
}

6.2.3 使用 Monitor 类实现读者-写者问题

csharp 复制代码
using System;
using System.Threading;

class Program
{
    static object lockObj = new object();
    static int readersCount = 0;
    static int resource = 0;

    static void Main(string[] args)
    {
        Thread[] readers = new Thread[5];
        Thread[] writers = new Thread[2];

        for (int i = 0; i < 5; i++)
        {
            readers[i] = new Thread(new ThreadStart(Reader));
            readers[i].Start();
        }

        for (int i = 0; i < 2; i++)
        {
            writers[i] = new Thread(new ThreadStart(Writer));
            writers[i].Start();
        }

        for (int i = 0; i < 5; i++)
        {
            readers[i].Join();
        }

        for (int i = 0; i < 2; i++)
        {
            writers[i].Join();
        }
    }

    static void Reader()
    {
        while (true)
        {
            lock (lockObj)
            {
                readersCount++;
                if (readersCount == 1)
                    Monitor.Enter(lockObj);
            }

            Console.WriteLine("Reader " + Thread.CurrentThread.ManagedThreadId + " reads: " + resource);

            lock (lockObj)
            {
                readersCount--;
                if (readersCount == 0)
                    Monitor.Exit(lockObj);
            }

            Thread.Sleep(1000);
        }
    }

    static void Writer()
    {
        while (true)
        {
            Monitor.Enter(lockObj);
            resource++;
            Console.WriteLine("Writer " + Thread.CurrentThread.ManagedThreadId + " writes: " + resource);
            Monitor.Exit(lockObj);
            Thread.Sleep(2000);
        }
    }
}

6.3 哲学家就餐问题(Dining Philosophers Problem)

五位哲学家围坐在一张圆桌旁,每位哲学家前面有一只筷子。哲学家思考和进餐,但只有同时拿到两只筷子时才能进餐,而筷子必须是干净的。需要解决资源竞争和死锁的问题。

6.3.1 使用Semaphore实现哲学家就餐问题

csharp 复制代码
using System;
using System.Threading;

class Program
{
    static Semaphore[] sticks = new Semaphore[5];
    static Semaphore table = new Semaphore(4, 4);

    static void Main(string[] args)
    {
        for (int i = 0; i < 5; i++)
        {
            sticks[i] = new Semaphore(1, 1);
        }

        Thread[] philosophers = new Thread[5];
        for (int i = 0; i < 5; i++)
        {
            philosophers[i] = new Thread(Philosopher);
            philosophers[i].Start(i);
        }

        for (int i = 0; i < 5; i++)
        {
            philosophers[i].Join();
        }
    }

    static void Philosopher(object id)
    {
        int philosopherId = (int)id;

        while (true)
        {
            // 思考
            Console.WriteLine($"Philosopher {philosopherId} is thinking.");

            // 拿筷子
            table.WaitOne();
            sticks[philosopherId].WaitOne();
            sticks[(philosopherId + 1) % 5].WaitOne();

            // 吃饭
            Console.WriteLine($"Philosopher {philosopherId} is eating.");

            // 放筷子
            sticks[philosopherId].Release();
            sticks[(philosopherId + 1) % 5].Release();
            table.Release();

            Thread.Sleep(2000);
        }
    }
}

6.3.2 使用Mutex实现哲学家就餐问题

csharp 复制代码
using System;
using System.Threading;

class Program
{
    static Mutex[] sticks = new Mutex[5];

    static void Main(string[] args)
    {
        for (int i = 0; i < 5; i++)
        {
            sticks[i] = new Mutex();
        }

        Thread[] philosophers = new Thread[5];
        for (int i = 0; i < 5; i++)
        {
            philosophers[i] = new Thread(Philosopher);
            philosophers[i].Start(i);
        }

        for (int i = 0; i < 5; i++)
        {
            philosophers[i].Join();
        }
    }

    static void Philosopher(object id)
    {
        int philosopherId = (int)id;

        while (true)
        {
            // 思考
            Console.WriteLine($"Philosopher {philosopherId} is thinking.");

            // 拿筷子
            sticks[philosopherId].WaitOne();
            sticks[(philosopherId + 1) % 5].WaitOne();

            // 吃饭
            Console.WriteLine($"Philosopher {philosopherId} is eating.");

            // 放筷子
            sticks[philosopherId].ReleaseMutex();
            sticks[(philosopherId + 1) % 5].ReleaseMutex();

            Thread.Sleep(2000);
        }
    }
}

6.3.3 使用Monitor实现哲学家就餐问题

csharp 复制代码
using System;
using System.Threading;

class Program
{
    static object[] sticks = new object[5];

    static void Main(string[] args)
    {
        for (int i = 0; i < 5; i++)
        {
            sticks[i] = new object();
        }

        Thread[] philosophers = new Thread[5];
        for (int i = 0; i < 5; i++)
        {
            philosophers[i] = new Thread(Philosopher);
            philosophers[i].Start(i);
        }

        for (int i = 0; i < 5; i++)
        {
            philosophers[i].Join();
        }
    }

    static void Philosopher(object id)
    {
        int philosopherId = (int)id;

        while (true)
        {
            // 思考
            Console.WriteLine($"Philosopher {philosopherId} is thinking.");

            lock (sticks[philosopherId])
            {
                // 拿左边筷子
                Monitor.Enter(sticks[philosopherId]);
                // 拿右边筷子
                Monitor.Enter(sticks[(philosopherId + 1) % 5]);

                // 吃饭
                Console.WriteLine($"Philosopher {philosopherId} is eating.");

                // 放筷子
                Monitor.Exit(sticks[philosopherId]);
                Monitor.Exit(sticks[(philosopherId + 1) % 5]);
            }

            Thread.Sleep(2000);
        }
    }
}

总结

本文简要探讨了 C# 中的多线程编程技术,重点介绍了锁的基本概念、线程锁的类型、锁的实现方式、无锁并发编程以及 C# 中的并发集合类和经典并发同步问题。通过学习本文,我们可以获得以下几个方面的收获:

  1. 理解多线程编程的基本概念:通过介绍锁的基本概念和原理,可以了解为什么在多线程编程中需要使用锁,以及锁是如何工作的。

  2. 掌握不同类型的线程锁:通过对自旋锁、互斥锁、混合锁和读写锁的介绍,可以了解各种锁的特点、适用场景和实现方式,以便在实际应用中选择合适的锁机制。

  3. 熟悉锁的实现方式:通过对 Monitor、Mutex、Semaphore 和 ReaderWriterLock 的介绍,可以了解不同锁的底层实现原理和使用方法,从而更好地应用于实际开发中。

  4. 了解无锁并发编程:通过介绍无锁算法和无锁并发编程的优势和局限性,可以了解在某些场景下无锁编程可以提供更好的性能和并发能力。

  5. 熟悉 C# 中的并发集合类 :通过介绍 ConcurrentBag、ConcurrentDictionary、ConcurrentQueue 和 ConcurrentStack

    等并发集合类,可以了解如何安全地在多线程环境中使用集合类。

  6. 解决经典并发同步问题:通过介绍生产者-消费者问题、读者-写者问题和哲学家就餐问题的解决方案,可以了解如何使用线程锁来解决实际的并发同步问题。

通过本文的学习,可以更加深入地理解并发编程的相关知识,掌握多线程编程的技巧,提高程序的性能和稳定性。

相关推荐
IT规划师3 小时前
开源 - Ideal库 - 常用枚举扩展方法(一)
开源·c#·.net core·ideal库·枚举转换
NetX行者10 小时前
.NET 9震撼来袭:基于.NET 8的五大功能亮点,引领开发新潮流
开发语言·microsoft·c#·.netcore
张某布响丸辣10 小时前
HTTP状态码详解
java·网络·python·网络协议·http·c#
飞舞的哈哈10 小时前
C# 有趣的小程序—桌面精灵详细讲解
c#
Skyshin3412 小时前
C# IEnumerator,IEnumerable ,Iterator
开发语言·c#
ling1s12 小时前
C#核心(7)索引器
开发语言·c#
LKID体13 小时前
win32com库基于wps对Word文档的基础操作
c#·word·wps
金蝶软件小李14 小时前
vector和docker的区别?
开发语言·docker·c#
金蝶软件小李15 小时前
图像处理椒盐噪声
开发语言·图像处理·算法·计算机视觉·c#
小吴同学·16 小时前
(实战)WebApi第13讲:怎么把不同表里的东西,包括同一个表里面不同的列设置成不同的实体,所有的给整合到一起?【前端+后端】、前端中点击标签后在界面中显示
c#·.netcore·.net core