Skip to content

Commit b1404be

Browse files
MrDOSdilyanpalauzov
authored andcommitted
Use a real read/write lock on the monitor thread.
THIS AND the previous commits are from github.com/NeuronRobotics/pull/211/ The old `MonitorThreadLock` boolean field was only checked at a very slow interval (5s!), and, itself not being synchronized, was prone to race conditions. More importantly, it wasn't set during the monitor thread's self-cleanup after hardware failure, so under typical access patterns, the monitor thread and the application thread would both try to clean up the monitor thread simultaneously. This race condition could occasionally lead to a segfault (only reproduced on macOS, but I've no doubt it could happen elsewhere). I've also attempted to clean up some redundant flag fields, and consolidate setting the remaining fields to further avoid concurrency/reentrancy issues.
1 parent e705e51 commit b1404be

File tree

2 files changed

+280
-145
lines changed

2 files changed

+280
-145
lines changed

src/main/c/src/SerialImp.c

Lines changed: 164 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3781,23 +3781,142 @@ JNIEXPORT void JNICALL RXTXPort(setflowcontrol)( JNIEnv *env,
37813781
return;
37823782
}
37833783

3784-
/*----------------------------------------------------------
3785-
unlock_monitor_thread
3784+
/**
3785+
* Write-lock the monitor thread to protect state mutation.
3786+
*
3787+
* Blocks until the write lock comes available.
3788+
*
3789+
* @param [in] env the JNI environment
3790+
* @param [in] obj an RXTXPort instance
3791+
* @return 0 on success; 1 on error
3792+
*/
3793+
int lock_monitor_thread(JNIEnv *env, jobject jobj)
3794+
{
3795+
jfieldID monitorThreadStateWriteLockField = (*env)->GetFieldID(
3796+
env,
3797+
(*env)->GetObjectClass(env, jobj),
3798+
"monitorThreadStateWriteLock",
3799+
"Ljava/util/concurrent/locks/Lock;");
3800+
if ((*env)->ExceptionCheck(env)) {
3801+
return 1;
3802+
}
37863803

3787-
accept: event_info_struct
3788-
perform: unlock the monitor thread so event notification can start.
3789-
return: none
3790-
exceptions: none
3791-
comments: Events can be missed otherwise.
3792-
----------------------------------------------------------*/
3804+
jobject monitorThreadStateWriteLock = (*env)->GetObjectField(
3805+
env,
3806+
jobj,
3807+
monitorThreadStateWriteLockField);
3808+
if ((*env)->ExceptionCheck(env)) {
3809+
return 1;
3810+
}
37933811

3794-
void unlock_monitor_thread( struct event_info_struct *eis )
3812+
jmethodID lock = (*env)->GetMethodID(
3813+
env,
3814+
(*env)->GetObjectClass(env, monitorThreadStateWriteLock),
3815+
"lock",
3816+
"()V");
3817+
if ((*env)->ExceptionCheck(env)) {
3818+
return 1;
3819+
}
3820+
3821+
(*env)->CallVoidMethod(env, monitorThreadStateWriteLock, lock);
3822+
if ((*env)->ExceptionCheck(env)) {
3823+
return 1;
3824+
}
3825+
3826+
return 0;
3827+
}
3828+
3829+
/**
3830+
* Signal that the monitor thread is ready for work.
3831+
*
3832+
* In order to signal the condition, the current thread must already hold the
3833+
* write lock.
3834+
*
3835+
* @param [in] env the JNI environment
3836+
* @param [in] obj an RXTXPort instance
3837+
* @return 0 on success; 1 on error
3838+
*/
3839+
int signal_monitor_thread_ready(JNIEnv *env, jobject jobj)
37953840
{
3796-
JNIEnv *env = eis->env;
3797-
jobject jobj = *(eis->jobj);
3841+
jfieldID monitorThreadReadyField = (*env)->GetFieldID(
3842+
env,
3843+
(*env)->GetObjectClass(env, jobj),
3844+
"monitorThreadReady",
3845+
"Ljava/util/concurrent/locks/Condition;");
3846+
if ((*env)->ExceptionCheck(env)) {
3847+
return 1;
3848+
}
37983849

3799-
jfieldID jfid = (*env)->GetFieldID( env, (*env)->GetObjectClass( env, jobj ), "MonitorThreadLock", "Z" );
3800-
(*env)->SetBooleanField( env, jobj, jfid, (jboolean) 0 );
3850+
jobject monitorThreadReady = (*env)->GetObjectField(
3851+
env,
3852+
jobj,
3853+
monitorThreadReadyField);
3854+
if ((*env)->ExceptionCheck(env)) {
3855+
return 1;
3856+
}
3857+
3858+
jmethodID signal = (*env)->GetMethodID(
3859+
env,
3860+
(*env)->GetObjectClass(env, monitorThreadReady),
3861+
"signal",
3862+
"()V");
3863+
if ((*env)->ExceptionCheck(env)) {
3864+
return 1;
3865+
}
3866+
3867+
(*env)->CallVoidMethod(env, monitorThreadReady, signal);
3868+
if ((*env)->ExceptionCheck(env)) {
3869+
return 1;
3870+
}
3871+
3872+
return 0;
3873+
}
3874+
3875+
/**
3876+
* Unlock the write lock on the monitor thread to permit read and write access
3877+
* by other threads.
3878+
*
3879+
* In order to unlock the monitor thread, the current thread must already hold
3880+
* the write lock.
3881+
*
3882+
* @param [in] env the JNI environment
3883+
* @param [in] obj an RXTXPort instance
3884+
* @return 0 on success; 1 on error
3885+
*/
3886+
int unlock_monitor_thread(JNIEnv *env, jobject jobj)
3887+
{
3888+
jfieldID monitorThreadStateWriteLockField = (*env)->GetFieldID(
3889+
env,
3890+
(*env)->GetObjectClass(env, jobj),
3891+
"monitorThreadStateWriteLock",
3892+
"Ljava/util/concurrent/locks/Lock;");
3893+
if ((*env)->ExceptionCheck(env)) {
3894+
return 1;
3895+
}
3896+
3897+
jobject monitorThreadStateWriteLock = (*env)->GetObjectField(
3898+
env,
3899+
jobj,
3900+
monitorThreadStateWriteLockField);
3901+
if ((*env)->ExceptionCheck(env)) {
3902+
return 1;
3903+
}
3904+
3905+
jmethodID unlock = (*env)->GetMethodID(
3906+
env,
3907+
(*env)->GetObjectClass(env, monitorThreadStateWriteLock),
3908+
"unlock",
3909+
"()V");
3910+
if ((*env)->ExceptionCheck(env)) {
3911+
return 1;
3912+
}
3913+
3914+
(*env)->CallVoidMethod(env, monitorThreadStateWriteLock, unlock);
3915+
if ((*env)->ExceptionCheck(env)) {
3916+
return 1;
3917+
}
3918+
3919+
return 0;
38013920
}
38023921

38033922
/*----------------------------------------------------------
@@ -4243,6 +4362,8 @@ RXTXPort.eventLoop
42434362
----------------------------------------------------------*/
42444363
JNIEXPORT void JNICALL RXTXPort(eventLoop)( JNIEnv *env, jobject jobj )
42454364
{
4365+
if (lock_monitor_thread(env, jobj)) goto end;
4366+
42464367
#ifdef WIN32
42474368
int i = 0;
42484369
#endif /* WIN32 */
@@ -4255,7 +4376,10 @@ JNIEXPORT void JNICALL RXTXPort(eventLoop)( JNIEnv *env, jobject jobj )
42554376
ENTER( "eventLoop\n" );
42564377
if ( !initialise_event_info_struct( &eis ) ) goto end;
42574378
if ( !init_threads( &eis ) ) goto end;
4258-
unlock_monitor_thread( &eis );
4379+
4380+
if (signal_monitor_thread_ready(env, jobj)) goto end;
4381+
if (unlock_monitor_thread(env, jobj)) goto end;
4382+
42594383
do{
42604384
report_time_eventLoop( );
42614385
do {
@@ -4271,13 +4395,18 @@ JNIEXPORT void JNICALL RXTXPort(eventLoop)( JNIEnv *env, jobject jobj )
42714395
* drain loop). That will also set `eis.closing`, so the
42724396
* function will return as usual in the next block.
42734397
*
4274-
* Note that `nativeavailable()` has thrown an exception at this
4275-
* point, so we need to be careful about calling further JNI
4276-
* functions. Conventional wisdom states that JNI functions
4277-
* called when an exception is pending “may lead to unexpected
4278-
* results”. Empirically, this seems to work okay; I suspect, at
4279-
* worst, the functions might return an error. */
4398+
* `nativeavailable()` has thrown an exception at this point, so
4399+
* in order to call anything else which uses exceptions (like
4400+
* the locking functions), we'll temporarily clear the exception
4401+
* then reset it before continuing. */
4402+
jthrowable hardwareException = (*env)->ExceptionOccurred(env);
4403+
(*env)->ExceptionClear(env);
4404+
4405+
if (lock_monitor_thread(env, jobj)) goto end;
42804406
RXTXPort(interruptEventLoop)(env, jobj);
4407+
if (unlock_monitor_thread(env, jobj)) goto end;
4408+
4409+
(*env)->Throw(env, hardwareException);
42814410
}
42824411
/* nothing goes between this call and select */
42834412
if( eis.closing )
@@ -4962,7 +5091,22 @@ JNIEXPORT void JNICALL RXTXPort(interruptEventLoop)(JNIEnv *env,
49625091
usleep(1000);
49635092
}
49645093
}
5094+
49655095
index->eventloop_interrupted = 1;
5096+
5097+
jfieldID monThreadisInterruptedField = (*env)->GetFieldID(
5098+
env,
5099+
(*env)->GetObjectClass(env, jobj),
5100+
"monThreadisInterrupted",
5101+
"Z");
5102+
if ((*env)->ExceptionCheck(env)) {
5103+
return;
5104+
}
5105+
(*env)->SetBooleanField(env, jobj, monThreadisInterruptedField, JNI_TRUE);
5106+
if ((*env)->ExceptionCheck(env)) {
5107+
return;
5108+
}
5109+
49665110
/*
49675111
Many OS's need a thread running to determine if output buffer is
49685112
empty. For Linux and Win32 it is not needed. So closing is used to

0 commit comments

Comments
 (0)