1

I need to have a class that will execute actions in thread pool, but these actions should be queued. For example:

method 1
method 2
method 3

When someone called method 1 from his thread he can also call method 2 or method 3 and they all 3 methods can be performed concurrently, but when another call came from user for method 1 or 2 or 3, this time the thread pool should block these call, until the old ones execution is completed.

Something like the below picture:

https://ibb.co/dBDY6YZ

Should I use channels?

Enigmativity
  • 113,464
  • 11
  • 89
  • 172
  • There already is a generic ConcurrectQueue class. You just need to pick wich type the T has to be. https://learn.microsoft.com/en-us/dotnet/api/system.collections.concurrent.concurrentqueue-1?view=netframework-4.8 – Christopher Aug 05 '19 at 01:03
  • In that Image, what is that 1-4 comming from the Main Thread? And why are they send to the functions? – Christopher Aug 05 '19 at 01:05
  • @Christopher well i consider them as calling sequence number,something like first call second call and so on...to visualize the process and architecture :( – aidin jalalvandi Aug 05 '19 at 01:15
  • @Christopher as you mentioned the CocurrectQueue is good for this solution.but how the class can determine which calling method should be add to queue? – aidin jalalvandi Aug 05 '19 at 01:17
  • You add the Elements to the queue in the exact order they should come out. | It is really hard to tell what your goal is. Right now there is a language barrier when reading your description. And your drawing is not really telling me what you want to do either. I would need to know the pattern you are shooting for to guess what you mean from this information. I need more information or you have to hope someone else can help you. – Christopher Aug 05 '19 at 01:18
  • So Method1 should run only at one thread at a time? An any other thread that tries to run concurrently this method should be blocked? – Theodor Zoulias Aug 05 '19 at 01:24
  • @Christopher let me tell you a real example.i am a client in server,i want to buy some stuff so i press add to cart button,mean while i will update my profile and add a comment on some topic.i want these request be perform in parallel.but when 5 time "add to cart" request came in same time,they should be added to a queue for this client.well the other clients have another thread. – aidin jalalvandi Aug 05 '19 at 01:28
  • "i am a client in server" That is the language barrier right there. You can not be a *client in the server*. You are either the client or the server of this specific interaction. – Christopher Aug 05 '19 at 01:31
  • @TheodorZoulias no no no.not blocking other threads.i'm not talking about blocking.i'm talking about queuing actions.queuing "add to cart" action.queuing "delete from basket" action.queuing "fire the light" action.but all these actions will be run async. – aidin jalalvandi Aug 05 '19 at 01:31
  • @Christopher sorry.my English is not very good.my bad! – aidin jalalvandi Aug 05 '19 at 01:32
  • "no no no.not blocking other threads" I never even talked about "blocking other threads". if anything you talked about that: "but when another call came from user for method 1 or 2 or 3,this time the thread pool should block these call" – Christopher Aug 05 '19 at 01:32
  • @Christopher i am a client in a server.i am an ant in a dessert.i am just client.what ever.just focus on the main point brother. – aidin jalalvandi Aug 05 '19 at 01:33
  • "i am an ant in a dessert" is a fully sensible, understandable sentence. Not hard to parse. "i am a client in a server" is not a sensible sentence. All my attempts to parse it return errors. – Christopher Aug 05 '19 at 01:39
  • @Christopher the thread pool should just blocking those Duplicate request.not other ones.or queuing them,this is a better answer.Queuing. – aidin jalalvandi Aug 05 '19 at 01:39
  • @Christopher well i can't be a stackoverflow user in quara server,can i? :| – aidin jalalvandi Aug 05 '19 at 01:41
  • So you want to do a shop application, or at least something that looks very similar? a) find a way to identify each client. Internet shops use a Session ID that has to be send with every request. Most others call it a Session ID too b) Have a "shopping card" collection for each Session ID. – Christopher Aug 05 '19 at 01:41
  • When a thread queues an action, what should happen next? 1) The thread awaits asynchronously for the action to complete or 2) The thread forgets about the action and continues doing other things? – Theodor Zoulias Aug 05 '19 at 01:43
  • @TheodorZoulias the pool thread will continuously calling those actions,but in sequentially way for each action.to answer you question the answer should be 2 i think. – aidin jalalvandi Aug 05 '19 at 01:46
  • @Christopher it's not shopping application,it's a game application,and i have already a session for each client.i just need that the application handle users request in parallel but in sequentially way. – aidin jalalvandi Aug 05 '19 at 01:47
  • OK, now I have enough info for an answer. I think you just need a list of `SemaphoreSlim`s (one per method). – Theodor Zoulias Aug 05 '19 at 01:48
  • @TheodorZoulias SemaphoreSlims is cool.but is there any better solution?any library.in shortest way? – aidin jalalvandi Aug 05 '19 at 01:51
  • Probably there are better solutions, but my knowledge about Channels, DataFlow and Reactive Extensions is very limited. – Theodor Zoulias Aug 05 '19 at 03:09

