如何在JNI环境的本机端正确同步线程?

时间:2017-06-07 19:09:07

标签: java c++ multithreading synchronization java-native-interface

问题简报

我通过JNI在一个进程中使用C ++和Java。对于有问题的用例,C ++线程和Java线程都访问相同的数据,他们在C ++端这样做,我想正确地同步访问。

到目前为止,几乎所有的JNI线程同步都在Java端,答案显而易见:使用提供的Java并发包和内置的并发语言功能。不幸的是,答案在C ++方面并不那么明显。

到目前为止我所尝试的内容简短

我尝试使用pthreads互斥锁认为即使我没有使用pthreads创建线程也可以工作,但是在尝试锁定时偶尔会卡住 - 我将在下面展示一个更远的例子。

问题详情

在我目前的具体用法中,c ++正在轮询Java在1秒计时器上提供的更改(不是我想要的,但是我不知道如果考虑到它的性质,我将如何使它成为事件驱动的。遗留c ++代码)。 Java线程通过调用本机函数来提供数据,c ++将数据复制到c ++结构中。

这是代码中的情况类型(发生在2个线程,Thread1和Thread2上):

代码示例

请注意SSCCE,因为它缺少TheDataTheDataWrapper的定义,但它们包含的内容并不重要。假设它们只包含几个公共int,如果这有助于你的思考过程(虽然,在我的例子中,它实际上是多个int数组和float数组)。

C ++:

class objectA
{
    void poll();
    void supplyData(JNIEnv* jni, jobject jthis, jobject data);
    TheDataWrapper cpp_data;
    bool isUpdated;

    void doStuff(TheDataWrapper* data);
};

// poll() happens on a c++ thread we will call Thread1
void objectA :: poll()
{
    // Here, both isUpdated and cpp_data need synchronization

    if(isUpdated)
    {
        do_stuff(&cpp_data);
        isUpdated = false;
    }
}

// supplyData happens on the Thread2, called as a native function from a java thread
void objectA :: supplyData(JNIEnv* jni, jobject jthis, jobject data)
{
    // some operation happens that copies the java data into a c++ equivalent
    // in my specific case this happens to be copying ints/floats from java arrays to c++ arrays
    // this needs to be synchronized
    cpp_data.copyFrom(data);
    isUpdated = true;
}

爪哇:

class ObjectB
{
    // f() happens on a Java thread which we will call Thread2
    public void f()
    {
        // for the general case it doesn't really matter what the data is
        TheData data = TheData.prepareData();
        supplyData(data);
    }

    public native void supplyData(TheData data);
}

到目前为止我所尝试的细节

当我尝试按下面的pthread锁定时,有时执行会卡在pthread_mutex_lock中。在这种情况下不应该存在死锁,但为了进一步测试,我运行了一个根本没有调用supplyData的情况(没有提供数据),所以不应该有死锁,但第一个无论如何,拨打poll的电话偶尔会挂起。在这种情况下,也许使用pthreads互斥锁不是一个好主意?或许我做了一些愚蠢的事情并继续忽视它。

到目前为止,我尝试使用pthreads如下:

代码示例

C ++:

class objectA
{
    pthread_mutex_t dataMutex;
    ... // everything else mentioned before
}

// called on c++ thread
void objectA :: poll()
{
    pthread_mutex_lock(&dataMutex);

    ... // all the poll stuff from before

    pthread_mutex_unlock(&dataMutex);
}

// called on java thread
void objectA :: supplyData(JNIEnv* jni, jobject jthis, jobject data)
{
    pthread_mutex_lock(&dataMutex);

    ... // all the supplyData stuff from before

    pthread_mutex_unlock(&dataMutex);
}

我想到的另一种选择但尚未完成

我还考虑过使用JNI回调java来使用java的并发控制来请求锁定。这应该工作,因为任何一个线程都应该根据需要在java端阻塞。但是,由于从c ++访问java过于冗长,我希望避免经历这种麻烦。我可能会创建一个c ++类,它将JNI调用封装到java中以请求java锁;这会简化c ++代码,虽然我想知道只是为了线程锁而在JNI上来回交叉的开销。

根据@Radiodef的评论,这似乎没有必要。似乎JNI包含已经处理c ++端锁定的MonitorEnter / MonitorExit函数。与java端的传统锁同时使用这些时存在缺陷,因此在使用之前please read here。我将尝试这一点,我希望MonitorEnter / MonitorExit将成为答案,我建议@Radiodef在评论中做出回答。

我怎样才能正确同步? pthread_mutex_(un)锁定应该工作吗?如果没有,我可以使用什么来在C ++线程和Java线程之间进行同步?

这里没有提供特定于JNI的C ++代码,因为JNI桥正在工作,我可以来回传递数据。问题是关于正确通信的c ++ / java线程之间的正确同步。

