Подтвердить что ты не робот

Как я могу заставить внешние методы прерываться?

Проблема

Я запускаю несколько вызовов некоторого внешнего метода с помощью ExecutorService. Я хотел бы иметь возможность прервать эти методы, но, к сожалению, они не проверяют флаг прерывания сами по себе. Есть ли способ заставить исключение быть поднятым из этих методов?

Я знаю, что исключение из произвольного местоположения потенциально опасно, в моем конкретном случае я готов воспользоваться этим шансом и подготовленный для решения последствий.

Подробнее

Посредством "внешнего метода" я подразумеваю некоторый метод (ы), который поступает из внешней библиотеки, и я не могу изменить его код (ну, я могу, но это сделает его кошмаром обслуживания всякий раз, когда выпущена новая версия).

Внешние методы вычисляются дорого, а не IO-bound, поэтому они не реагируют на обычные прерывания, и я не могу принудительно закрыть канал или сокет или что-то еще. Как я уже упоминал ранее, они также не проверяют флаг прерывания.

Код концептуально выглядит как:

// my code
public void myMethod() {
    Object o = externalMethod(x);
}

// External code
public class ExternalLibrary {
    public Object externalMethod(Object) {
        innerMethod1();
        innerMethod1();
        innerMethod1();
    }

    private void innerMethod1() {
        innerMethod2();
        // computationally intensive operations
    }

    private void innerMethod2() {
        // computationally intensive operations
    }
}

Что я пробовал

Thread.stop() теоретически сделает то, что я хочу, но не только он устарел, но также доступен только для реальных потоков, тогда как Я работаю с задачами исполнителя (которые также могут делиться потоками с будущими задачами, например, при работе в пуле потоков). Тем не менее, если не найдено лучшего решения, я конвертирую свой код для использования старомодных потоков вместо этого и использую этот метод.

Другой вариант, который я пробовал, - отметить myMethod() и подобные методы специальным аннотацией "Прерывание", а затем использовать AspectJ (который я, по общему признанию, новичок), для того, чтобы поймать все вызовы методов там: что-то вроде:

@Before("call(* *.*(..)) && withincode(@Interruptable * *.*(..))")
public void checkInterrupt(JoinPoint thisJoinPoint) {
    if (Thread.interrupted()) throw new ForcefulInterruption();
}

Но withincode не является рекурсивным для методов, вызванных методами сопоставления, поэтому мне пришлось бы отредактировать эту аннотацию во внешний код.

Наконец, это похоже на предыдущий мой вопрос - хотя заметная разница в том, что теперь я имею дело с внешней библиотекой.

4b9b3361

Ответ 1

Я взломал уродливое решение моей проблемы. Это не очень, но это работает в моем случае, поэтому я отправляю его здесь, если он поможет кому-то еще.

Что я сделал, это профиль частей библиотеки моего приложения, надеясь, что я могу выделить небольшую группу методов, которые вызываются повторно - например, некоторые методы get или equals() или что-то вдоль этих линий; а затем я мог бы вставить следующий сегмент кода:

if (Thread.interrupted()) {
    // Not really necessary, but could help if the library does check it itself in some other place:
    Thread.currentThread().interrupt();
    // Wrapping the checked InterruptedException because the signature doesn't declare it:
    throw new RuntimeException(new InterruptedException());
}

Либо вставьте его вручную, отредактировав библиотечный код, либо автоматически, написав соответствующий аспект. Обратите внимание, что если библиотека пытается поймать и усвоить RuntimeException, заброшенное исключение можно заменить чем-то другим, которое библиотека не пытается поймать.

К счастью для меня, используя VisualVM, мне удалось найти один метод, называемый очень большим количеством раз при конкретном использовании Я делал из библиотеки. После добавления вышеуказанного сегмента кода он теперь корректно реагирует на прерывания.

Это, конечно, не поддерживается, плюс ничего не гарантирует, что библиотека будет многократно вызывать этот метод в других сценариях; но он работал у меня, и, поскольку он относительно прост профилировать другие приложения и вставлять туда чеки, я считаю это общим, если не уродливым, решением.

Ответ 2

