How do you properly synchronize threads on the native side of a JNI environment?
Asked Answered
C

2

14

Question Brief

I am using C++ and Java in one process via JNI. For the use case in question, both a C++ thread and a Java thread are accessing the same data, they are doing so on the C++ side, and I want to properly synchronize the access.

So far, almost all of my JNI thread synchronization has been on the Java side where the answer is obvious: use the provided Java concurrency package and the built-in concurrency language features. Unfortunately, the answer is not so obvious on the C++ side.

What I've Tried so far Brief

I tried using a pthreads mutex thinking that it might work even though I'm not using pthreads to create threads, but that occasionally gets stuck when trying to lock - I'll show an example of that farther below.

Question Details

In my current, specific usage, c++ is polling for changes provided by Java on a 1 second timer (not what I'd like, but I'm not sure how I would make it event-driven given the nature of the legacy c++ code). The Java thread provides data by calling a native function and c++ copies the data into a c++ structure.

This is the type of situation in code (happens on 2 threads, Thread1 and Thread2):

Code Example

Note quite an SSCCE, as it's missing definitions for TheData and TheDataWrapper, but it doesn't really matter what they contain. Assume they merely contain a couple of public ints if that helps your thought process (though, in my case, it's actually multiple arrays of int and arrays of 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;
}

Java:

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);
}

What I've Tried so far Details

When I tried pthread's locking as below, sometimes execution gets stuck in pthread_mutex_lock. There should not be a deadlock in this situation, but just to test further I ran a scenario where supplyData was not getting called at all (no data was being supplied), so no deadlock should have been possible, yet the first call to poll will occasionally hang anyway. Perhaps using a pthreads mutex is not a good idea in this situation? Or perhaps I did something stupid and keep overlooking it.

So far, I tried using pthreads as below:

Code Example

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);
}

Another option I thought about but have not done

I also considered using JNI to call back into java to request a lock using java's concurrency control. That should work, as either thread should block on the java side as needed. However, since accessing java from c++ is overly verbose, I was hoping to avoid going through that headache. I probably could make a c++ class which encapsulates JNI calls into java to request a java lock; that would simplify c++ code, though I wonder about the overhead of crossing back and forth over JNI just for thread locks.

It seems this is not necessary, per the comment by @Radiodef. It appears JNI includes MonitorEnter/MonitorExit functions which already handle the locking on the c++ side. There are pitfalls when using these at the same time as conventional locks on the java side, so please read here before using. I will be trying this out, and I expect that MonitorEnter/MonitorExit will be the answer and I recommend @Radiodef make an answer out of the comment.

Closing

How could I properly synchronize this? Should pthread_mutex_(un)lock work? If not, what can I use to synchronize between the C++ thread and the Java thread?

No JNI-specific C++ code is provided here since the JNI bridge is working and I can pass data back and forth. The question is specifically about proper synchronization between c++/java threads that are otherwise correctly communicating.

As mentioned before, I would prefer to avoid the polling scheme, but that might end up being another question. The legacy c++ code displays its part of the user interface in X/motif, and if I recall correctly the c++ thread above happens to be the event thread for display. The java thread will end up being the java event dispatch thread once the java user interface for this class is plugged in, though for now the java thread is an automated test thread; either way, it's a separate java thread.

The C++ thread is attached to the JVM. In fact, that is the C++ thread that created the JVM, so it should be attached by default.

I have been successful with plugging in other Java user interface elements into this program, but this is the first time C++ has needed non-atomic data from Java which needed to be synchronized. Is there a generally accepted correct way to do this?

Cotangent answered 7/6, 2017 at 19:9 Comment(6)
I don't have an answer for you, unfortunately. Just wanted to say; nice detailed thorough question. Thanks.Phillie
JNI has functions which perform synchronization from native code. However, something I don't know is whether the Java memory model guarantees consistency in memory which is allocated by native code and therefore outside Java heap space. I therefore don't know whether MonitorEnter and MonitorExit necessarily solve the problem here.Widower
@Widower I think that is exactly what I need. I did not realize those existed. I was worried I would have to use JNI to call back into Java just to make a lock; that would have been an overly verbose headache for something so simple and common. That is probably why the functions you mention were added.Cotangent
@Widower Also, as for your concern about the memory, I will keep that in mind. I don't think that should apply in my current case at hand, but it will likely pop up in future cases. Thank you.Cotangent
Do you have long-running C++ threads that need to operate on the data in parallel with Java threads? If not - and all your C++ access is via direct JNI calls (make the native call and return) the simplest solution is to just share a lock object on the Java side. Synchronize on that object before modifying the data in Java or making a native call that can modify the data. That way you never get into native code without knowing it's already safe to modify the object.Rigi
@AndrewHenle I have long running threads on both c++ and java sides, and both sides make calls to the other. This is a legacy c++ app with its own display (multiple windows on multiple monitors) for which I am adding new features and new windows, most of the new windows in java. Many of the java and c++ objects need to talk to each other with data supplies and requests going in both directions: c++-to-java and java-to-c++. C++ is the "host" language, creating a JVM instance from within c++ and calling some java methods which kick off the java windows and threads.Cotangent
C
4

