Synchronizing native threads with reader/writer locks at Android NDK

The previous two recipes cover thread synchronization with mutex and conditional variables. This recipe discusses reader/writer locks in Android NDK.

Getting ready...

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.

How to do it...

The following steps will help you create an Android project that demonstrates the usage of the pthread reader/writer lock:

  1. Create an Android application named 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.
  2. Right-click on the project NativeThreadsRWLock, select Android Tools | Add Native Support.
  3. Add a Java file named MainActivity.java under package cookbook.chapter6.nativethreadsrwlock. This Java file simply loads the native library NativeThreadsRWLock and calls the native method jni_start_threads.
  4. Add two files named 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;
    }
  5. Add an 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)
  6. Build and run the Android project, and use the following command to monitor the logcat output:
    $ adb logcat -v time NativeThreadsRWLock:I *:S

    The logcat output is shown as follows:

    How to do it...

How it works...

The reader/writer lock is internally implemented with a mutex and a conditional variable. It has the following rules:

  • If a thread tries to acquire a read lock for a resource, it can succeed as long as no other threads hold a write lock for the resource.
  • If a thread tries to acquire a write lock for a resource, it can succeed only when no other threads hold a write or read lock for the resource.
  • The reader/writer lock guarantees only one thread can modify (need to get the write lock) the resource, while permitting multiple threads to read the resource (need to get the read lock). It also makes sure no reads happen when the resource is being changed. In the following sections we describe the reader/writer lock functions provided by Android pthread.h.

Initialize and destroy a reader/writer lock

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.

Note

There is also a macro PTHREAD_RWLOCK_INITIALIZER defined to initialize a reader/writer lock. The default attributes are used in this case.

Using a reader/writer lock

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:

Using a reader/writer lock

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:

Using a reader/writer lock

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's more...

There are a few more read/write lock functions defined in pthread.h.

Timed read/write lock and trylock

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

Reader/writer lock attribute functions

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset