Вызов трудоемкой задачи JNI в виде потока

У меня сложная проблема с вызовом нативной функции с использованием JNI из потока.

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

В основном это должно быть довольно простым, примерно так:

public class CalculationEngine {
  private CalculationEngine(){}

  public static void calculateInBackground(final Parameters parameters) {

    new Thread(new Runnable() {
      public void run() {
        // Someone might change the parameters while our thread is running, so:
        final Parameters clonedParameters = parameters.clone();
        Results results = new Results();
        natCalc(clonedParameters, results);
        EventBus.publish("Results", results);
      }
    }).start();

  }

  public static void calculateNormally(final Parameters parameters) {
    Results results = new Results();
    natCalc(parameters, results);
    EventBus.publish("Results", results);
  }

  private static native synchronized void
    natCalc(Parameters parameters, Results results);      
}

Теперь метод calculateNormally, который блокирует основную программу, работает нормально, но метод calculateInBackground, который просто создает фоновый поток для выполнения того же действия, вызывает различные сбои в собственном коде, когда он вызывается последовательно . Последовательно я имею в виду, что он вызывается снова только после того, как предыдущий поток завершился и вернул результат. Обратите внимание, что собственный код помечен synchronized, чтобы гарантировать, что только один его экземпляр может работать одновременно.

Мой вопрос: как собственный код может вести себя по-разному в зависимости от того, вызывается ли он из основного потока или из какого-то другого потока? Это похоже на то, что нативный код сохраняет «состояние» и на самом деле не выходит, когда он вызывается из потока, отличного от основного потока. Есть ли способ «очистить» или «сбросить» поток после его завершения? В JNI & Threads должно быть что-то, чего я просто не знаю.

Спасибо за любые подсказки!


person Joonas Pulakka    schedule 21.07.2009    source источник
comment
Предлагаем вам добавить это в качестве ответа - это фактическое решение и весьма полезно.   -  person Robert Munteanu    schedule 21.07.2009
comment
Хорошая идея. Теперь это ответ.   -  person Joonas Pulakka    schedule 21.07.2009


Ответы (4)


Я нашел рабочее решение, погуглив и найдя фразу "Я нашел JNI чтобы быть очень глючным при вызове из отдельных потоков... Поэтому убедитесь, что только один поток когда-либо вызывает ваш собственный код!". Это кажется правдой; решение состоит в том, чтобы сохранить постоянный, «повторно используемый» поток — я использовал Executors.newSingleThreadExecutor() — и вызывать собственный код только из этого потока. Оно работает.

Таким образом, с точки зрения JNI разница была не между основным потоком и каким-то другим потоком, а в использовании разных потоков в последовательных вызовах. Обратите внимание, что в проблемном коде каждый раз создавался новый поток. Это должно работать именно так, но это не так. (И нет, я не кэширую указатель JNIEnv.)

Было бы интересно узнать, является ли это ошибкой JNI, ошибкой в ​​​​родном коде, чем-то во взаимодействии между ними и ОС или чем-то еще. Но иногда у вас просто нет возможности детально отладить 10000+ строк существующего кода, тем не менее, вы счастливы заставить его работать. Вот рабочая версия примера кода, назовем это обходным решением:

public class CalculationEngine {
  private CalculationEngine(){}

  private static Parameters parameters;
  private static ExecutorService executor = Executors.newSingleThreadExecutor();

  private static Runnable analysis = new Runnable() {
      public synchronized void run() {
        Results results = new Results();
        natCalc(parameters, results);
        EventBus.publish("Results", results);
      }
  };  

  public static synchronized void
    calculateInBackground(final Parameters parameters) {
      CalculationEngine.parameters = parameters.clone();
      executor.submit(analysis);
  }

