📜 ⬆️ ⬇️

Queuing calls to the native library from managed code with Dispatcher

In the recent past, I encountered the following problem: in an ongoing project (under .net), it was necessary to organize interaction with external resources (hardware, a specific full-text database). These resources were accessed by means of libraries containing API functions that were written using different languages ​​(C ++, Delphi), and united their one property: they did not support calls from different threads. While the architecture of the application being developed, dictated by the functional requirements, implied the need to access these resources from various threads. The most logical way out was to create a separate thread for access to each library (resource) and transfer all requests to this thread with queuing as necessary.

Formulation of the problem


Thus, the problem to be solved can be formulated as follows: it is required to develop a mechanism allowing to execute a code segment (execution element) in the context of a given (target) thread, regardless of the context in which the execution was initiated. A derived requirement is the queuing of execution elements, since the target thread can execute no more than one execution element at a time.

Implementation options


The first option that comes to mind is the use of a queue of objects of the Action class:
')
ConcurrentQueue<Action> _actions; 


At the same time, client threads add execution elements to it:

 _actions.Enqueue(() => { < > }); 


And the target thread performs them in the order of receipt:

 Action action; if (_actions.TryDequeue()) { action(); } 


This implementation certainly has a right to exist, but frontal implementation in this case will not be optimal in terms of resource use and performance, and more advanced techniques will require much more time to develop and test and are likely to be the invention of the bicycle. Therefore, this article will consider a more elegant way to solve the problem, using the tools provided by the .NET Framework. Namely, System.Windows.Threading.Dispatcher, which probably works similarly to the description above and uses exclusive locks.

Implementation


Suppose that we have an ExternalInterface class that implements an interface to an external library, the method call of which must occur in a separate thread:

 internal static class ExternalInterface { public static void Method1() { throw new NotImplementedException(); } public static int Method2() { throw new NotImplementedException(); } public static int Method3() { throw new NotImplementedException(); } private static void ExternalMethod1() { //    Thread.Sleep(1000); } private static int ExternalMethod2() { //    Thread.Sleep(1000); return 1; } private static void ExternalMethod3() { //    Thread.Sleep(1000); } } 


Where ExternalMethod1, ExternalMethod2 and ExternalMethod3 are the imported methods of the external library, and the Method1, Method2 and Method3 methods are class interface methods whose only task is to execute the imported methods in the context of the same selected stream.
There is also a client class (in the simplest case, the Program class of the console application), in which the ExternalInterface methods are called from various threads:

 class Program { static void ThreadMethod1() { ExternalInterface.Method1(); } static void ThreadMethod2() { ExternalInterface.Method2(); } static void ThreadMethod3() { ExternalInterface.Method3(); } static void Main(string[] args){ } } 


We now turn to the organization of calls to imported methods in one thread. To begin with, we will create a stream in the context that the call of the imported methods will pass, and prepare it for the waiting calls:

 //     private static Thread _dispatchingThread; // Dispatcher      private static Dispatcher _dispatchObject; public static void Open() { //     _dispatchingThread = new Thread(DispatchingThreadMethod); _dispatchingThread.Start(); } private static void DispatchingThreadMethod() { Console.WriteLine(Thread.CurrentThread.ManagedThreadId); //  ,    // ... //  Dispatcher'  _dispatchObject = Dispatcher.CurrentDispatcher; //      Dispatcher.Run(); //  ,    // ... } 


Next, simplify the transfer of the execution element to the target thread:

 private void Dispatch(Action action) { if (_dispatchObject != null) { _dispatchObject.Invoke(action); } } 


However, such an implementation allows several threads to pass execution elements to the target thread, which will lead to interlocking of calling threads (this behavior is observed only when using specific native libraries and is not reproduced in this example, that is, in some cases this step can be skipped). You can queue requests using an exclusive lock. Change the code as follows:

 private void Dispatch(Action action) { if (_dispatchObject != null) { lock (_dispatchObject) { _dispatchObject.Invoke(action); } } } 


Now we implement the interface methods:

 public static void Method1() { Dispatch(() => ExternalMethod1()); } public static int Method2() { int result = 0; Dispatch(() => { result = ExternalMethod2(); }); return result; } public static void Method3() { Dispatch(() => ExternalMethod3()); } 


To correctly end the_dispatchingThread thread, add another method to the ExternalInterface class:

 public static void Close() { _dispatchObject.InvokeShutdown(); } 


To prove that the call to all methods will take place in the same thread, execute the following code:

 static void Main(string[] args) { ExternalInterface.Open(); //       Dispatcher //     Thread.Sleep(1000); new Thread(ThreadMethod1).Start(); new Thread(ThreadMethod2).Start(); new Thread(ThreadMethod3).Start(); Console.ReadKey(); ExternalInterface.Close(); } 


The result is a fourfold printing of the same thread identifier, which means that all method calls were executed in the context of a single thread.

In conclusion, I would like to say that the use of this code in serious projects will require some improvements, since the purpose of the article was to show only the mechanism for solving the problem. The same example does not limit the scope of this mechanism. This approach can be used in any situation where work from one thread is required, but it is impossible to work from the main program flow. A similar approach would also be appropriate when queuing the execution elements for a certain stream.

Source: https://habr.com/ru/post/149135/


All Articles