0

I want to have a thread that have a permanent lifetime (think of it like a process) and that manages a resource (a critical section in my case it's a 3G modem accessed via a serial port).

Other threads will send requests to this thread via a method and wait until it sends a response or a timeout has expired.

I can achieve this with the Qt C++ framework but I didn't find an easy way to do it in C# (I already asked a question the other time and someone told me about the threading context but he didn't answer my question), I have to write something like Qt's signal/slot system and it's a tedious task.

A colleague told me to take a look at AsyncEx but I haven't found an example that illustrates what I want to achieve and I'm not sure if it will help (neither will the async/await stuff).

It really pisses me off because I have a C# buggy code that uses locks (concurrency bugs and no it's not me who wrote it) and I would like to use this threading model to refactor it.

It's unbelievable that only Qt has done something powerful to handle this case (thread aware signals/slots and event loops), the others only offer useless garbage.

Update : In Qt, we can't invoke a method with timeout. It will block until the other thread sends a reply and according to this https://bugreports.qt.io/browse/QTBUG-63870 it's better to perform a non-blocking request...

Update 2 : Following JonasH's answer, I made this example : https://github.com/embeddedmz/message_passing_on_csharp which could be useful for people who share the same point of view as me concerning the threading model (message passing) which in my opinion is much less harmful than others (you know the multi-threaded programs which work well most of the time, for me it's binary it's either works all the time or nothing).

I will share another solution found by my work colleagues when they will show it to me in a future update.

Aminos
  • 754
  • 1
  • 20
  • 40
  • Does it really need to be one flag? Surely it's fine if only one thread can access the resource at a time? – ProgrammingLlama Jun 09 '22 at 08:51
  • Related: [C# Moving Database to a separate thread, without busy wait](https://stackoverflow.com/questions/58379898/c-sharp-moving-database-to-a-separate-thread-without-busy-wait) – Theodor Zoulias Jun 09 '22 at 10:16
  • In retrospect this question looks more like an advertisement for Qt, whatever that is, than like a genuine quest to solve a problem. You should at least include a link to the Qt home page, so that we know what you are talking about! :-) – Theodor Zoulias Jun 09 '22 at 13:54
  • You can take a look to this other question I have asked in the past : https://stackoverflow.com/questions/67418826/possibility-of-converting-a-c-qt-threads-using-event-loops-to-c-sharp-or-java and if you read the comments on JonasH anwser you will see that I created a github repo to test his answer. – Aminos Jun 09 '22 at 14:36
  • @TheodorZoulias see this also : https://github.com/dotnet/runtime/discussions/60474 – Aminos Jun 09 '22 at 14:40
  • 2
    Re, "It's unbelievable that only Qt has done something powerful to handle this case." You should trust your instincts there. One reason why some assertion might _seem_ unbelievable, is that it's complete bollox. The use-case that you are describing is as old as multi-threading itself. Practically _every_ multi-threading library provides the tools you need to write a long-running "server" thread that manages some resource on behalf of other threads. – Solomon Slow Jun 09 '22 at 15:09
  • @SolomonSlow OK talk is cheap, show me how to do it with C# or even Java and exactly as with the facilities that Qt offers (different type of connections between signal/slots, event loop etc...) and help me with creating an example, I have just started here : github.com/embeddedmz/message_passing_on_csharp – Aminos Jun 09 '22 at 15:12
  • 2
    Sorry, I guess I didn't understand that you want to solve the problem "exactly as with the facilities that Qt offers," and I didn't know that a high-quality C# binding for Qt might be a hard thing to find. I can't help you with that because I don't really know "the facilities that Qt offers." I've never personally written code to call the Qt API in any project. – Solomon Slow Jun 09 '22 at 15:28
  • 1
    Take a look at Microsoft's Reactive Framework and the `EventLoopScheduler` implementation. – Enigmativity Jun 10 '22 at 11:52
  • @Enigmativity what is the purpose of this framework ? – Aminos Jun 10 '22 at 12:45
  • 1
    @Aminos - The Reactive Framework is an event-like system that turns `IEnumerable` (a pull-based collection) to an `IObservable` (a reactive or push-based collection). It abstracts away the threading elements of coding and allows you to use LINQ when composing observables. It contains a number of schedulers that can be explicitly used, with `EventLoopScheduler` being a single-threaded, long life-time scheduler. NuGet `System.Reactive`. – Enigmativity Jun 10 '22 at 22:08
  • Looks complicated. Thanks anyway, I found the class on the github repo, I will take a look at it one day. – Aminos Jun 11 '22 at 09:54

1 Answers1

2

One way would be with a shared LimitedConcurrencyLevelTaskScheduler (see example). That could ensure only a single thread has exclusive access to the resource at any one time, while not needing the block a thread when the resource in not needed.

The typical way to write this functionality yourself would be with a blocking collection. I.e. the worker thread would run a foreach loop over the GetConsumingEnumerable and process each item. A very simplified example could look something like:

        private TResource resource;
        private BlockingCollection<Action<TResource>> queue = new();
        public void Start() => Task.Run(ThreadMain);
        public Task<T> Enqueue<T>(Func<TResource, T> method)
        {
            var tcs = new TaskCompletionSource<T>();
            queue.Add(r => tcs.SetResult(method(r)));
            return tcs.Task;
        }
        private void ThreadMain()
        {
            foreach (var action in queue.GetConsumingEnumerable())
            {
                action(resource);
            }
        }

But you would need to add things like error handling, cancellation, stopping the thread etc.