Мне приходят в голову следующие странные идеи:

  • Использование библиотеки модификаций кода байта, например Javassist, для введения проверок прерываний в разных точках байт-кода. Просто в начале методов может быть недостаточно, так как вы упоминаете, что эти внешние методы не являются рекурсивными, поэтому вы можете принудительно остановить их в любой момент. Выполнение этого на уровне байтового кода также сделает его очень отзывчивым, например. даже если внешний код работает в цикле или что-то еще, можно было бы ввести проверки прерываний. Однако это добавит некоторые накладные расходы, поэтому общая производительность будет медленнее.
  • Запуск отдельных процессов (например, отдельных виртуальных машин) для внешнего кода. Прерывание процессов может быть намного проще, чем другие решения. Недостатком является то, что вам понадобится какой-то канал связи между внешним кодом и вашим кодом, например. IPC, сокетами и т.д. Второй недостаток заключается в том, что для запуска новых виртуальных машин требуется гораздо больше ресурсов (процессор, память), и это может быть специфично для среды. Это будет работать, если вы начнете выполнение нескольких задач с использованием внешнего кода, но не сотен задач. Кроме того, производительность пострадает, но само вычисление будет таким же быстрым, как оригинал. Процессы можно принудительно остановить, используя java.lang.Process.destroy()
  • Использование специального SecurityManager, который выполняет проверку прерываний для каждого из методов checkXXX. Если внешний код каким-то образом вызывает привилегированные методы, может быть достаточно, чтобы вы прервали их в этих местах. Примером может быть java.lang.SecurityManager.checkPropertyAccess(String), если внешний код периодически считывает системное свойство.

Ответ 3

Это решение также непросто, но оно может работать: с помощью Javassist или CGLIB вы можете вставлять код в начале каждого внутреннего метода (который предположительно вызывается методом main run()), чтобы проверить, нить жива или какой-либо другой флаг (если это другой флаг, вам также нужно добавить его вместе с методом его установки).

Я предлагаю Javassist/CGLIB вместо расширения класса с помощью кода, потому что вы упоминаете его как внешний, и вы не хотите менять исходный код, и он может измениться в будущем. Поэтому добавление проверок прерываний во время выполнения будет работать для текущей версии, а также в будущих версиях, даже если имена внутренних методов изменяются (или их параметры, возвращаемые значения и т.д.). Вам просто нужно взять класс и добавить проверки прерываний в начале каждого метода, который не является методом run().

Ответ 4

Опция:

Хотя я не думаю, что остановленный поток серьезно помешал бы исполнителям (в конце концов, они должны быть безупречными), есть альтернативное решение, которое не включает остановку потока.

Если ваши задачи не изменят ничего в других частях системы (это справедливое предположение, иначе вы бы не пытались их сбивать), то вы можете использовать JDI для пополнения нежелательного стека кадр выключается и обычно выходит из задачи.

public class StoppableTask implements Runnable {

private boolean stopped;
private Runnable targetTask;
private volatile Thread runner;
private String id;

public StoppableTask(TestTask targetTask) {
    this.targetTask = targetTask;
    this.id = UUID.randomUUID().toString();
}

@Override
public void run() {
    if( !stopped ) {
        runner = Thread.currentThread();
        targetTask.run();
    } else {
        System.out.println( "Task "+id+" stopped.");
    }
}

public Thread getRunner() {
    return runner;
}

public String getId() {
    return id;
}
}

Это runnable, который обертывает все ваши другие runnables. В нем хранится ссылка на исполняемый поток (будет важна позже) и идентификатор, чтобы мы могли найти его с помощью вызова JDI.

public class Main {

public static void main(String[] args) throws IOException, IllegalConnectorArgumentsException, InterruptedException, IncompatibleThreadStateException, InvalidTypeException, ClassNotLoadedException {
    //connect to the virtual machine
    VirtualMachineManager manager = Bootstrap.virtualMachineManager();
    VirtualMachine vm = null;
    for( AttachingConnector con : manager.attachingConnectors() ) {
        if( con instanceof SocketAttachingConnector ) {
            SocketAttachingConnector smac = (SocketAttachingConnector)con;
            Map<String,? extends Connector.Argument> arg = smac.defaultArguments();
            arg.get( "port" ).setValue( "8000");
            arg.get( "hostname" ).setValue( "localhost" );
            vm = smac.attach( arg );
        }
    }

    //start the test task
    ExecutorService service = Executors.newCachedThreadPool();
    StoppableTask task = new StoppableTask( new TestTask() );
    service.execute( task );
    Thread.sleep( 1000 );

    // iterate over all the threads
    for( ThreadReference thread : vm.allThreads() ) {
        //iterate over all the objects referencing the thread
        //could take a long time, limiting the number of referring
        //objects scanned is possible though, as not many objects will
        //reference our runner thread
        for( ObjectReference ob : thread.referringObjects( 0 ) ) {
            //this cast is safe, as no primitive values can reference a thread
            ReferenceType obType = (ReferenceType)ob.type();
            //if thread is referenced by a stoppable task
            if( obType.name().equals( StoppableTask.class.getName() ) ) {

                StringReference taskId = (StringReference)ob.getValue( obType.fieldByName( "id" ));

                if( task.getId().equals( taskId.value() ) ) {
                    //task with matching id found
                    System.out.println( "Task "+task.getId()+" found.");

                    //suspend thread
                    thread.suspend();

                    Iterator<StackFrame> it = thread.frames().iterator();
                    while( it.hasNext() ) {
                        StackFrame frame = it.next();
                        //find stack frame containing StoppableTask.run()
                        if( ob.equals( frame.thisObject() ) ) {
                            //pop all frames up to the frame below run()
                            thread.popFrames( it.next() );
                            //set stopped to true
                            ob.setValue( obType.fieldByName( "stopped") , vm.mirrorOf( true ) );
                            break;
                        }
                    }
                    //resume thread
                    thread.resume();

                }

            }
        }
    }

}
}

