令牌环式同步扩展

令牌环式同步扩展:从 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,兼容性好,易于理解。
  • 特殊需求:根据具体情况选择其他实现方式。

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

相关推荐
v***57002 小时前
SpringBoot项目集成ONLYOFFICE
java·spring boot·后端
阿萨德528号2 小时前
Spring Boot实战:从零构建企业级用户中心系统(八)- 总结与最佳实践
java·spring boot·后端
Java小卷3 小时前
KIE Drools 10.x 规则引擎快速入门
java·后端
Java天梯之路3 小时前
Spring Boot 钩子全集实战(九):`@PostConstruct` 详解
java·spring boot·后端
十间fish3 小时前
车载大端序和tcp大端序
后端
毕设源码-郭学长4 小时前
【开题答辩全过程】以 基于Springboot图书管理系统为例,包含答辩的问题和答案
java·spring boot·后端
毕设源码-钟学长4 小时前
【开题答辩全过程】以 基于springboot网络游戏账号租赁以及出售系统为例,包含答辩的问题和答案
java·spring boot·后端
恒者走天下5 小时前
cpp / c++部分岗位招聘要求分享
后端