We have covered mutex, conditional variables, and reader/writer lock in the previous three recipes. This is the last recipe on threads synchronization at Android NDK, and we will discuss semaphores.
Readers are expected to read through the previous three recipes, Synchronizing native threads with mutex at Android NDK, Synchronizing native threads with conditional variables at Android NDK, and Synchronizing native threads with reader/writer locks at Android NDK, before this one.
The following steps will help you create an Android project that demonstrates the usage of pthread reader/writer lock:
NativeThreadsSemaphore
. Set the package name as cookbook.chapter6.nativethreadssemaphore
. 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.nativethreadssemaphore
. This Java file simply loads the native library NativeThreadsSemaphore
and calls the native jni_start_threads
method.mylog.h
and NativeThreadsSemaphore.cpp
under the jni
folder. A part of the code in NativeThreadsSemaphore.cpp
is shown as follows:jni_start_threads
creates pNumOfConsumer
number of consumer threads, pNumOfProducer
number of producer threads, and numOfSlots
number of slots:
void jni_start_threads(JNIEnv *pEnv, jobject pObj, int pNumOfConsumer, int pNumOfProducer, int numOfSlots) { pthread_t *ths; int i, ret; int *thNum; pthread_mutex_init(&mux, NULL); sem_init(&emptySem, 0, numOfSlots); sem_init(&fullSem, 0, 0); ths = (pthread_t*)malloc(sizeof(pthread_t)*(pNumOfConsumer+pNumOfProducer)); thNum = (int*)malloc(sizeof(int)*(pNumOfConsumer+pNumOfProducer)); for (i = 0; i < pNumOfConsumer + pNumOfProducer; ++i) { thNum[i] = i; if (i < pNumOfConsumer) { ret = pthread_create(&ths[i], NULL, un_by_consumer_thread, (void*)&(thNum[i])); } else { ret = pthread_create(&ths[i], NULL, run_by_producer_thread, (void*)&(thNum[i])); } } for (i = 0; i < pNumOfConsumer+pNumOfProducer; ++i) { ret = pthread_join(ths[i], NULL); } sem_destroy(&emptySem); sem_destroy(&fullSem); pthread_mutex_destroy(&mux); free(thNum); free(ths); }
run_by_consumer_thread
is the function executed by the consumer thread:
void *run_by_consumer_thread(void *arg) { int* threadNum = (int*)arg; int i; for (i = 0; i < 4; ++i) { sem_wait(&fullSem); pthread_mutex_lock(&mux); --numOfItems; pthread_mutex_unlock(&mux); sem_post(&emptySem); } return NULL; }
run_by_producer_thread
is the
function executed by producer thread:
void *run_by_producer_thread(void *arg) { int* threadNum = (int*)arg; int i; for (i = 0; i < 4; ++i) { sem_wait(&emptySem); pthread_mutex_lock(&mux); ++numOfItems; pthread_mutex_unlock(&mux); sem_post(&fullSem); } return NULL; }
Android.mk
file under the jni
folder with the following content:LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE := NativeThreadsSemaphore LOCAL_SRC_FILES := NativeThreadsSemaphore.cpp LOCAL_LDLIBS := -llog include $(BUILD_SHARED_LIBRARY)
logcat
output:$ adb logcat -v time NativeThreadsSemaphore:I *:S
The logcat
output is shown in the following screenshot:
Semaphores are essentially integer counters. Two primary operations are supported by a semaphore:
post
is called somewhere else to increment semaphore value.The semaphore related functions are defined in semaphore.h
rather than pthread.h
. Next, we describe a few key functions.
The following three functions are defined to initialize or destroy a semaphore:
extern int sem_init(sem_t *sem, int pshared, unsigned int value); extern int sem_init(sem_t *, int, unsigned int value); extern int sem_destroy(sem_t *);
The first two functions are used to initialize a semaphore. They both initialize the semaphore pointed by the input argument sem
with the value indicated by the argument value
. The first function also accepts an argument pshared
, which should be set to zero for thread synchronization. If it is set to nonzero, the semaphore can be shared between processes, which is not supported on Android and therefore not discussed.
The following functions are defined to use a semaphore.
extern int sem_trywait(sem_t *); extern int sem_wait(sem_t *); extern int sem_post(sem_t *); extern int sem_getvalue(sem_t *, int *);
The first two functions are used to wait on a semaphore. If the semaphore value is not zero, then the value is decreased by one. If the value is zero, the first function will return a nonzero value to indicate failure, while the second function will block the calling thread. The third function is used to increase the semaphore value by one, and the last function is used to query the value of the semaphore. Note that the value is returned through the second input argument rather than the return value.
In our sample project, we used two semaphores emptySem
and fullSem
, and a mutex mux
. The app will create a few producer threads and consumer threads. The emptySem
semaphore is used to indicate the number of slots available to store the items produced by the producer thread, while fullSem
refers to the number of items for the consumer thread to consume. The mutex mux
is used to ensure no two threads can access the shared counter numOfItems
at one time.
The producer thread will need to wait on the emptySem
semaphore. When it is unblocked, the producer has obtained an empty slot. It will lock mux
and then update the shared count numOfItems
, which means a new item has been produced. Therefore, it will call the post
function on fullSem
to increment its value.
On the other hand, the consumer thread will wait on fullSem
. When it is unblocked, the consumer has consumed an item. It will lock mux
and then update the shared count numOfItems
. A new empty slot is available because of the item consumed, so the consumer thread will call post on emptySem
to increment its value.