The previous two recipes cover thread synchronization with mutex and conditional variables. This recipe discusses reader/writer locks in Android NDK.
Readers are recommended to read the previous two recipes, Synchronizing native threads with mutex at Android NDK and Synchronizing native threads with conditional variables at Android NDK, before going through this one.
The following steps will help you create an Android project that demonstrates the usage of the pthread reader/writer lock:
NativeThreadsRWLock
. Set the package name as cookbook.chapter6.nativethreadsrwlock
. Refer to the Loading native libraries and registering native methods recipe in Chapter 2, Java Native Interface for more detailed instructions.MainActivity.java
under package cookbook.chapter6.nativethreadsrwlock
. This Java file simply loads the native library NativeThreadsRWLock
and calls the native method jni_start_threads
.mylog.h
and NativeThreadsRWLock.cpp
under the jni
folder. A part of the code in NativeThreadsRWLock.cpp
is shown as follows:jni_start_threads
starts pNumOfReader
reader threads and pNumOfWriter
writer threads:
void jni_start_threads(JNIEnv *pEnv, jobject pObj, int pNumOfReader, int pNumOfWriter) { pthread_t *ths; int i, ret; int *thNum; ths = (pthread_t*)malloc(sizeof(pthread_t)*(pNumOfReader+pNumOfWriter)); thNum = (int*)malloc(sizeof(int)*(pNumOfReader+pNumOfWriter)); pthread_rwlock_init(&rwlock, NULL); for (i = 0; i < pNumOfReader + pNumOfWriter; ++i) { thNum[i] = i; if (i < pNumOfReader) { ret = pthread_create(&ths[i], NULL, run_by_read_thread, (void*)&(thNum[i])); } else { ret = pthread_create(&ths[i], NULL, run_by_write_thread, (void*)&(thNum[i])); } } for (i = 0; i < pNumOfReader+pNumOfWriter; ++i) { ret = pthread_join(ths[i], NULL); } pthread_rwlock_destroy(&rwlock); free(thNum); free(ths); }
The run_by_read_thread
function is
executed by the reader threads:
void *run_by_read_thread(void *arg) { int* threadNum = (int*)arg; int ifRun = 1; int accessTimes = 0; int ifPrint = 1; while (ifRun) { if (!pthread_rwlock_rdlock(&rwlock)) { if (100000*numOfWriter == sharedCnt) { ifRun = 0; } if (0 <= sharedCnt && ifPrint) { LOGI(1, "reader thread %d sharedCnt value before processing %d ", *threadNum, sharedCnt); int j, k;//some dummy processing for (j = 0; j < 100000; ++j) { k = j*2; k = sqrt(k); } ifPrint = 0; LOGI(1, "reader thread %d sharedCnt value after processing %d %d ", *threadNum, sharedCnt, k); } if ((++accessTimes) == INT_MAX/5) { accessTimes = 0; LOGI(1, "reader thread %d still running: %d ", *threadNum, sharedCnt); } pthread_rwlock_unlock(&rwlock); } } LOGI(1, "reader thread %d return %d ", *threadNum, sharedCnt); return NULL; }
The run_by_write_thread
function is executed by the writer threads:
void *run_by_write_thread(void *arg) { int cnt = 100000, i, j, k; int* threadNum = (int*)arg; for (i = 0; i < cnt; ++i) { if (!pthread_rwlock_wrlock(&rwlock)) { int lastShCnt = sharedCnt; for (j = 0; j < 10; ++j) { //some dummy processing k = j*2; k = sqrt(k); } sharedCnt = lastShCnt + 1; pthread_rwlock_unlock(&rwlock); } } LOGI(1, "writer thread %d return %d %d ", *threadNum, sharedCnt, k); return NULL; }
Android.mk
file under the jni
folder with the following content:LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE := NativeThreadsRWLock LOCAL_SRC_FILES := NativeThreadsRWLock.cpp LOCAL_LDLIBS := -llog include $(BUILD_SHARED_LIBRARY)
logcat
output:$ adb logcat -v time NativeThreadsRWLock:I *:S
The logcat
output is shown as follows:
The reader/writer lock is internally implemented with a mutex and a conditional variable. It has the following rules:
pthread.h
.The following two functions are defined to initialize and destroy a reader/writer lock:
int pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr); int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
pthread_rwlock_init
initializes a reader/writer lock pointed by the rwlock
argument with the attributes referred by argument attr
. If attr
is set to NULL
, the default attributes are used.
pthread_rwlock_destroy
accepts a pointer to a reader/writer lock and destroys it.
The following two functions are defined to acquire a read and a write lock respectively:
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock); int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
Both functions accept a pointer to the reader/writer lock and return a zero to indicate success. If the lock cannot be acquired, the calling thread will be blocked until the block is available or till an error occurs.
The following function is defined to unlock either read lock or write lock:
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
In our sample code, we demonstrated the usage of these functions. In run_by_read_thread
function, the read threads need to acquire the read lock in order to access the value of the shared resource sharedCnt
. In the run_by_write_thread
function, the write threads need to acquire the write lock before updating the shared resource sharedCnt
.
If we remove the code which locks and unlocks the read and write lock, build the application, and rerun it, the output is as shown in the following screenshot:
As shown in the output, the shared resource sharedCnt
is updated to a value less than the final value when reader/writer lock is enabled. The reason is illustrated in the following diagram:
In this diagram, two writers get the same value (N) of the shared counter, and both update the value from N to N+1. When they write the value back to the shared counter, the shared counter is updated from N to N+1 although it is updated twice by two writers. This illustrates why we need write lock. Also note at reader threads, two reads of the sharedCnt
(one before processing and one after processing) give us two different values because the writers have updated the value. This may not be desirable sometimes and that is why a read lock is necessary at times.
There are a few more read/write lock functions defined in pthread.h
.
Android pthread.h
defines the following two
functions to allow the calling thread to specify a timeout value when trying to acquire the read or write lock:
int pthread_rwlock_timedrdlock(pthread_rwlock_t *rwlock, const struct timespec *abs_timeout); int pthread_rwlock_timedwrlock(pthread_rwlock_t *rwlock, const struct timespec *abs_timeout);
In addition, the following two functions are available for the calling thread to acquire read or write lock without blocking itself. If the lock is not available, the functions will return a nonzero value instead of blocking:
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock); int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
Android pthread.h
defines a set of functions to initialize and manipulate a reader/writer lock attribute, which can be passed to pthread_rwlock_init
as the second argument. These functions include pthread_rwlockattr_init
, pthread_rwlockattr_destroy
, pthread_rwlockattr_setpshared
, and pthread_rwlockattr_getpshared
. They are not used often in Android NDK development and therefore not discussed here.