3 Answers3

2

Here is my suggestion. For each synchronous method, an asynchronous method should be added. For example the method FireTheGun is synchronous:

private static void FireTheGun(int bulletsCount)
{
    var ratata = Enumerable.Repeat("Ta", bulletsCount).Prepend("Ra");
    Console.WriteLine(String.Join("-", ratata));
}

The asynchronous counterpart FireTheGunAsync is very simple, because the complexity of queuing the synchronous action is delegated to a helper method QueueAsync.

public static Task FireTheGunAsync(int bulletsCount)
{
    return QueueAsync(FireTheGun, bulletsCount);
}

Here is the implementation of QueueAsync. Each action has its dedicated SemaphoreSlim, to prevent multiple concurrent executions:

private static ConcurrentDictionary<MethodInfo, SemaphoreSlim> semaphores =
    new ConcurrentDictionary<MethodInfo, SemaphoreSlim>();

public static Task QueueAsync<T1>(Action<T1> action, T1 param1)
{
    return Task.Run(async () =>
    {
        var semaphore = semaphores
            .GetOrAdd(action.Method, key => new SemaphoreSlim(1));
        await semaphore.WaitAsync();
        try
        {
            action(param1);
        }
        finally
        {
            semaphore.Release();
        }
    });
}

Usage example:

FireTheGunAsync(5);
FireTheGunAsync(8);

Output:

Ra-Ta-Ta-Ta-Ta-Ta
Ra-Ta-Ta-Ta-Ta-Ta-Ta-Ta-Ta

Implementing versions of QueueAsync with different number of parameters should be trivial.


Update: My previous implementation of QueueAsync has the probably undesirable behavior that executes the actions in random order. This happens because the second task may be the first one to acquire the semaphore. Below is an implementation that guaranties the corrent order of execution. The performance could be bad in case of high contention, because each task is entering a loop until it takes the semaphore in the right order.

private class QueueInfo
{
    public SemaphoreSlim Semaphore = new SemaphoreSlim(1);
    public int TicketToRide = 0;
    public int Current = 0;
}

private static ConcurrentDictionary<MethodInfo, QueueInfo> queues =
    new ConcurrentDictionary<MethodInfo, QueueInfo>();

public static Task QueueAsync<T1>(Action<T1> action, T1 param1)
{
    var queue = queues.GetOrAdd(action.Method, key => new QueueInfo());
    var ticket = Interlocked.Increment(ref queue.TicketToRide);
    return Task.Run(async () =>
    {
        while (true) // Loop until our ticket becomes current
        {
            await queue.Semaphore.WaitAsync();
            try
            {
                if (Interlocked.CompareExchange(ref queue.Current,
                    ticket, ticket - 1) == ticket - 1)
                {
                    action(param1);
                    break;
                }
            }
            finally
            {
                queue.Semaphore.Release();
            }
        }
    });
}
Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
2

To should i use channels?, the answer is yes, but there are other features available too.

