Skip to content

ChannelReader.Completion does not complete even after all items are read and TryComplete() is called #123544

@arkolka

Description

@arkolka

Description

Description:

I encountered a scenario where ChannelReader.Completion never becomes IsCompleted == true, even though:

  • All items in the channel have been successfully read using TryRead.
  • channel.Writer.TryComplete() has been called.
    This causes code that awaits channel.Reader.Completion or checks IsCompleted to hang indefinitely.

Reproduction Steps

using System;
using System.Threading.Channels;
using System.Threading.Tasks;

public class Program
{
    public static async Task Main()
    {
        while (true)
        {
            var channel = Channel.CreateUnbounded<int>(new UnboundedChannelOptions
            {
                SingleWriter = false,
                SingleReader = false,
                AllowSynchronousContinuations = false
            });

            await channel.Writer.WriteAsync(1);
            await channel.Writer.WriteAsync(2);

            channel.Reader.TryRead(out _);

            var t1 = Task.Run(() =>
            {
                channel.Reader.TryRead(out _);
            });

            var t2 = Task.Run(() =>
            {
                channel.Writer.TryComplete();
            });

            await Task.WhenAll(t1, t2);

            channel.Reader.TryRead(out _);

            if (!channel.Reader.Completion.IsCompleted)
            {
                while (true)
                {
                    channel.Writer.TryComplete();
                    channel.Reader.TryRead(out _);
                    Console.WriteLine(channel.Reader.Completion.IsCompleted);
                    await Task.Delay(1000);
                }
            }
        }
    }
}

Expected behavior

channel.Reader.Completion.IsCompleted should become true after:

  • All items have been read.
  • channel.Writer.TryComplete() has been called.

Actual behavior

channel.Reader.Completion.IsCompleted stays false indefinitely.

Regression?

No response

Known Workarounds

No response

Configuration

  • net 10 (sdk 10.0.100)
  • macOS Version 15.6.1
  • ARM64
  • not specific

Other information

Looking at the internal implementations of TryRead in different channel types.

UnboundedChannel

public override bool TryRead([MaybeNullWhen(false)] out T item)
{
    UnboundedChannel<T> parent = this._parent;
    if (parent._items.TryDequeue(out item))
    {
        UnboundedChannel<T>.UnboundedChannelReader.CompleteIfDone(parent);
        return true;
    }
    item = default;
    return false;
}
  • TryRead in UnboundedChannel does not acquire any lock when dequeuing items.
  • The CompleteIfDone method may be invoked concurrently, and internal state updates may not be fully synchronized in multithreaded scenarios.

BoundedChannel

public override bool TryRead([MaybeNullWhen(false)] out T item)
{
    BoundedChannel<T> parent = this._parent;
    lock (parent.SyncObj)
    {
        if (!parent._items.IsEmpty)
        {
            item = this.DequeueItemAndPostProcess();
            return true;
        }
    }
    item = default;
    return false;
}
  • In contrast, BoundedChannel.TryRead uses a lock (SyncObj) to ensure that the dequeue operation and state updates are fully synchronized.
  • The issue with Completion never completing does not occur with BoundedChannel, even under the same concurrent usage patterns.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions