|
JNIPort for Dolphin Smalltalk |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Back to Goodies |
The Problems with ThreadsThis section is about the problems caused by the mismatch between the ways that the Java runtime and Dolphin use and understand operating system threads. The discussion may be quite off-putting, since it describes various ways of getting into trouble without expecting to. Of course, you can do that with any threaded code, but the Java/Dolphin mix is particularly hard to get right. There's nothing much more I can do to help with that; I've tried to make this section as comprehensible as I can, but it's still not an easy read. I offer the following guideline:
OverviewThe Java runtime from Sun implements Java threads as Windows threads. Other JVMs for Windows either do the same, or multiplex several Java threads onto each OS thread (I believe that both the IBM and BEA runtimes do this, or can be configured to do so). In either case the Java runtime is thread-intensive and is thread-aware.
Dolphin, on the other hand, executes all Smalltalk code in just one OS thread. All the
Smalltalk With a good deal of messing around, some determined turning of blind eyes to theoretical problems, and quite a bit of care, it is possible to reconcile these two systems. Almost all of the problems revolve around callbacks from the Java runtime into Dolphin. (The other problems are described below.) The Java runtime calls Dolphin code in two ways: when a Java “native method” is implemented by Dolphin code, and when the Java runtime's “hook” functions are implemented by Dolphin external callbacks. Later sub-sections go into more details, but the problem, and the general strategy for avoiding it, are the same in both cases. The General Problem
The problem is that the callback into Dolphin can be made from an OS thread that is not the one
that Dolphin uses for running Smalltalk code (call it the Smalltalk thread).
In an ideal world Dolphin would be able to execute Smalltalk code on any OS thread, in which
case these issues would vanish, but this is not an ideal world. Fortunately, Dolphin does have
a basic ability to handle such calls. When the Dolphin VM finds that one of its
Unfortunately that can lead to deadlocks. One way is because Dolphin cannot service callbacks while the Smalltalk thread is executing Java code:
The threads are now deadlocked; each waiting for the other.
Another way to create a deadlock is if the a background thread takes out a lock, and
then invokes an
The threads are deadlocked. A related, but not identical, source of deadlocks is:
The General “Solution”The technique that JNIPort uses for dealing with these case is to decouple the two threads wherever possible. It requires a queue and an additional thread. When the background thread wants to issue a callback, it does not call into Dolphin directly. Instead it places the request onto a queue, and then immediately returns to its caller. The additional thread, the demon thread, has been sleeping waiting for the queue to become non-empty. When it wakes up, it sees the notification, removes it from the queue and then calls back into Dolphin. The demon thread will be blocked at this point, waiting for Dolphin to finish handling the callback, but the original background thread has not had to wait, and so the deadlocks are avoided. Naturally, this technique can only be used when the caller is not interested in the answer to the call. There is an additional complication. If the code in the Java runtime that issues the call was running on the Smalltalk thread (because it had been invoked from Dolphin via JNI), then it should issue the callback directly, rather than putting it on the queue. For pure notifications (where no answer is wanted) side-stepping the queue is just avoiding a needless inefficiency. However if an answer is needed, then the caller cannot use the queue or else a different deadlock would occur, as follows:
Hook functions
The Java runtime has three hook functions. These are pointers to functions that
will be invoked by the runtime to allow client code to monitor the state of the JVM. In particular
this is used for the runtime's debugging/logging output, such as the trace of loaded classes
produced if the
A particularly common way that the hook functions can cause deadlocks is through the
Java logging/tracing feature. One example is Swing. When Swing is starting up, as the
first window is displayed, Swing starts a new Java thread to handle user-interactions.
So, in the first call to display a window, something like
The JNI Helper library is an external DLL that provides wrapper functions for external callbacks. The wrapper functions use the queue/demon approach internally to ensure that threads other than the Smalltalk thread are never blocked waiting for Dolphin to handle the notification. With the JNI Helper, as far as I know, all the deadlocks arising from logging are avoided. Native Methods, Notifications, and RequestsThe other way that a Java runtime can invoke code in Dolphin is via Java's “native methods”. A native method is a Java method that is declared but not defined in the Java code, instead an implementation is provided by external (to the JVM) code. The implementation can be provided by using JNI to set a pointer to a handler function (it can also be provided by a DLL but that is not relevant to JNIPort). As in the case of hooks, Dolphin's external callback feature can be used to create a “function pointer” that will actually cause Smalltalk code to be executed. But, again, there is a risk of deadlocks. JNIPort includes a Java library that uses the queue/demon approach to avoid or reduce the risk. In this case JNIPort can't eliminate the risk, since sometimes the caller does need to wait for an answer. For that reason Java callbacks are broken down into Notifications and Requests. Both go via the queue, but only the latter wait for an answer. See Callbacks for more information on how to use them. There is an additional reason for using the queue/demon approach for Java callbacks. It is unrelated to deadlocks, but is caused by a quirk of the way that JNI works with threads.
All normal JNI operations are invoked via a pointer to a In fact, whenever the Java runtime calls a native method, it creates new JNIEnv that will be valid only for the calling thread, and only until the callback returns. (The main point of this seems to be a half-baked attempt to make managing references to Java objects from C code slightly automatic.) That doesn't affect this discussion but does cause an additional problem that is described below.
The problem is that it makes it impossible to pass references to Java objects from Java to
Smalltalk!
The reason is as follows. Suppose that some Dolphin
Fortunately a slight modification to the queue/demon approach provides a way around the problem.
In JNIPort you are not encouraged to declare native methods and create your own
The Notifications and Requests are Java objects that hold a reference to the originator of the request, a parameter object (which, of course, may be an array), and a “tag” object that is used to identify the Smalltalk handler code. The mapping from tag object to handlers is held by the callback registry as described in Callbacks. The details of the interaction are quite complicated, and unfortunately have some unintuitive consequences for the order of events. To start with the simplest case, here's what happens when Java code, that happens to be running on the Smalltalk thread, sends a notification to Smalltalk:
The case where an answer is required is very similar. The differences are only that the Java code creates a Request object instead of a Notification; that the callback registry records the answer from the handler in the request object; and that the Java code (presumably) reads the answer and makes some use of it. The next case is slightly more complicated. If a notification is issued from a thread other than the Smalltalk thread, then there are three threads that are actively involved, as follows:
By the way, I'm glossing over the details of how various race conditions are avoided. They don't affect the overall design; see the code if you're interested. The next case is more complicated again. If a request is issued from a thread other than the Smalltalk thread, then it must wait for a reply:
One thing to note is that, for Notifications and Requests sent from the Smalltalk thread, the caller does not return until all pending callbacks have been handled. So, if there are a number of background threads submitting callbacks at the same time, the original Java code may be blocked until there happens to be a pause long enough for Dolphin to empty the queue. Another thing to note is that this scheme does not protect against all deadlocks. Notifications should be deadlock free, but if you are using Callbacks then you should ensure that you do not hold any locks on Java objects while you are waiting for the response. The last important point is that, as mentioned above, if the Smalltalk thread is not executing Smalltalk, then it is not available to handle callbacks. In particular code like the following can work unexpectedly:
If that method is called from Smalltalk, then it will block Smalltalk until it returns, so no callbacks can be handled until it does. If any of the worker threads issue Notifications, then Dolphin will not see them until after all the threads have died. If any of them makes a callback Requests then it will deadlock — the thread will be waiting for Dolphin to handle the callback, but the Smalltalk thread will still be blocked waiting for all the threads to complete. FinalisationJNIPort releases references to Java objects by finalisation. If you read the “guideline” at the beginning of this section, then you may be struck by the thought that the Dolphin finaliser Process will run even if the rest of Dolphin is passively handling callbacks from Java. The impression is correct, and the way that references to Java objects are cleared by finalisation does break the guideline. The problem is really more serious than just not adhering to an informal guideline. JNI specifies that “local” JNI references must be released by the same JNIEnv as created them. What's more, it insists (a severe and pointless restriction, in my view) that they cannot be released at all if there's another JNIEnv active at the time for the running thread. That means that finalisation cannot feasibly be used to clear local references unless JNIPort as a whole can be sure that only one JNIEnv is ever used.
That condition can be met only if no Java callbacks are ever used (the hook functions don't
cause a problem here). That is the reason that there are two slightly different subclasses
of the Smalltalk class Other ProblemsLastly, there are two race conditions that — as far as I can see — cannot feasibly be removed. In both cases there is a very small time period during which, if the luck were against you, JNIPort would use the wrong JNIEnv for some operation. It would probably be possible to remove the race conditions by adding yet more complexity, and putting Semaphore protection around every access to the JNIEnv. That would cause a considerable increase in the already large overhead involved in using JNIPort. Since I have not been able to produce any scenario where the problems manifest in practice, I have not taken that step. Possibly a future version of JNIPort will have a configurable policy of some sort rather than hard-wiring the decision.
The first case applies during a callback. As the callback is processed, i.e. when
the Dolphin VM invokes the
The other case is about exception checking of JNI calls, it is applicable even
if callbacks are not enabled. JNI uses a per-JNIEnv (and, therefore, thread-local)
flag to say 'an exception has been thrown from Java', and all JNI calls should check
the flag (all subsequent JNI calls will fail until it is cleared). Since all Dolphin
|
Copyright © Chris Uppal, 2003-2005
Java, JNI (probably), JVM (possibly), and God knows what else, are trademarks of Sun Microsystems, Inc.