Another alternative might be a semaphoreSlim with a limit of one, and use WaitAsync with async/await to ensure exclusive access. I don't think a semaphore would guarantee any particular ordering if that is important to you. You may also risk blocking the UI thread if the work takes time.

There are also frameworks like DataFlow that might help, but I'm not familiar enough with it to provide any recommendation.

For a introduction to task based programming, see Task-based asynchronous programming and Asynchronous programming with async and await.

JonasH
  • 28,608
  • 2
  • 10
  • 23
  • This was the first thought I had, so +1. I do wonder if OP really just needs a semaphore that limits access to the resource to one thread at a time though. – ProgrammingLlama Jun 09 '22 at 09:00
  • @DiplomacyNotWar The thread that manages the resource is just a loop that dequeues a thread safe list, performs the request of the thread that enqueued it and then sends a reply to this last. I checked that Qt doesn't provide a timeout so the call will block until the thread sends a reply (https://bugreports.qt.io/browse/QTBUG-63870). – Aminos Jun 09 '22 at 09:15
  • @JonasH If a sending thread is expecting a reply (e.g. a string), how to do it nicely ? – Aminos Jun 09 '22 at 09:21
  • 1
    @Aminos Use `Task`, as shown in the example. Most communication between threads should be done thru Tasks. With a SemaphoreSlim or a LimitedConcurrencyTaskScheduler you would get tasks automatically, with a separate thread you need to use TaskCompletionSource to create tasks. – JonasH Jun 09 '22 at 09:24
  • @JonasH can you please code an example and share it on github? I often need to implement what I described in my question in C#. I once used a blocking collection to send objects representing decoded frames retrieved from a serial port managed by a secondary thread to a consumer who uses the main thread but this is a simple case. If I have several threads, I will have to code several blocking collections: one per thread and it's ugly and not dynamic. Isn't there a simple but reliable solution to this problem which I think is very common (Qt provides a solution that I like). – Aminos Jun 09 '22 at 12:37
  • @Aminos I do not have any more complete example. But it should not be particularly difficult encapsulate all the synchronization into a single class that can be reused. I'm not sure what it is you are having a problem with. – JonasH Jun 09 '22 at 12:42
  • @JonasH What can I do with the Task object returned by Enqueue ? can the thread that called Enqueue, use that object to wait the result ? if so, can you add an example in your answer. Thanks. – Aminos Jun 09 '22 at 13:34
  • 1
    +1. A more polished implementation would `catch` an error in the `method()` invocation, and with propagate this error through the [`SetException`](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.taskcompletionsource-1.setexception) method. – Theodor Zoulias Jun 09 '22 at 13:38
  • 1
    @Aminos Task literally have a `.Wait()` method. But if you are doing *anything* in .net that involves concurrency, you really need to learn about tasks and async/await. Added a few links in the answer, but there are plenty of other resources if you look around. – JonasH Jun 09 '22 at 13:44
  • @JonasH I did but I avoid this stuff because it's hard (and we need a thread context that is not available with console apps). So I avoid them because they will belong to the set of the problems I have to deal with. Once again, Qt FTW. – Aminos Jun 09 '22 at 13:45
  • @JonasH I made an example here https://github.com/embeddedmz/message_passing_on_csharp but the IDE is telling me on this line "queue.Add(() => tcs.SetResult(method()))" that Action doesn't accept 0 arguments. – Aminos Jun 09 '22 at 14:29
  • 1
    @Aminos Oups, it should be `queue.Add(r => tcs.SetResult(method(r)))` – JonasH Jun 10 '22 at 06:45
  • 1
    And get rid of the while loop, it does not work as you intend it to. Use `BlockingCollection.CompleteAdding` to finish the queue, allowing the foreach loop to exit. – JonasH Jun 10 '22 at 06:48
  • @JonasH on the doc, I can see that there's a version that take a cancellation token. Otherwise, I can use Try take with a timeout period. – Aminos Jun 10 '22 at 07:34
  • @JonasH I have finished the example, can you take a look at it, execute it and tell me what you think about it ? Thanks. – Aminos Jun 10 '22 at 16:01
  • 1
    @Aminos You really need to add error handling like Theodor mentioned, and I would still recommend using completeAdding instead of tryGet. Add a timer to inject tasks if you need to do something periodically. – JonasH Jun 12 '22 at 10:24
  • @JonasH I prefer TryTake since I want to some other stuff in the thread managing the resource (updating the status of the resource and firing events). – Aminos Jun 13 '22 at 09:55
  • @Aminos You should handle that by using a timer to create a task for updating the status. And you should also allow the user to shutdown the queue and wait for all queued items to be processed. But you are of course welcome to ignore any advice, It is not *me* that will suffer from such design problems. – JonasH Jun 13 '22 at 11:08
  • @JonasH but if I use C# timers, I will return to the main problem since timers run on different threads and I want the resource to be managed by a single thread (a gatekeeper). I agree that I must process all queue elements before exiting the program (cancellation token). – Aminos Jun 13 '22 at 12:57
  • 1
    @Aminos not if you treat a status update like any other task and put them onto the queue. – JonasH Jun 13 '22 at 13:39
  • @JonahH I can't enqueue methods that return void. What can I do ? thanks ! – Aminos Jun 14 '22 at 08:40
  • @aminos See https://stackoverflow.com/questions/11969208/non-generic-taskcompletionsource-or-alternative – JonasH Jun 14 '22 at 08:45
  • @JonasH so for methods that returns void I am obliged to change the return type to bool and return a dummy value ? I can do that on the lambda I enqueue so it doesn't matter, it's not very pretty but it's OK. Thanks – Aminos Jun 14 '22 at 08:58