In this post I consider the concept and its implementation (so far in the initial, but working stage), which has recently become very attractive for me. I didn’t have any experience in programming on signals, so I could have missed something or thought it was not optimal, so I’m writing here. I hope for qualified feedback and advice. Despite the fact that the library has just started to develop, I have already started using it in real projects, on real workload, this helps to quickly understand what is really needed and where to go next. So all the above code is in working condition, compiled and ready to use. The only thing is done on Framework 4.5, but I do not think that it will be an obstacle for someone, but if the idea turns out to be worthwhile, it will not be rebuilt under 3.5 problems.
What is wrong with the current paradigm
The device of a normal application on .NET implies that we have a set of classes, classes have data, and the methods that process this data. Also, our classes need to know about each other, about public methods, properties and events. That is, we have a strongly connected architecture. Of course, we can reduce connectivity, build interaction exclusively through interfaces and factories (which will increase the code size by a factor of two, and significantly complicate readability), we can remove open methods and cost everything on events, we can think of a lot of things, but go to the loosely coupled architecture anyway it will not work, we get at best a “medium” connection.
')
Yes, and there is still such a thing, which with the development of processors becomes more and more relevant, it is asynchronous, microsoft does a lot of good in this direction, the same PLINQ, any sugar like await, but all this is done in the usual framework of the OOP, and we you still have to create threads yourself, even if in the form of tasks, but by yourself. It is necessary to track the end of the execution of tasks to determine when the resources will become unnecessary.
In general, all this gradually becomes annoying, it becomes too lazy to write the same things in each new project, when it would be more correct to focus on the logic of the task.
Formalization of the new rules of the game
To begin with, we introduce a hard division, there is data, and there is a code of business logic (hereinafter simply logic), data is classes that (suddenly) contain data, and (since we have .NET, not Erlang), methods and properties to facilitate their presentation. It makes no sense to completely remove the methods when we can combine the advantages of the two approaches.
Classes of logic operate on data, and communicate with each other using signals. At the same time, they do not contain any public methods, properties, or events other than the constructor and destructor (or implementation of the IDisposable interface).
It is also logical to make logic classes in the form of singletons, but not necessarily, it all depends on the task and your decision.
The logic class contains its own internal methods, and signal handlers, it also has the right to generate new signals itself, which can be processed in any other logic class (even in itself).
The signal is an identifier, and, optionally, some kind of payload (reference to data in memory).
You can do anything with a signal identifier, a string, a GUID, etc., for myself I chose an enumeration and its value as it, mainly because I like
IntelliSense , I haven’t yet come up with the best. Also with this approach, it will not be possible to make a mistake when generating or subscribing to a signal, as, for example, in the case of string identifiers.
As a payload, the most frequent is the transfer of data by reference, and here we have to observe another important rule, the data should not be changed in the handlers until it is controlled, and remains on the programmer’s conscience. This rule arises from the fact that any number of handlers in different classes can be subscribed to one signal, and a change in the data in one of them will lead to a failure in the others. (I think that's why there is a restriction in the Erlang prohibiting reassigning variables).
One more important rule, we should forget about threads / tasks, and about any other code parallelization in logic classes, the library is also responsible for this, the next paragraph will show how this is achieved. This requirement is especially important to comply with, if we need to establish the fact that the signal processing has ended by all subscribers.
An example from the code below, we have a temporary file storage in which files are placed only at the processing stage, when a file appears, the storage gives a signal, all subscribers start processing (in parallel), read the file, write a log about its appearance, after processing ends, the repository receives (automatically) a signal that everyone who wanted to have done everything they wanted with this file, and no one needs it anymore, which means it can be safely removed. If some handler creates a thread and returns control to the library, the file can be deleted before the code in the manually created thread reads it, and we have a hard-to-catch failure.
Apply new rules
Initialization:
An enumeration whose value is used as a signal:
An example of a class (link to the code at the end of the article, the code itself from the task below) containing a signal handler:
An example of signal generation and signal processing completion handler with all synchronous and asynchronous processors.
[rtSignalAsyncHanlder(DirectoryWatcherSignal.ChangedDirectory)] void NewFileHandler(rtSignal signal) { string path = (string)signal.State; ......................................................................
The following attributes are available for specifying signal handlers:
- [rtSignalHanlder (SignalID)] - an attribute of the signal handler that will be called synchronously
- [rtSignalAsyncHanlder (SignalID)] - an attribute of the signal handler that will be invoked asynchronously
- [rtSignalCompletedHanlder (SignalID)] - attribute of the method that receives the signal when all the signal handlers have completed their work (including asynchronous)
- [rtSignalCompletedAsyncHanlder (SignalID)] - an attribute of the method that receives the signal, when all the signal handlers have completed their work (including asynchronous), the method is executed asynchronously
To generate a signal, use the following format:
rtSignalCore.Signal();
or
rtSignalCore.Signal(, _);
probably worth coming up with something more beautiful until it comes down.
What solves the signal approach
- Asynchrony becomes a consequence, and does not require additional efforts, no need to create threads / tasks, everything is achieved by marking handlers with necessary attributes.
- Weak code connectivity, business logic classes do not need to know about each other at all, it’s enough to describe the possible signals.
- Ease of testing individual components, due to the removal of hard links between classes.
- Ease and readability of the code
We try to put into practice
For convenience, we come up with a problem that we solve as with the help of the proposed approach, and in the usual way for comparison.
Task:
There is an input directory in which files appear, when a file appears, move it to the buffer, perform actions on it, after which we move it to the archive and write the result of processing to the log. What kind of treatment in this case does not matter.
As an example, I wrote two applications to solve this problem, one using signals, the second is normal. Programs are completely identical, except for the way classes interact.
Sketches in the case of no signals

Using signals where classes do not know about each other’s methods and events, and generally don’t know about the environment:

Charts from the profiler for the test on 11210 files of small size:
No signals:

Using signals:

By the graphs and the actual usage, it is clear that the introduction of signals did not cause any damage to the performance; rather, on the contrary, the processing is stable regardless of the number of files.
Conclusion
Once again, I was convinced of the universality of the C # language; indeed, it is possible to program on it using any paradigm, and if not, then finish it to the desired state.
While it is difficult to judge how effective the use of signals in the .NET environment, it is difficult to immediately drop the usual writing style and start thinking in the framework of the new model. Subjectively, the code becomes lighter and asynchrony is the result of a new model, which also pleases. Objectively - it will be clear with time. At the moment it is clear that the performance does not affect the worse. For myself, I decided that I would try to switch to this programming model and continue to develop the library and tools.
Already before publication, I found materials on
signals and slots in QT , in general, the ideas are similar, but in QT I did not find whether it was possible to determine whether the signal processing was finished from all slots.
I don’t know whether there have already been attempts to implement a similar model in .NET, if there have been links to share, it’s interesting to compare the approaches.
The project at sourceforge (everything is bad with English, if you find errors there, please unsubscribe)
UPD # 1: thanks to the user
mayorovp , a good comment on signal generation and handlers, you can now write handlers with any number of arguments, of any type, and pass these arguments when generating a signal (with checking the correspondence of the transmitted types at runtime).
Examples:
- No argument
- An example of passing a single argument of type string
- An example with two arguments
The performance drop on the tests is not noticed.