0

I have a list of data and want to create the number of Tasks corresponding to the number of elements in the list. But I don't know how to Complete a Channel properly.

My code, but the Channel doesn't close as I expect.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Channels;
using System.Threading.Tasks;

namespace Ding.LearningNewThings
{
    public class MultipleChannel
    {
        public static async Task RunMiltipleChannel()
        {
            List<Place> listPlace = Place.InitData();

            Channel<Position> _dataChannel = Channel.CreateUnbounded<Position>();

            var listTask = new Task[11];

            var listStatus = new bool[10];

            for (int k = 0; k < listPlace.Count(); k++)
            {
                var place = listPlace[k];
                listTask[k] = Task.Run(async () =>
                {
                    int kk = k;
                    int count = 0;

                    Random r = new Random();

                    while (count < 10)
                    {
                        int id = r.Next(1, 1000);
                        var position = new Position()
                        {
                            ID = id,
                            Name = $"Postion{id}",
                            PlaceID = place.ID,
                            PlaceName = place.Name
                        };

                        Console.WriteLine($"WRITE: Position ID: {position.ID}, Postion Name: {position.Name}");
                        await _dataChannel.Writer.WriteAsync(position);
                        count++;
                    }

                    lock (listStatus)
                    {
                        if(count == 10)
                        {
                            listStatus[k] = true;
                        }

                        bool isStop = true;
                        
                        foreach(var status in listStatus)
                        {
                            if (!status)
                            {
                                isStop = false;
                            }
                        }

                        if (isStop)
                        {
                            _dataChannel.Writer.Complete();
                            Console.WriteLine("Stopped");
                        }
                    }

                });
            }


            listTask[10] = Task.Run(async () =>
            {
                while (await _dataChannel.Reader.WaitToReadAsync())
                {
                    await Task.Delay(100);

                    var data = await _dataChannel.Reader.ReadAsync();

                    Console.WriteLine($"READ: Position ID: {data.ID}, Postion Name: {data.Name}");
                }
            });

            await Task.WhenAll(listTask);

        }
    }

    public class Place
    {
        public int ID { get; set; }
        public string Name { get; set; }

        public static List<Place> InitData()
        {
            var listData = new List<Place>();

            for (int i = 0; i < 10; i++)
            {
                var data = new Place()
                {
                    ID = i,
                    Name = $"Postion{i}",

                };

                listData.Add(data);
            }
            return listData;
        }
    }

    public class Position
    {
        public int ID { get; set; }
        public int PlaceID { get; set; }
        public string PlaceName { get; set; }
        public string Name { get; set; }

        public static List<Position> InitData()
        {
            var listData = new List<Position>();

            for (int i = 0; i < 10; i++)
            {
                var data = new Position()
                {
                    ID = i,
                    Name = $"Postion{i}"
                };

                listData.Add(data);
            }
            return listData;
        }
    }


}

In case I want to create separate Channels for each Task, how do I Complete them? Please give me an example.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
quanchinhong
  • 142
  • 3
  • 18

2 Answers2

1

Using the iteration variable inside a task is problematic. i changes after the task is initialized. For example:

const int count = 10;
Task[] tasks = new Task[count];
for (int i = 0; i < count; i++)
{
    tasks[i] = Task.Run(() => Console.WriteLine(i));
}
await Task.WhenAll(tasks);

Will give the following output:

10
10
10
10
10
10
10
10
10
10

Locking does not help because i changes before the lock statement is reached.

Using a second variable inside the loop gives the expected result:

