.NET 10 中无缓冲通道 Unbuffered Channel 支持(Rendezvous Channel)
Intro
在 .NET 10 中,System.Threading.Channels 引入了一项令人兴奋的新功能:支持创建容量为 0 的有界通道,又可以称为无缓冲通道,也称为 Rendezvous Channel(会合通道)。
What
在之前的 .NET 版本中,创建有界通道时容量必须至少为 1,否则会抛出异常。而从 .NET 10 开始,您可以使用 Channel.CreateBounded(0) 创建一个容量为 0 的通道,这样就没有任何缓冲,一个是无缓冲的通道,Unbuffered Channel。
容量表示通道可以缓冲的项目数量。当容量为 0 时,通道无法缓冲任何内容,这使其成为一个 会合点(Rendezvous):读取者和写入者必须同时在通道处等待,才能直接从写入者传递数据到读取者。
这种语义与其他语言和库中的概念一致。例如,在其他语言中,如果不指定缓冲区大小或指定大小为 0,您同样会得到一个无缓冲的会合通道。
使用场景
无缓冲通道确保在业务工作流中的关键交接点同步
使用 带缓冲通道 时,发送方可以在没有确认的情况下继续,这可能导致消费和发送的速率不匹配从而导致一些资源不匹配的问题。
主要使用场景如下:
1. 同步和协调
-
• 确保发送者和接收者在完全相同的时间点同步 -
• 保证操作完成后再继续
2. 交接/直接传输
-
• 从生产者到消费者的数据直接传输,没有中间存储 -
• 确保数据被接收后发送者才继续 -
• 当你需要确认数据已被接受时非常有用
3. 信号/事件
-
• 信号表示某个事件已经发生 -
• 工作线程通知完成或就绪 -
• 例如:使用 done通道来信号goroutine完成
4. 速率限制
-
• 自然的反压机制 - 如果接收者未准备好,发送者会被阻塞 -
• 防止数据过多导致接收者不堪重负 -
• 迫使生产者降低速度以适应消费者的处理速度,尤其是有资源限制或要严格控制资源使用的场景
5. 请求-响应模式
-
• 实现同步请求-响应通信 -
• 确保请求完全处理后再继续
6. 资源协调
-
• 协调对共享资源的访问 -
• 确保管道中各阶段之间工作的有序交接
无缓冲通道的关键特性是提供 保证同步 - 双方必须同时准备好,这使得它们在需要严格协调而不是异步通信时非常理想。
使用示例
基本用法
// 创建一个容量为 0 的有界通道
var channel = Channel.CreateBounded<int>(0);
// 写入操作会阻塞,直到有读取者准备好接收
var writeTask = channel.Writer.WriteAsync(42);
// 读取操作会阻塞,直到有写入者准备好发送
var value = await channel.Reader.ReadAsync(); // 返回 42
await writeTask; // 写入完成
使用 BoundedChannelOptions 配置
var options = new BoundedChannelOptions(0)
{
FullMode = BoundedChannelFullMode.Wait,
AllowSynchronousContinuations = false
};
var channel = Channel.CreateBounded<string>(options);
主要特性
1. 同步传递
-
• 读取者和写入者必须同时准备好才能完成数据传递 -
• 没有缓冲,数据直接从写入者传递到读取者
2. 支持多种完整模式
Rendezvous Channel 支持 BoundedChannelFullMode 的不同模式:
-
• Wait:写入操作等待读取者就绪(默认行为) -
• DropWrite:如果没有读取者,立即丢弃写入的数据 -
• DropOldest/DropNewest:与DropWrite行为相同(因为没有缓冲)
var channel = Channel.CreateBounded<int>(new BoundedChannelOptions(0)
{
FullMode = BoundedChannelFullMode.DropWrite
},
droppedItem => Console.WriteLine($"丢弃的项目: {droppedItem}"));
channel.Writer.TryWrite(100); // 如果没有读取者,立即成功并调用回调
3. Count 属性始终为 0
由于没有缓冲,channel.Reader.Count 始终返回 0,即使有等待的写入者或读取者。
var channel = Channel.CreateBounded<int>(0);
var write1 = channel.Writer.WriteAsync(1); // 阻塞
var write2 = channel.Writer.WriteAsync(2); // 阻塞
Console.WriteLine(channel.Reader.Count); // 输出: 0
性能考虑
Rendezvous Channel 在某些场景下可以提供更好的性能和更清晰的语义:
-
• 无内存分配:不需要为缓冲分配空间 -
• 直接传递:数据直接从生产者传递到消费者,没有中间存储 -
• 同步协调:自然实现生产者-消费者之间的节流控制
适用场景
Rendezvous Channel 特别适合以下场景:
-
1. 严格的生产者-消费者同步:需要确保每个生产的项都被立即消费 -
2. 流量控制:自然限制生产者的速度以匹配消费者 -
3. 协调模式:实现类似 CSP(Communicating Sequential Processes)风格的并发模式 -
4. 资源管理:确保资源在传递时不会积压
生产者消费者同步示例
var channel = Channel.CreateBounded<WorkItem>(0);
var producer = Task.Run(async () =>
{
for (var i = 0; i < 5; i++)
{
Console.WriteLine($"[Producer]准备生产项目 {i} {DateTimeOffset.Now}");
await channel.Writer.WriteAsync(new WorkItem(i));
Console.WriteLine($"[Producer]项目 {i} 已交付 {DateTimeOffset.Now}");
}
channel.Writer.Complete();
});
var consumer = Task.Run(async () =>
{
await foreach (var item in channel.Reader.ReadAllAsync())
{
Console.WriteLine($"[Consumer]接收到项目 {item.Id} {DateTimeOffset.Now}");
await Task.Delay(1000); // 模拟处理
Console.WriteLine($"[Consumer]项目 {item.Id} 处理完成 {DateTimeOffset.Now}");
}
});
await Task.WhenAll(producer, consumer);
file sealed record WorkItem(int Id);
输出示例:
从输出结果可以看出,在消费者完成前一个消息的消费任务之前,生产者是不能生产第二个任务的,只有前面的完成了后面的才能生产成功,从而保证发送的速率和消费的速率持平。
More
容量为 0 的无缓冲通道(Rendezvous Channel)为 .NET 开发者提供了一种强大的新工具,用于实现严格的同步和流量控制。
如果您的应用需要精确的生产者-消费者协调或流量控制,这项新功能将是一个不错选择。
References
-
• https://github.com/dotnet/runtime/pull/116097 -
• https://github.com/dotnet/runtime/issues/94046 -
• https://learn.microsoft.com/en-us/dotnet/api/system.threading.channels.channel?view=net-10.0 -
• https://github.com/WeihanLi/SamplesInPractice/blob/main/net10sample/Net10Samples/ChannelSample.cs

