令牌环式同步扩展

令牌环式同步扩展:从 Ping-Pong 到 ABC 交替执行

在上一篇博客中,我们介绍了六种实现两个线程交替执行的方法。本文将作为扩展,探讨如何将这些实现方式改造为三个线程交替执行 "A"、"B"、"C",形成 "ABCABC..." 的环形序列。

问题描述

实现三个线程交替打印 "A"、"B"、"C",共打印 100 轮,形成 "ABCABC..." 的交替序列。

六种实现方式的改造分析

1. ManualResetEvent / AutoResetEvent 实现

改造难度:⭐⭐ 容易

scss 复制代码
internal class ABC_EventTestCode
{
    public static void Print()
    {
        var a = new AutoResetEvent(true);  // A 初始可用
        var b = new AutoResetEvent(false); // B 初始不可用
        var c = new AutoResetEvent(false); // C 初始不可用
​
        // 线程 A
        Task.Run(() =>
        {
            for (int i = 0; i < 100; i++)
            {
                a.WaitOne();    // 等待 A 信号
                Console.WriteLine("A");
                b.Set();        // 释放 B 信号
            }
        });
​
        // 线程 B
        Task.Run(() =>
        {
            for (int i = 0; i < 100; i++)
            {
                b.WaitOne();    // 等待 B 信号
                Console.WriteLine("B");
                c.Set();        // 释放 C 信号
            }
        });
​
        // 线程 C
        Task.Run(() =>
        {
            for (int i = 0; i < 100; i++)
            {
                c.WaitOne();    // 等待 C 信号
                Console.WriteLine("C");
                a.Set();        // 释放 A 信号,形成闭环
            }
        });
    }
}

工作原理

  • 使用三个 AutoResetEvent 对象,初始状态分别为 true(A 可用)、false(B 不可用)和 false(C 不可用)
  • 每个线程执行前调用 WaitOne() 等待信号
  • 执行完成后,通过 Set() 通知下一个线程,最后一个线程通知第一个线程形成闭环

优缺点

  • ✅ 改造简单,逻辑清晰
  • ✅ AutoResetEvent 自动重置,无需手动调用 Reset()
  • ✅ 适用于传统同步场景
  • ❌ 不支持异步编程,会阻塞线程
  • ❌ ManualResetEvent 需要手动 Reset,三个信号时容易漏掉

2. SemaphoreSlim 实现

改造难度:⭐⭐ 容易

csharp 复制代码
internal class ABC_SemaphoreSlimTestCode
{
    // A 初始可用(1),B、C 初始不可用(0)
    private SemaphoreSlim semA = new SemaphoreSlim(1, 1);
    private SemaphoreSlim semB = new SemaphoreSlim(0, 1);
    private SemaphoreSlim semC = new SemaphoreSlim(0, 1);
​
    public void Print()
    {
        // 线程 A
        Task.Run(async () =>
        {
            for (int i = 0; i < 100; i++)
            {
                await semA.WaitAsync();   // 等待 A 信号
                Console.WriteLine("A");
                semB.Release();           // 释放 B 信号
            }
        });
​
        // 线程 B
        Task.Run(async () =>
        {
            for (int i = 0; i < 100; i++)
            {
                await semB.WaitAsync();   // 等待 B 信号
                Console.WriteLine("B");
                semC.Release();           // 释放 C 信号
            }
        });
​
        // 线程 C
        Task.Run(async () =>
        {
            for (int i = 0; i < 100; i++)
            {
                await semC.WaitAsync();   // 等待 C 信号
                Console.WriteLine("C");
                semA.Release();           // 释放 A 信号,形成闭环
            }
        });
    }
}

工作原理

  • 使用三个 SemaphoreSlim 对象,初始计数分别为 1(A 可用)、0(B 不可用)和 0(C 不可用)
  • 每个线程执行前调用 WaitAsync() 等待信号
  • 执行完成后,通过 Release() 增加下一个信号量的计数,最后一个线程释放第一个信号量形成闭环

优缺点

  • ✅ 改造简单,代码简洁
  • ✅ 支持异步编程,使用 await 语法更现代
  • ✅ 轻量级,性能较好
  • ✅ 最适合 ABC 场景,代码清晰易维护
  • ✅ 支持超时和取消操作

3. TaskCompletionSource 实现

改造难度:⭐⭐⭐ 中等