И для справки, вызов "библиотеки" я протестировал с помощью:

public class TestTask implements Runnable {

    @Override
    public void run() {
        long l = 0;
        while( true ) {
            l++;
            if( l % 1000000L == 0 )
                System.out.print( ".");
        }

    }
}

Вы можете попробовать, запустив класс Main с параметром командной строки -agentlib:jdwp=transport=dt_socket,server=y,address=localhost:8000,timeout=5000,suspend=n. Он работает с двумя оговорками. Во-первых, если выполняется собственный код (thisObject кадра равно нулю), вам придется подождать, пока он не закончится. Во-вторых, блоки finally не вызываются, поэтому потенциально могут протекать различные ресурсы.

Ответ 5

Вы писали:

Другой вариант, который я пробовал, - отметить myMethod() и аналогичные методы с специальной аннотацией "Прерывание", а затем использовать AspectJ (который Я, по общему признанию, новичок), чтобы поймать все вызовы методов там - что-то вроде:

@Before("call(* *.*(..)) && withincode(@Interruptable * *.*(..))")
public void checkInterrupt(JoinPoint thisJoinPoint) {
    if (Thread.interrupted()) throw new ForcefulInterruption();
}

Но withincode не является рекурсивным для методов, вызванных сопоставлением методов, поэтому мне пришлось бы отредактировать эту аннотацию во внешнюю код.

Идея AspectJ хороша, но вам нужно

  • используйте cflow() или cflowbelow() для рекурсивного соответствия определенному потоку управления (например, что-то вроде @Before("cflow(execution(@Interruptable * *(..)))")).
  • убедитесь, что вы также сплести свою внешнюю библиотеку, а не только собственный код. Это можно сделать, используя бинарное плетение, используя классы файлов JAR и повторно упаковывая их в новый JAR файл или применяя LTW (время загрузки во время запуска приложения) (т.е. во время загрузки класса).

Возможно, вам даже не понадобится аннотация вашего маркера, если ваша внешняя библиотека имеет имя пакета, которое вы можете определить с помощью within(). AspectJ действительно мощный, и часто существует более одного способа решения проблемы. Я бы рекомендовал использовать его, потому что он был создан для таких начинаний, как ваш.

Ответ 6

Если внутренние методы имеют похожие имена, вы можете использовать определение pointcut в xml (spring/AspectJ) вместо аннотаций, поэтому не требуется модификация внешней библиотеки.

Ответ 7

Как mhaller, лучший вариант - запустить новый процесс. Поскольку ваши задания не являются кооперативными, у вас никогда не будет гарантий по завершению Thread.

Хорошим решением вашей проблемы будет использование библиотеки, которая поддерживает произвольную приостановку/остановку "легких потоков", таких как Akka вместо службы-исполнителя, хотя это может быть немного излишним.

Хотя я никогда не использовал Akka и не могу подтвердить, что он работает так, как вы ожидаете, docs заявляют, что stop() для остановки участников.

Ответ 8

Насколько я знаю, существует два способа использования аспекта

  • AspecJ
  • Spring АОП

AspectJ - это настраиваемый компилятор, который перехватывает методы, которые компилируются (что означает, что он не может получить "внешние методы". Spring AOP (по умолчанию) использует метод проксирования класса для методов перехвата во время выполнения (поэтому он может перехватывать "внешние методы", но проблема с Spring AOP заключается в том, что он не может прокси-прокси-прокси-классы (которые AspectJ может делать потому что это не прокси-классы). Я думаю, AspectJ может помочь вам в этом случае.