Dataflow

.NET already offers this feature through the TPL Dataflow classes. You can use an ActionBlock class to pass messages (ie data) to a worker method that executes i the background with guaranteed order and a configurable degree of parallelism. Channels are a new feature which does essentially the same job.

What you describe is actually the simplest way of using an ActionBlock - just post data messages to it and have it process them one by one :

void Method1(MyDataObject1 data){...}

var block=new ActionBlock<MyDataObject1>(Method1);

//Start sending data to the block

for(var msg in someListOfItems)
{
    block.PostAsync(msg);
}

By default, an ActionBlock has an infinite input queue. It will use only one task to process messages asynchronously, in the order they are posted.

When you're done with it, you can tell it to Complete() and await asynchronously for all remaining items to finish processing :

block.Complete();
await block.Completion;

To handle different methods, you can simply use multiple blocks, eg :

var block1=new ActionBlock<MyDataObject1>(Method1);
var block2=new ActionBlock<MyDataObject1>(Method2);

Channels

Channels are a lower-level feature than blocks. This means you have to write more code but you get far better control on how the "processing blocks" work. In fact, you can probably rewrite the TPL Dataflow library using channels.

You could create a processing block similar to an ActionBlock with the following (a bit naive) method:

ChannelWriter<TIn> Work(Action<TIn> action)
{
    var channel=Channel.CreateUnbounded<TIn>();
    var workerTask=Task.Run(async ()=>{
        await foreach(var msg in channel.Reader.ReadAllAsync())
        {
            action(msg);
        }
    })

    var writer=channel.Writer;

    return writer;
}

This method creates a channel and runs a task in the background to read data asynchronously and process them. I'm cheating "a bit" here by using await foreach and ChannelReader.ReadAllAsync() which are available in C#8 and .NET Core 3.0.

This method can be used like a block :

ChannelWriter<DataObject1> writer1 = Work(Method1);

foreach(var msg in someListOfItems)
{
    writer1.WriteAsync(msg);
}

writer1.Complete();

There's a lot more to Channels though. SignalR for example uses them to allow streaming of notifications to the clients.

Panagiotis Kanavos
  • 120,703
  • 13
  • 188
  • 236
  • nice explanation.thanks a lot.you may know that i'm developing a multiplayer game,but this is very big messy work. – aidin jalalvandi Aug 05 '19 at 11:37
  • @aidinjalalvandi blocks and especially channels are even more useful in this case - you could create an "agent" for each active player/NPC/thing in general and send messages to it through its own channel. You can combine worker functions in pipelines, route messages from one worker to another etc. – Panagiotis Kanavos Aug 05 '19 at 11:49
  • @aidinjalalvandi or you can create "workers" that listen to multiple channels, and process higher priority messages first. They could read from a `killed` or `shutdown` channel first before proceeding with other messages. – Panagiotis Kanavos Aug 05 '19 at 11:50
  • is there any way to undo the operations when a task is canceld,something like animating something in game and then cancel it.is it called cache? – aidin jalalvandi Aug 05 '19 at 11:58
0

What about this solution?

public class ConcurrentQueue
{
    private Dictionary<byte, PoolFiber> Actionsfiber;
    public ConcurrentQueue()
    {
        Actionsfiber = new Dictionary<byte, PoolFiber>()
        {
            { 1, new PoolFiber() },
            { 2, new PoolFiber() },
            { 3, new PoolFiber() },
        };
        foreach (var fiber in Actionsfiber.Values)
        {
            fiber.Start();
        }
    }
        
    public void ExecuteAction(Action Action , byte Code)
    {
        if (Actionsfiber.ContainsKey(Code))
            Actionsfiber[Code].Enqueue(() => { Action.Invoke(); });
        else
            Console.WriteLine($"invalid byte code");
    }

}

public static void SomeAction1()
{
    Console.WriteLine($"{DateTime.Now} Action 1 is working");
    for (long i = 0; i < 2400000000; i++)
    {

    }
    Console.WriteLine($"{DateTime.Now} Action 1 stopped");
}
            