csharp 复制代码
internal class ABC_TaskCompletionSourceTestCode
{
    public static async Task PrintAsync()
    {
        var tcsA = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
        var tcsB = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
        var tcsC = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
​
        // 初始让 A 可以执行
        tcsA.SetResult(true);
​
        // 线程 A
        var taskA = Task.Run(async () =>
        {
            for (int i = 0; i < 100; i++)
            {
                await tcsA.Task;     // 等待 A 信号
                Console.WriteLine("A");
                tcsA = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
                tcsB.SetResult(true); // 通知 B 可以执行
            }
        });
​
        // 线程 B
        var taskB = Task.Run(async () =>
        {
            for (int i = 0; i < 100; i++)
            {
                await tcsB.Task;     // 等待 B 信号
                Console.WriteLine("B");
                tcsB = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
                tcsC.SetResult(true); // 通知 C 可以执行
            }
        });
​
        // 线程 C
        var taskC = Task.Run(async () =>
        {
            for (int i = 0; i < 100; i++)
            {
                await tcsC.Task;     // 等待 C 信号
                Console.WriteLine("C");
                tcsC = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
                tcsA.SetResult(true); // 通知 A 可以执行,形成闭环
            }
        });
​
        await Task.WhenAll(taskA, taskB, taskC);
    }
}

工作原理

  • 使用三个 TaskCompletionSource 对象
  • 初始时,将 A 的任务标记为完成,允许 A 立即执行
  • 每个线程执行前等待对应的 Task 完成
  • 执行完成后,创建新的 TaskCompletionSource 对象,并将下一个线程的任务标记为完成,最后一个线程通知第一个线程形成闭环

优缺点

  • ✅ 支持异步编程模型
  • ✅ 可以传递实际数据,不仅仅是信号
  • ❌ 每次迭代都需要创建三个新的 TaskCompletionSource 对象,内存开销较大
  • ❌ 代码相对复杂,需要理解异步编程模型
  • ❌ 对于纯信号控制场景,有点杀鸡用牛刀

4. Channel 实现

改造难度:⭐⭐ 容易

csharp 复制代码
internal class ABC_ChannelTestCode
{
    public static async Task PrintAsync()
    {
        var chA = Channel.CreateUnbounded<bool>();
        var chB = Channel.CreateUnbounded<bool>();
        var chC = Channel.CreateUnbounded<bool>();
​
        // 初始让 A 可以执行
        await chA.Writer.WriteAsync(true);
​
        // 线程 A
        var taskA = Task.Run(async () =>
        {
            for (int i = 0; i < 100; i++)
            {
                await chA.Reader.ReadAsync(); // 等待 A 信号
                Console.WriteLine("A");
                await chB.Writer.WriteAsync(true); // 通知 B 可以执行
            }
        });
​
        // 线程 B
        var taskB = Task.Run(async () =>
        {
            for (int i = 0; i < 100; i++)
            {
                await chB.Reader.ReadAsync(); // 等待 B 信号
                Console.WriteLine("B");
                await chC.Writer.WriteAsync(true); // 通知 C 可以执行
            }
        });
​
        // 线程 C
        var taskC = Task.Run(async () =>
        {
            for (int i = 0; i < 100; i++)
            {
                await chC.Reader.ReadAsync(); // 等待 C 信号
                Console.WriteLine("C");
                await chA.Writer.WriteAsync(true); // 通知 A 可以执行,形成闭环
            }
        });
​
        await Task.WhenAll(taskA, taskB, taskC);
    }
}

工作原理

  • 使用三个 Channel 对象创建无界通道
  • 初始时,向 A 通道写入数据,允许 A 立即执行
  • 每个线程从自己的通道读取数据(等待信号)
  • 执行完成后,向下一个通道写入数据(发送信号),最后一个线程向第一个通道写入数据形成闭环

优缺点

  • ✅ 现代异步编程模型,设计优雅
  • ✅ 高性能,适合高并发场景
  • ✅ 可以轻松扩展为传递复杂数据
  • ❌ 需要 .NET Core 3.0+ 或 .NET 5+ 支持
  • ❌ 对于纯信号控制场景,可能显得过于复杂

5. Monitor/lock 实现

改造难度:⭐⭐⭐⭐ 困难

