Synchronizing native threads with mutex at Android NDK

This recipe discusses how to use pthread mutex at Android NDK.

How to do it...

The following steps help to create an Android project that demonstrates the usage of pthread mutex:

  1. Create an Android application named NativeThreadsMutex. Set the package name as cookbook.chapter6.nativethreadsmutex. 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 NativeThreadsMutex, select Android Tools | Add Native Support.
  3. Add a Java file named MainActivity.java under the cookbook.chapter6.nativethreadsmutex package. This Java file simply loads the native NativeThreadsMutex library and calls the native jni_start_threads method.
  4. Add two files named mylog.h and NativeThreadsMutex.cpp in the jni folder. NativeThreadsMutex.cpp contains the code to start two threads. The two threads will update a shared counter. A part of the code is shown as follows:

    The run_by_thread1 function is executed by the first native thread:

    int cnt = 0;
    int THR = 10;
    void *run_by_thread1(void *arg) {
      int* threadNum = (int*)arg;
      while (cnt < THR) {
        pthread_mutex_lock(&mux1);
        while ( pthread_mutex_trylock(&mux2) ) {
          pthread_mutex_unlock(&mux1);  //avoid deadlock
          usleep(50000);  //if failed to get mux2, release mux1 first
          pthread_mutex_lock(&mux1);
        }
        ++cnt;
        LOGI(1, "thread %d: cnt = %d", *threadNum, cnt);
        pthread_mutex_unlock(&mux1);
        pthread_mutex_unlock(&mux2);
        sleep(1);
      }
    }

    The run_by_thread2 function is executed by the second native thread:

    void *run_by_thread2(void *arg) {
      int* threadNum = (int*)arg;
      while (cnt < THR) {
        pthread_mutex_lock(&mux2);
        while ( pthread_mutex_trylock(&mux1) ) {
          pthread_mutex_unlock(&mux2);  //avoid deadlock
          usleep(50000);   //if failed to get mux2, release mux1 first
          pthread_mutex_lock(&mux2);
        }
        ++cnt;
        LOGI(1, "thread %d: cnt = %d", *threadNum, cnt);
        pthread_mutex_unlock(&mux2);
        pthread_mutex_unlock(&mux1);
        sleep(1);
      }
    }
  5. Add an Android.mk file in the jni folder with the following content:
    LOCAL_PATH := $(call my-dir)
    include $(CLEAR_VARS)
    LOCAL_MODULE := NativeThreadsMutex
    LOCAL_SRC_FILES := NativeThreadsMutex.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 NativeThreadsMutex:I *:S

    The logcat output is shown as follows:

    How to do it...
  7. We also implemented a native method jni_start_threads_dead in NativeThreadsMutex.cpp, which can probably cause a deadlock (we may need to run the code a few times to produce the deadlock situation). If we call jni_start_threads_dead in MainActivity.java, the two threads will start and then block as shown in the following logcat output:
    How to do it...

    As indicated in this screenshot, the two threads cannot proceed after started.

How it works...

The sample project demonstrates how to use mutex to synchronize native threads. We describe the details as follows:

Initialize and destroy mutex

A mutex can be initialized with the pthread_mutex_init function, which has the following prototype:

int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);

The input argument mutex is a pointer to the mutex to be initialized and attr indicates the attributes of mutex. If attr is set to NULL, the default attributes will be used. The function will return a zero if the mutex is initialized successfully and a non-zero value otherwise.

Note

A macro PTHREAD_MUTEX_INITIALIZER is also defined in pthread.h to initialize a mutex with default attributes.

When we are done with the mutex, we can destroy it with the pthread_mutex_destroy function, which has the following prototype:

int pthread_mutex_destroy(pthread_mutex_t *mutex);

The input argument is a pointer pointing to the mutex to be destroyed.

In our sample project, we created two mutexes mux1 and mux2 to synchronize the access of a shared counter cnt by the two threads. After the two threads exit, we destroyed the mutexes.

Using the mutex

The following four functions are available to lock and unlock a mutex:

int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_lock_timeout_np(pthread_mutex_t *mutex, unsigned msecs);

In all the four functions, the input argument refers to the mutex object in use. A zero return value indicates the mutex is locked or unlocked successfully. The last function allows us to specify a wait timeout in milliseconds. If it cannot acquire the mutex after the timeout, it will return EBUSY to indicate failure.

Note

The pthread_mutex_timedlock function is defined in some pthread implementations to allow us to specify a timeout value. However, this function is not available in the Android Bionic C library.

We demonstrated the usage of the functions previously in our example. In function run_by_thread1, we first lock mux1 by pthread_mutex_lock, and then mux2 by pthread_mutex_trylock. If mux2 cannot be locked, we unlock mux1, sleep for 50 milliseconds, and try again. If mux2 can be locked, we update the shared counter cnt, log its current value, and then release the two mutexes. Another function run_by_thread2 is similar to run_by_thread1, except that it locks mux2 first, and then mux1. The two functions are executed by two threads. This can be illustrated by the following diagram:

Using the mutex

As shown in the preceding diagram, thread 1 needs to obtain mux1, then mux2 in order to update cnt, while thread 2 needs to acquire mux2, then mux1 to update cnt. In case thread 1 locked mux1 and thread 2 locked mux2, neither threads can proceed. This corresponds to the situation where pthread_mutex_trylock returns a nonzero value. If this happens, one thread will give up its mutex so the other thread can proceed to update the shared counter cnt and release the two mutexes. Note that we can replace the pthread_mutex_trylock with pthread_mutex_lock_timeout_np in our code. Readers are encouraged to try it out themselves.

We also implemented a native method jni_start_threads_dead which will probably cause a deadlock. The thread setup is similar to the previous case, but we use pthread_mutex_lock instead of pthread_mutex_trylock, and the threads do not give up the mutexes they have already locked. This can be illustrated as shown in the following diagram:

Using the mutex

Thread 1 tries to lock mux1 and then mux2, while thread 2 tries to lock mux2 and then mux1. In case where thread 1 has locked mux1 and thread 2 has locked mux2, none of the threads can proceed. Because they won't give up the mutexes they've obtained, the two threads will be blocked forever. This is referred to as a deadlock.

There's more...

Recall that second input argument for function pthread_mutex_init is a pointer to pthread_mutexattr_t. A few functions are defined in pthread.h to initialize, manipulate, and destroy mutex attributes, including:

  • pthread_mutexattr_init
  • pthread_mutexattr_destroy
  • pthread_mutexattr_gettype
  • pthread_mutexattr_settype
  • pthread_mutexattr_setpshared
  • pthread_mutexattr_getpshared

Interested readers can look into the pthread.h header file for more information.

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

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