  private static native synchronized void
    natCalc(Parameters parameters, Results results);      
}
person Joonas Pulakka    schedule 21.07.2009
comment
Если вы не можете найти конкретные ошибки, которые применимы к вашей ситуации, или можете определить на низком уровне, что происходит, вы не должны обвинять JNI - независимо от того, что утверждает какой-то случайный человек в Интернете. См. bugs.sun.com/bugdatabase. - person kdgregory; 21.07.2009
comment
Между прочим, весьма вероятно, что ваш код JNI удерживает данные между вызовами. Или, что более вероятно, предоставление данных таким образом, что несколько параллельных потоков могут измениться (тот факт, что это работает при работе в одном потоке, указывает на это направление). - person kdgregory; 21.07.2009
comment
Я попытаюсь сообщить об ошибке, хотя это сложно, потому что нативный код, над которым я работаю, является проприетарным, поэтому я определенно не могу отправить его в Sun, и как воспроизвести что-то без кода...? В любом случае, теперь я один из случайных людей в Интернете, которые утверждают, что вызовы JNI могут работать лучше, если они выполняются из одного потока :-) - person Joonas Pulakka; 21.07.2009
comment
@kdgregory: Даже в первом примере я ограничил количество одновременных потоков до одного (ключевое слово synchronized в нативном объявлении — лишь одна из мер безопасности), потому что я знаю, что мой код JNI определенно не работает. не является потокобезопасным (есть некоторые статические/глобальные переменные). Но кажется, что вызывающий поток должен каждый раз быть одним и тем же отдельным потоком. - person Joonas Pulakka; 21.07.2009
comment
Если вы собираетесь отправить отчет об ошибке, вы должны четко указать, что не удается, с помощью примера кода; иначе он просто закроется. И прежде чем вы это сделаете (а не посмотрите внимательно на свой собственный код), вы можете взглянуть на исходный код для java.lang.System. Посмотреть все нативные методы? Если бы JNI действительно не работал в многопоточной среде, как вы думаете, сколько времени потребуется, чтобы кто-нибудь заметил, что эти методы не работают? - person kdgregory; 21.07.2009
comment
Ну, трудно сказать, кого винить. Но увидеть код? Я не обвиняю JNI в проблемах с многопоточностью — я даже не пробовал многопоточность, так как знаю, что мой собственный (устаревший) код не является потокобезопасным. Дело в том, что моя проблема была решена путем вызова функции всегда из одного и того же потока, а не последовательного вызова из разных потоков. - person Joonas Pulakka; 21.07.2009
comment
Так что есть что-то, что зависит от потока. Это что-то либо в вашем коде, либо в JVM. Если вы хотите обвинить JVM, а не свой код, это ваш выбор. Не совсем рационально и, вполне вероятно, приведет к ошибкам в будущем. - person kdgregory; 21.07.2009
comment
Говорить, что это ваш код, так же легко, как обвинять JVM. В любом случае, если вы заставите работать более 10000 строк устаревшего кода, слегка изменив среду - даже если вы не совсем понимаете, что происходит - это рациональный выбор, ИМХО. Другой вариант — отлаживать его месяцами и, может быть, что-то придумать. Рационально ли это, это ваш выбор :-) - person Joonas Pulakka; 22.07.2009
comment
...или, возможно, мне следует использовать слово прагматичный. Во всяком случае, изменил мой ответ сейчас. - person Joonas Pulakka; 22.07.2009

Мой совет по использованию JNI: НЕ ДЕЛАЙТЕ, если вы можете этого избежать. Скорее всего, это вызовет проблемы со стабильностью для вас. Вот несколько возможных альтернатив:

  1. Перекодируйте нативную библиотеку на Java.
  2. Напишите команду-обертку для нативной библиотеки на C/C++/что угодно и запустите ее с помощью java.lang.Process и друзей
  3. Превратите нативную библиотеку в демон и получите к ней доступ с помощью сокетов.
person Stephen C    schedule 22.07.2009
comment
Спасибо за ваши мысли. Альтернатива 1. непрактична, когда у вас есть МНОГО существующего нетривиального собственного кода для взаимодействия. Альтернативы 2 и 3 вполне жизнеспособны, однако тогда вам нужно создать какой-то протокол для отправки ваших материалов туда и обратно между нативной стороной и стороной Java. JNI, в принципе, наиболее прямолинеен, если у вас всего несколько вызовов функций. Но на практике это, кажется, имеет свои особенности; он может выявить недостатки в существующем собственном коде, независимо от того, насколько тщательно вы создаете слой JNI... - person Joonas Pulakka; 22.07.2009
comment
Вы правы в необходимости определить (частный) протокол. И еще одна проблема с альтернативами 2 и 3 заключается в том, что накладные расходы на взаимодействие больше; то есть fork/exec или RPC вместо вызова процедуры. Но, несмотря на это, я бы все равно выбрал решение, отличное от JNI, если только не было веских причин не делать этого. - person Stephen C; 22.07.2009
comment
JNI не для слабонервных. Подход Visual Studio к взаимодействию нативного кода и среды CLR намного проще. Однако недостаток простоты компенсируется гибкостью. Если бы модель JNI/JVM была ошибочной, сейчас она бы исчезла, но на самом деле это более простая модель, чем CLR. Ваша рекомендация избегать JNI хороша для тех, кто плохо понимает, как работают потоки и что делают базовые структуры данных в JVM. Но для тех, у кого более высокий уровень знаний в области компьютерных наук, я бы рекомендовал изучить эту тему, прежде чем отказываться от JNI. - person ; 16.10.2012

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

Задействован COM-объект с многопоточной связью. Многопоточные COM-объекты апартамента, которые являются единственным типом, который может создавать VB, могут использоваться только в потоке, который их создает.

Функции безопасности, такие как олицетворение, часто изолированы от потоков. Если код инициализации модифицировал контекст потока, будущие вызовы, которые предполагают наличие контекста, завершатся ошибкой.

Хранилище памяти для конкретных потоков — это метод поддержки многопоточности в некоторых приложениях (Java также имеет такую ​​функцию).

person Jim Rush    schedule 30.09.2010
comment
Спасибо. Это действительно Windows .dll, и объяснение, связанное с COM-объектом, имеет большой смысл. Я этого не знал. - person Joonas Pulakka; 30.09.2010

Здесь есть хорошая документация по этому поводу: Раздел 8.1 JNI и потоки. http://java.sun.com/docs/books/jni/download/jni.pdf

person David Portabella    schedule 04.09.2011