arduino 复制代码
internal class ABC_MonitorLockTestCode
{
    private object lockObj = new object();
    private int turn = 0; // 0=A, 1=B, 2=C
​
    public void Print()
    {
        // 线程 A
        Task.Run(() =>
        {
            for (int i = 0; i < 100; i++)
            {
                lock (lockObj)
                {
                    while (turn != 0) // 等待自己的回合
                    {
                        Monitor.Wait(lockObj); // 释放锁并等待信号
                    }
                    
                    Console.WriteLine("A");
                    turn = 1; // 切换到 B 回合
                    Monitor.PulseAll(lockObj); // 唤醒所有等待的线程
                }
            }
        });
​
        // 线程 B
        Task.Run(() =>
        {
            for (int i = 0; i < 100; i++)
            {
                lock (lockObj)
                {
                    while (turn != 1) // 等待自己的回合
                    {
                        Monitor.Wait(lockObj); // 释放锁并等待信号
                    }
                    
                    Console.WriteLine("B");
                    turn = 2; // 切换到 C 回合
                    Monitor.PulseAll(lockObj); // 唤醒所有等待的线程
                }
            }
        });
​
        // 线程 C
        Task.Run(() =>
        {
            for (int i = 0; i < 100; i++)
            {
                lock (lockObj)
                {
                    while (turn != 2) // 等待自己的回合
                    {
                        Monitor.Wait(lockObj); // 释放锁并等待信号
                    }
                    
                    Console.WriteLine("C");
                    turn = 0; // 切换到 A 回合,形成闭环
                    Monitor.PulseAll(lockObj); // 唤醒所有等待的线程
                }
            }
        });
    }
}

工作原理

  • 使用一个共享的 lockObj 作为同步对象
  • 使用一个整数变量 turn 来跟踪当前应该执行的线程(0=A, 1=B, 2=C)
  • 每个线程在执行前检查是否是自己的回合,如果不是则等待
  • 执行完成后,切换回合状态并唤醒所有等待的线程

优缺点

  • ✅ 不需要额外的同步原语,只使用 .NET 内置的 Monitor 机制
  • ❌ 改造复杂,代码冗长
  • ❌ 不支持异步编程,会阻塞线程
  • ❌ 需要使用 PulseAll() 唤醒所有线程,性能较差
  • ❌ 三个线程竞争锁,容易导致不必要的唤醒
  • ❌ 代码复杂度指数增长,难以维护

6. 混合方案:流水线 Channel 实现

改造难度:⭐⭐ 容易

csharp 复制代码
internal class ABC_PipelineChannelTestCode
{
    public static async Task PrintAsync()
    {
        // 创建两个通道,形成 A → B → C 的流水线
        var abChannel = Channel.CreateUnbounded<string>();
        var bcChannel = Channel.CreateUnbounded<string>();
​
        // 线程 A - 生产数据
        var taskA = Task.Run(async () =>
        {
            for (int i = 0; i < 100; i++)
            {
                string data = $"A-{i}";
                Console.WriteLine($"A: {data}");
                await abChannel.Writer.WriteAsync(data); // 传递给 B
            }
            abChannel.Writer.Complete(); // 完成写入
        });
​
        // 线程 B - 处理数据
        var taskB = Task.Run(async () =>
        {
            await foreach (var data in abChannel.Reader.ReadAllAsync())
            {
                string processedData = $"B-{data}";
                Console.WriteLine($"B: {processedData}");
                await bcChannel.Writer.WriteAsync(processedData); // 传递给 C
            }
            bcChannel.Writer.Complete(); // 完成写入
        });
​
        // 线程 C - 消费数据
        var taskC = Task.Run(async () =>
        {
            await foreach (var data in bcChannel.Reader.ReadAllAsync())
            {
                string finalData = $"C-{data}";
                Console.WriteLine($"C: {finalData}");
            }
        });
​
        await Task.WhenAll(taskA, taskB, taskC);
    }
}

工作原理

  • 使用两个 Channel 对象创建流水线
  • 线程 A 生产数据并写入第一个通道
  • 线程 B 从第一个通道读取数据,处理后写入第二个通道
  • 线程 C 从第二个通道读取数据并消费

优缺点

  • ✅ 非常适合需要传递数据的场景
  • ✅ 现代异步编程模型,设计优雅
  • ✅ 高性能,适合高并发场景
  • ✅ 代码逻辑清晰,易于理解
  • ❌ 对于纯信号控制场景,可能显得过于复杂
  • ❌ 需要 .NET Core 3.0+ 或 .NET 5+ 支持

实现方式对比