If both threads are attached to the JVM, then you can access the JNI's synchronization via JNIEnv's MonitorEnter(jobject) and MonitorExit(jobject) functions. Just as it sounds, MonitorEnter aquires a lock on the provided jobject, and MonitorExit releases the lock on the provided jobject.

NOTE: There are some pitfalls to be aware of! Notice the second to last paragraph of MonitorEnter's description and the last paragraph of MonitorExit's description about mixing and matching MonitorEnter/MonitorExit with other similar mechanisms which you might otherwise think are compatible.

See here

MonitorEnter

jint MonitorEnter(JNIEnv *env, jobject obj);

Enters the monitor associated with the underlying Java object referred to by obj. Enters the monitor associated with the object referred to by obj. The obj reference must not be NULL. Each Java object has a monitor associated with it. If the current thread already owns the monitor associated with obj, it increments a counter in the monitor indicating the number of times this thread has entered the monitor. If the monitor associated with obj is not owned by any thread, the current thread becomes the owner of the monitor, setting the entry count of this monitor to 1. If another thread already owns the monitor associated with obj, the current thread waits until the monitor is released, then tries again to gain ownership.

A monitor entered through a MonitorEnter JNI function call cannot be exited using the monitorexit Java virtual machine instruction or a synchronized method return. A MonitorEnter JNI function call and a monitorenter Java virtual machine instruction may race to enter the monitor associated with the same object.

To avoid deadlocks, a monitor entered through a MonitorEnter JNI function call must be exited using the MonitorExit JNI call, unless the DetachCurrentThread call is used to implicitly release JNI monitors.

LINKAGE:

Index 217 in the JNIEnv interface function table.

PARAMETERS:

env: the JNI interface pointer.

obj: a normal Java object or class object.

RETURNS:

Returns “0” on success; returns a negative value on failure.

and

MonitorExit

jint MonitorExit(JNIEnv *env, jobject obj);

The current thread must be the owner of the monitor associated with the underlying Java object referred to by obj. The thread decrements the counter indicating the number of times it has entered this monitor. If the value of the counter becomes zero, the current thread releases the monitor.

Native code must not use MonitorExit to exit a monitor entered through a synchronized method or a monitorenter Java virtual machine instruction.

LINKAGE:

Index 218 in the JNIEnv interface function table.

PARAMETERS:

env: the JNI interface pointer.

obj: a normal Java object or class object.

RETURNS:

Returns “0” on success; returns a negative value on failure.

EXCEPTIONS:

IllegalMonitorStateException: if the current thread does not own the monitor.

So the C++ code in the question which attempted to use pthreads should be changed as following (code assumes the JNIEnv* pointer was acquired somehow beforehand in typical JNI fashion):

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

// called on c++ thread
void objectA :: poll()
{
    // You will need to aquire jniEnv pointer somehow just as usual for JNI
    jniEnv->MonitorEnter(dataMutex);

    ... // all the poll stuff from before

    jniEnv->MonitorExit(dataMutex);
}

// called on java thread
void objectA :: supplyData(JNIEnv* jni, jobject jthis, jobject data)
{
    // You will need to aquire jniEnv pointer somehow just as usual for JNI
    jniEnv->MonitorEnter(dataMutex);

    ... // all the supplyData stuff from before

    jniEnv->MonitorExit(dataMutex);
}

Kudos to @Radiodef who provided the answer. Unfortunately it was as a comment. I waited until afternoon next day to allow time for Radiodef to make it an answer, so now I'm doing it. Thank you Radiodef for providing the nudge I needed to fix this.

Cotangent answered 8/6, 2017 at 17:45 Comment(0)
T
0

If you are synchronizing between a native thread and a java thread, it may be prudent to use both a native mutex and a Java Monitor. Also if you have it available, I recommend using std::mutex to establish synchronization within native threads. std::lock_guard is also useful, and creating some wrapper for the java monitor which has a .lock() and .unlock() method so you can use them with std::lock_guard would also be helpful (then you can let the C++ Compiler do its job). The main reason I say you should use both is because MonitorEnter isn't perfect, its prone to race-conditions. Specifically, as far as I can tell from the jni documentation, it doesn't establish synchronization (JNI Documentation). The use of the native std::mutex.lock() will synchronize-with the native unlock.

#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};
...
}




Truda answered 2/2, 2019 at 1:51 Comment(2)
You wrote "MonitorEnter isn't perfect, its prone to race-conditions. Specifically, as far as I can tell from the jni documentation, it doesn't establish synchronization" Where do you see that in the documentation? I cannot find that in the MonitorEnter/MonitorExit documentation linked. In fact, if I understand your "doesn't provide synchronization" statement correctly, that would surprise me a lot and I think that would drastically reduce the usefulness of those functions.Cotangent
@Cotangent I said, as far as I can tell, it doesn't establish synchronization, more specifically between Different Native Threads. There isn't any documentation which supports that it does and C++ vs. java have very different idea's of what synchronization means. What I mean is, a JNI Lock may not be sufficient to guard against native data-races from my reading into the documentation. If it is, then that would be a pleasent suprise and I will edit my post accordingly if it can be proven (or I determine that from my own testing),Truda

© 2022 - 2024 — McMap. All rights reserved.