const int count = 10;
Task[] tasks = new Task[count];
for (int i = 0; i < count; i++)
{
    int j = i;
    tasks[i] = Task.Run(() => Console.WriteLine(j));
}
await Task.WhenAll(tasks);
6
3
1
0
2
4
5
7
8
9
Michael
  • 1,166
  • 5
  • 4
  • I want to focus on Channel – quanchinhong Aug 13 '21 at 05:13
  • This is a subsequent error. `listResult[k] = true;` uses always the last element (k is the iteration variable). So listResult will never be completely set to true, therefore isStop is always false and `_dataChannel.Writer.Complete();` is never reached. – Michael Aug 13 '21 at 05:22
  • Does the code run without exceptions at all? k does not set the last element, it sets one after it. That should cause an index exception. – Michael Aug 13 '21 at 05:26
  • I have tried using local variable instead iteration, but the complete still never reached – quanchinhong Aug 13 '21 at 05:52
  • Maybe listPlaces have less than 10 elements. For simplification I would count the completed Tasks instead of using a bool list. In your lock you increment the completed counter and check if completed == listPlace.Count after that. – Michael Aug 13 '21 at 06:08
  • ```completeTask++; if(completeTask == listTask.Count()) { _dataChannel.Writer.Complete(); } ``` I have tried this, but nothing change – quanchinhong Aug 13 '21 at 06:18
  • I can share live share in VS 2019, do you want to join? – quanchinhong Aug 13 '21 at 06:20
  • I'm at work right now, but I have no more ideas and can only recommend general troubleshooting: Instead of `_dataChannel.Writer.Complete()` write an output, comment out the code before until the condition is reached and then re-enable it step by step. – Michael Aug 13 '21 at 06:32
  • the solution you mention above is to use a counter, but the Channel is terminated before it is completed as intended. – quanchinhong Aug 13 '21 at 06:47
0

Here's the modified code.

I eliminated the lock and complicated logic there. Also fixed variable capture and added a sanity check on reads.

Comments are in line. Please try running and ask if you have questions.

public class MultipleChannel
{
    public static async Task RunMiltipleChannel()
    {
        List<Place> listPlace = Place.InitData();

        Channel<Position> _dataChannel = Channel.CreateUnbounded<Position>();

        var listTask = new Task[11];
        
        //Count the number of writer tasks that finished
        int completedTasks = 0;

        for (int k = 0; k < listPlace.Count; k++)
        {
            var place = listPlace[k];
            //Important to avoid closures
            var kCapture = k;
            listTask[kCapture] = Task.Run(async () =>
            {
                int kk = kCapture;
                int count = 0;

                Random r = new Random();

                while (count < 10)
                {
                    int id = r.Next(1, 1000);
                    var position = new Position()
                    {
                        ID = id,
                        Name = $"Postion{id}",
                        PlaceID = place.ID,
                        PlaceName = place.Name
                    };

                    Console.WriteLine($"WRITE: Position ID: {position.ID}, Postion Name: {position.Name}");
                    await _dataChannel.Writer.WriteAsync(position);
                    count++;
                }

                //Thread-safe check to see if this is the last task to complete
                if (Interlocked.Increment(ref completedTasks) == 10)
                {
                    _dataChannel.Writer.Complete();
                    Console.WriteLine($"Task {kk} finished, CHANNEL COMPLETED");
                }
                else
                {
                    Console.WriteLine($"Task {kk} finished");
                }

            });
        }

        //Make sure we read everything
        int readCount = 0;
        listTask[10] = Task.Run(async () =>
        {
            while (await _dataChannel.Reader.WaitToReadAsync())
            {
                await Task.Delay(100);
                var data = await _dataChannel.Reader.ReadAsync();
                readCount++;
                Console.WriteLine($"READ: Position ID: {data.ID}, Postion Name: {data.Name}");
            }
        });

        await Task.WhenAll(listTask);

        //Sanity check
        Console.WriteLine($"Read {readCount} position data");
    }
}

I can confirm channel close, and 100 items read.

Zer0
  • 7,191
  • 1
  • 20
  • 34
  • What is the difference between Interlocked and lock statements? – quanchinhong Aug 14 '21 at 03:52
  • @quanchinhong That's outside this discussion, but it eliminated a lot of code. In short `lock` creates mutual exclusion and possible blocking plus context switching (it's shorthand for `Monitor.Enter` and `Monitor.Exit`). Overkill in this case. The `Interlocked` classe is thread-safe, and typically implemented at the CPU instruction level and non-blocking. As opposed to the kernel level like `lock`. Obviously we need some way to synchronize access to `completedTasks` and `Interlocked.Increment` is perfect here and does not block. It's a thread-safe way to do an increment. – Zer0 Aug 16 '21 at 00:16