18

I have many examples of Java bytecode, all of which I'd like to execute from Clojure. Each sequence of bytecode may contain an infinite loop, in which case I'd like to stop running it after a couple of seconds. I've been looking at futures as a means of doing this. Having hunted around for a couple of implementations I've tried both this code:

(deref (future (loop[a 1] (recur a)) :done!) 1000 :impatient!)

...and also the code at https://gist.github.com/3124000

In both cases, the loop appears to be timed out appropriately (and in the latter case the future is reported to have been both done and cancelled), but I see my CPU usage rise to 99% or thereabouts and stay there. I also see that each time I run this code, my Java process gains an extra thread.

It looks to me like the future is being cancelled, but the code is still running. In my program I will need to run, and time out, some very tight infinite loops (e.g. the Java bytecode equivalent of "20 PRINT GOTO 10") and I don't have the option of modifying the code that I'm running.

Any ideas why I'm seeing this behaviour; what I could do to prevent it; or alternative methods for me to realise my aim of running and timing out such code?

Tom Hume
  • 399
  • 1
  • 4
  • 12

3 Answers3

8

Java's supported mechanism for thread cancellation is interrupts. The .stop() method is deprecated for a reason - see the docs, Effective Java, Java Concurrency in Practice, etc.

In fact, the implementation of FutureTask (which backs future-cancel) is interruption-based.

As of today, each Clojure future is backed by an unbounded thread pool (just like send-off actions). So you may leverage the thread interruption primitives:

(def worker
  (let [p (promise)]
    {:future (future
               (let [t (Thread/currentThread)]
                 (deliver p t)
                 (while (not (Thread/interrupted))
                   (println 42)
                   (Thread/sleep 400))))
     :thread @p}))

Now one can successfully execute either (.interrupt (:thread worker)) or (future-cancel (:future worker)).

Although this couples the thread cancellation mechanism to that of Futures, the available Executor implementations are smart enough to clear the interrupted status that a previous task may have been set, so interruptions to one task never affect other tasks - even if they are run on the same thread.

The point is that preemptively killing threads is generally a bad idea - if you want to cancel tasks, you have to make them explicitly cancellable, in some or other way. OTOH, in your particular case you are executing opaque bytecode so there's no more option than resorting to .stop().

deprecated
  • 4,948
  • 3
  • 38
  • 58
4

The only way I've found to actual kill a the code executing inside a thread is to use the deprecated .stop method. In a lot of cases, the reason that it is deprecated isn't really important.

I had a function in clojail for doing just that. Feel free to snatch the function out or just pull in the library.

Rayne
  • 28,305
  • 16
  • 83
  • 100
  • That looks really useful, but when I do (thunk-timeout (loop [a 0] (recur a)) 10000) the function just hangs and CPU goes up... no timeout even after 10000ms. Any ideas? – Tom Hume Jul 17 '12 at 10:48
  • I spoke to soon; when I create a ThreadGroup and pass it into your thunk-timeout, a recurring function is indeed stopped. Thankyou :) – Tom Hume Jul 17 '12 at 19:31
  • why need ThreadGroup, just wrap it in fn is enough :) (thunk-timeout (fn [] (loop [a 0] (recur a)) 10000))) – o0omycomputero0o Dec 16 '15 at 03:57
  • How Not To Deprecate: have a super convenient method and replace it with some clunky work-around. Thanks for making it not clunky. – Kevin Kostlan Jan 05 '16 at 22:31
1

According to the Java API doc, cancel is not granted to be able to stop the task immediately.

Attempts to cancel execution of this task. This attempt will fail if the task has already completed, has already been cancelled, or could not be cancelled for some other reason. If successful, and this task has not started when cancel is called, this task should never run. If the task has already started, then the mayInterruptIfRunning parameter determines whether the thread executing this task should be interrupted in an attempt to stop the task.

Ning Sun
  • 1,987
  • 1
  • 18
  • 24