public static void SomeAction2()
{
    Console.WriteLine($"{DateTime.Now} Action 2 is working");
    for (long i = 0; i < 5000000000; i++)
    {

    }
    Console.WriteLine($"{DateTime.Now} Action 2 stopped");
}
            
public static void SomeAction3()
{
    Console.WriteLine($"{DateTime.Now} Action 3 is working");
    for (long i = 0; i < 5000000000; i++)
    {

    }
    Console.WriteLine($"{DateTime.Now} Action 3 stopped");
}


public static void Main(string[] args)
{
    ConcurrentQueue concurrentQueue = new ConcurrentQueue();

    concurrentQueue.ExecuteAction(SomeAction1, 1);
    concurrentQueue.ExecuteAction(SomeAction2, 2);
    concurrentQueue.ExecuteAction(SomeAction3, 3);
    concurrentQueue.ExecuteAction(SomeAction1, 1);
    concurrentQueue.ExecuteAction(SomeAction2, 2);
    concurrentQueue.ExecuteAction(SomeAction3, 3);

    Console.WriteLine($"press any key to exit the program");
    Console.ReadKey();
}

the output :

8/5/2019 7:56:57 AM Action 1 is working

8/5/2019 7:56:57 AM Action 3 is working

8/5/2019 7:56:57 AM Action 2 is working

8/5/2019 7:57:08 AM Action 1 stopped

8/5/2019 7:57:08 AM Action 1 is working

8/5/2019 7:57:15 AM Action 2 stopped

8/5/2019 7:57:15 AM Action 2 is working

8/5/2019 7:57:16 AM Action 3 stopped

8/5/2019 7:57:16 AM Action 3 is working

8/5/2019 7:57:18 AM Action 1 stopped

8/5/2019 7:57:33 AM Action 2 stopped

8/5/2019 7:57:33 AM Action 3 stopped

the poolFiber is a class in the ExitGames.Concurrency.Fibers namespace. more info :

How To Avoid Race Conditions And Other Multithreading Issues?

Community
  • 1
  • 1
  • Does this enqueues the actions for execution in the thread pool, as per the requirements of the question? – Theodor Zoulias Aug 05 '19 at 04:03
  • @TheodorZoulias ofcourse.i have tested it in multi thread way right now and it worked for me at least. – aidin jalalvandi Aug 05 '19 at 04:05
  • You may need to add `Thread.CurrentThread.ManagedThreadId` in the console output, to have a better idea about what's going on. – Theodor Zoulias Aug 05 '19 at 04:08
  • @TheodorZoulias right.i was a outputing the thread id right now.actually when i created a thread and thread start and call these enqueues for each thread ,from ConcurrentQueue class the thread id's are eqauls to the thread created.but in "SomeActions" methods the threads are different,because the poolfiber create a taskList that works in different thread according to above link.but i dont think so it will effect to my game.because i just need to handle the users input in parallel.but does this code affect any CPU issue?i mean,am i using the CPU too much for each client? – aidin jalalvandi Aug 05 '19 at 04:15
  • @TheodorZoulias look at this picture. https://ibb.co/ZVPVnvV . i puted a thread.sleep on each "SomeActions" method.so the most cost time for executing "SomeActions" method is 4 second.the programm end in 9 secod.there is 2 function call.2 * 4 = 8 second.is this good news or not? – aidin jalalvandi Aug 05 '19 at 04:46
  • I can't comment on this implementation because I am not familiar with the class `PoolFiber`. – Theodor Zoulias Aug 05 '19 at 05:25
  • @TheodorZoulias well the PoolFiber is like a Queue that will call functions.it's just as simple as it is. – aidin jalalvandi Aug 05 '19 at 07:31
  • @aidinjalalvandi queued processing is already provided either through TPL Dataflow classes or channels. You don't need any external classes – Panagiotis Kanavos Aug 05 '19 at 11:37