Using externals in multithreaded applications

by Manu (modified: 2008 Jan 07)

Using a C/C++ external in an Eiffel system is pretty easy. Simply do a C/C++ inline and provide the arguments to the C/C++ externals.

When transforming the same system into a multithreaded application one has to pay attention to something else: C/C++ externals that may take a long time to execute.

Using blocking qualifier

The issue is specific to the Eiffel Software GC implementation which is a stop the world type of GC. In other words, when a GC cycle is triggered, all the running threads have to be stopped. However, when a thread is executing an external C/C++ code, the Eiffel runtime has no way to stop that thread reliably on all the supported platforms. Consequently, the GC cycle will start only after the external C/C++ code has completed and this could cause an unacceptable delay.

The adopted solution was to add a new qualifier to C/C++ externals: blocking. This keywords directly follows to the C or C++ keyword in the external specification. Here is an example taken from the EiffelBase library {EXECUTION_ENVIRONMENT}.sleep:

sleep (nanoseconds: INTEGER_64) is -- Suspend thread execution for interval specified in -- `nanoseconds' (1 nanosecond = 10^(-9) second). require non_negative_nanoseconds: nanoseconds >= 0 external "C blocking use %"eif_threads.h%"" alias "eif_thr_sleep" end

Since it is cumbersome to have to go through all the externals and add the missing blocking qualifier, one may ask, why it isn't the default behavior. There are 2 elements of response:

  1. C/C++ externals that last a long time are usually rare.
  2. Making a C/C++ externals blocking by default would potentially cause race conditions (thus random results/crashes) when the C/C++ externals call back to Eiffel code.

This is really point #2 that made us choose the current described solution of adding the blocking qualifier wherever it was needed, since when you have to choose between a deadlock and a crash, the deadlock alternative is much easier to debug.

To figure out when you need to put a blocking qualifier, simply execute your program and when it seems to freeze, look at the call stacks for the various threads in a C debugger. Usually, the scenario is the following:

  1. One thread is taking all the CPU by looping
  2. All the other threads are blocked in some thread synchronization routines
  3. One thread is executing a C external call.

That's it, #3 is the place where you need to add the blocking qualifier.

Callbacks from C to Eiffel

Let's tackle a more complicated scenario where you have a C/C++ external calling some Eiffel code. Here is what you need to do. First, the C/C++ externals needs to be marked blocking, then in the C/C++ code where you do a call back to the Eiffel code, you need to prefix and suffix the call by the following macros EIF_ENTER_EIFFEL and EIF_EXIT_EIFFEL. The reason is that when executing the C code for a blocking C external, it is as if the runtime forgot about the existence of that thread. Calling EIF_ENTER_EIFFEL simply tells the runtime that the thread is again live. Calling EIF_EXIT_EIFFEL has the effect of making the runtime to forget the existence of that thread.

Moreover before calling the Eiffel routine, you need to ensure that no GC cycle is happening, as it is possible that while you were calling EIF_ENTER_EIFFEL the runtime was performing a GC cycle and you do not want to execute some Eiffel code while the GC is in a cycle. To wait for the completion of any pending GC cycle, one has to use the RTGC macro.

For example, here is what one would typically have:

my_c_routine is external "C blocking use %"some_header_file.h%"" end

void my_c_routine (void) { /* Some C code here. */ EIF_ENTER_EIFFEL; RTGC; your_eiffel_routine_call (eif_access(my_object), ....); EIF_EXIT_EIFFEL; /* Some other C code here. */ }

External threads callbacks

If your callback is performed in a thread which was not created by the Eiffel runtime through the EiffelThread library, then some more code has to be added.

The first time your external code wants to call some Eiffel code, you need to initialize the runtime for that particular thread of execution by calling the EIF_INITIALIZE_AUX_THREAD macro.

Once it is initialized, you can perform the call back in the same manner as in the second part of the article.

After the completion of the call, you have 2 possibilities:

  • Clear the data register for the current thread (meaning you know that this thread is never to execute Eiffel code again)
  • Wait for a notification that the current thread is going to terminate.

In both cases, you need to manually call eif_thr_exit to free the Eiffel runtime allocated memory for that thread.

To summarize your C callback routine could look like:

void my_c_routine (void) { EIF_INITIALIZE_AUX_THREAD; EIF_ENTER_EIFFEL; RTGC; your_eiffel_routine_call (eif_access(my_object), ....); EIF_EXIT_EIFFEL; eif_thr_exit(); }

I hope this tutorial was helpful.

Comments
  • Chris Saunders (14 years ago 22/4/2010)

    Wording

    Hi Manu. Here is a quote from this tutorial: "Consequence, the GC cycle will start only". My native tongue is English and this looks incorrect to me. I would word it as the following: "Consequently, the GC cycle will start only". I don't think the comma is needed here but I do not think it is really incorrect.

    • Manu (14 years ago 22/4/2010)

      Thanks, I've fixed that.