实现方式 改造难度 ABC 适合度 推荐场景
AutoResetEvent ⭐⭐ ⭐⭐⭐ 传统同步,无 async/await
SemaphoreSlim ⭐⭐ ⭐⭐⭐⭐⭐ 现代异步,首选
TaskCompletionSource ⭐⭐⭐ ⭐⭐ 需传递复杂数据
Channel ⭐⭐ ⭐⭐⭐ 高并发+数据传递
Monitor/lock ⭐⭐⭐⭐ 简单场景,避免多信号
流水线 Channel ⭐⭐ ⭐⭐⭐⭐⭐ 数据流转(A→B→C)

通用模式:扩展到 N 个线程

使用 SemaphoreSlim 可以很容易地扩展到 N 个线程的交替执行。以下是一个通用的实现模式:

ini 复制代码
internal class NThreadsSemaphoreSlimTestCode
{
    private List<SemaphoreSlim> semaphores;
    private int threadCount;
​
    public NThreadsSemaphoreSlimTestCode(int count)
    {
        threadCount = count;
        semaphores = new List<SemaphoreSlim>();
        
        // 初始化信号量,第一个初始可用,其余初始不可用
        for (int i = 0; i < count; i++)
        {
            semaphores.Add(new SemaphoreSlim(i == 0 ? 1 : 0, 1));
        }
    }
​
    public void Print()
    {
        for (int i = 0; i < threadCount; i++)
        {
            int threadIndex = i;
            char threadChar = (char)('A' + threadIndex);
            
            Task.Run(async () =>
            {
                for (int j = 0; j < 100; j++)
                {
                    // 等待自己的信号
                    await semaphores[threadIndex].WaitAsync();
                    Console.WriteLine(threadChar);
                    
                    // 释放下一个线程的信号,最后一个线程释放第一个线程的信号
                    int nextIndex = (threadIndex + 1) % threadCount;
                    semaphores[nextIndex].Release();
                }
            });
        }
    }
}
​
// 使用示例:
// var nThreadsTest = new NThreadsSemaphoreSlimTestCode(5); // 5个线程,交替打印 ABCDE
// nThreadsTest.Print();

结论

从两种线程扩展到三种线程(ABC)的交替执行,不同实现方式的表现差异更加明显:

  1. SemaphoreSlim 仍然是最佳选择,代码简洁、支持异步、性能良好,尤其是在多线程场景下优势更加突出。
  2. AutoResetEvent 也是一个不错的选择,改造简单,适合传统同步场景。
  3. Channel 在需要传递数据的场景下表现优异,尤其是流水线模式非常适合数据处理流程。
  4. TaskCompletionSource 虽然可以实现,但在多线程场景下代码膨胀严重,内存开销较大。
  5. Monitor/lock 在多线程场景下表现最差,代码复杂,性能较差,不推荐使用。

选择哪种实现方式,取决于具体的场景需求:

  • 纯信号控制 (ABCABC):首选 SemaphoreSlim,代码最简洁,性能最好。
  • 需要传递数据 :首选 Channel 流水线模式,尤其是在 A→B→C 需要处理数据的场景。
  • 传统同步场景 :选择 AutoResetEvent,兼容性好,易于理解。
  • 特殊需求:根据具体情况选择其他实现方式。

通过本文的扩展,我们可以看到令牌环式同步模式的灵活性和多样性,以及不同同步原语在不同场景下的适用情况。了解这些实现方式,有助于我们在实际项目中选择合适的同步机制,编写高效、可靠的并发代码。

相关推荐
开心就好202525 分钟前
不同阶段的 iOS 应用混淆工具怎么组合使用,源码混淆、IPA混淆
后端·ios
架构师沉默33 分钟前
程序员如何避免猝死?
java·后端·架构
椰奶燕麦1 小时前
Windows PackageManager (winget) 核心故障排错与通用修复指南
后端
zjjsctcdl1 小时前
springBoot发布https服务及调用
spring boot·后端·https
zdl6862 小时前
Spring Boot文件上传
java·spring boot·后端
世界哪有真情2 小时前
哇!绝了!原来这么简单!我的 Java 项目代码终于被 “拯救” 了!
java·后端
RMB Player2 小时前
Spring Boot 集成飞书推送超详细教程:文本消息、签名校验、封装工具类一篇搞定
java·网络·spring boot·后端·spring·飞书
重庆小透明2 小时前
【搞定面试之mysql】第三篇 mysql的锁
java·后端·mysql·面试·职场和发展
武超杰3 小时前
Spring Boot入门教程
java·spring boot·后端
IT 行者3 小时前
Spring Boot 集成 JavaMail 163邮箱配置详解
java·spring boot·后端