如前所述,我宁愿避免使用投票方案,但这可能最终成为另一个问题。遗留的c ++代码在X / motif中显示其用户界面的一部分,如果我没记错,上面的c ++线程碰巧是用于显示的事件线程。一旦插入了这个类的java用户界面,java线程将最终成为java事件调度线程,尽管现在java线程是一个自动测试线程;无论哪种方式,它都是一个单独的java线程。

C ++线程附加到JVM。实际上,这是创建JVM的C ++线程,因此默认情况下应该附加它。

我成功地将其他Java用户界面元素插入到该程序中,但这是C ++第一次需要来自Java的非原子数据需要同步。是否有普遍接受的正确方法来做到这一点?

2 个答案:

答案 0 :(得分:2)

如果两个线程都连接到JVM,那么您可以通过JNIEnv的{​​{1}}和MonitorEnter(jobject)函数访问JNI的同步。就像听起来一样,MonitorExit(jobject)要求锁定所提供的MonitorEnter,并jobject释放对所提供的MonitorExit的锁定。

注意:有一些陷阱需要注意!请注意jobject的说明的倒数第二段以及MonitorEnter关于将MonitorExit / MonitorEnter与其他类似机制进行混合和匹配的说明的最后一段否则认为是兼容的。

请参阅here

  

<强> MonitorEnter

     

jint MonitorEnter(JNIEnv * env,jobject obj);

     

输入与引用的基础Java对象关联的监视器   通过obj。进入与所引用对象关联的监视器   通过obj。 obj引用不能为NULL。每个Java对象都有一个   与之相关的监视器。如果当前线程已经拥有   与obj关联的监视器,它会增加监视器中的计数器   表示此线程进入监视器的次数。如果   与obj关联的监视器不属于任何线程,   当前线程成为监视器的所有者,设置条目   此监视器的计数为1.如果另一个线程已拥有监视器   与obj相关联,当前线程等待直到监视器   释放,然后再次尝试获得所有权。

     

通过MonitorEnter JNI函数调用输入的监视器不能   退出使用monitorexit Java虚拟机指令或   同步方法返回。一个MonitorEnter JNI函数调用和一个   monitorenter Java虚拟机指令可能会竞争进入   与同一对象关联的监视器。

     

为避免死锁,通过MonitorEnter JNI输入监视器   必须使用MonitorExit JNI调用退出函数调用,除非   DetachCurrentThread调用用于隐式释放JNI   显示器

     

<强> LINKAGE

     

JNIEnv接口函数表中的索引217。

     

<强>参数

     

env:JNI接口指针。

     

obj:普通的Java对象或类对象。

     

<强>返回

     

成功时返回“0”;失败时返回负值。

  

<强> MonitorExit

     

jint MonitorExit(JNIEnv * env,jobject obj);

     

当前线程必须是与之关联的监视器的所有者   obj引用的底层Java对象。线程递减   计数器指示它进入此次的次数   监控。如果计数器的值变为零,则为当前线程   释放显示器。

     

本机代码不得使用MonitorExit退出通过输入的监视器   同步方法或monitorenter Java虚拟机   指令。

     

<强> LINKAGE

     

JNIEnv接口函数表中的索引218。

     

<强>参数

     

env:JNI接口指针。

     

obj:普通的Java对象或类对象。

     

<强>返回

     

成功时返回“0”;失败时返回负值。

     

<强>例外

     

IllegalMonitorStateException:如果当前线程不拥有   监视。

因此,尝试使用pthreads的问题中的C ++代码应该如下更改(代码假设事先以典型的JNI方式以某种方式获取MonitorExit指针):

JNIEnv*

向@Radiodef致敬,他提供了答案。不幸的是,这是一个评论。我等到第二天下午让Radiodef有时间给它答案,所以现在我正在做。感谢Radiodef提供我需要解决的问题。

答案 1 :(得分:0)

如果要在本机线程和Java线程之间进行同步,则最好同时使用本机互斥锁和Java监视器。 另外,如果有可用的内存,我建议使用std :: mutex在本机线程中建立同步。 std :: lock_guard也很有用,并为具有.lock()和.unlock()方法的java监视器创建一些包装,以便您可以将它们与std :: lock_guard一起使用(然后可以让C ++编译器执行其工作)。 我说您应该同时使用两者的主要原因是因为MonitorEnter并不完美,它很容易出现竞争状况。具体来说,据jni文档所知,它不会建立同步(JNI Documentation)。 使用本地std :: mutex.lock()将与本地解锁同步。

#include <mutex>
jobject magicObtainLockObject();
JNIEnv* magicObtainJNIEnv();

struct compound_lock{
private:
std::mutex mtx;
public:
void lock(){
mtx.lock();
magicObtainJNIEnv()->MonitorEnter(magicObtainLockObject());
}
void unlock(){
magicObtainJNIEnv()->MonitorExit(magicObtainLockObject());
mtx.unlock();
}
};

struct objectA{
...
compound_lock lock;
};

void objectA::poll(){
std::lock_guard<compound_lock> sync{lock};
...
}

void objectA::supplyData(JNIEnv* jni, jobject jthis, jobject data){
std::lock_guard<compound_lock> sync{lock};
...
}