Native Thread

155 阅读17分钟

线程可以在同一个进程中同时执行多个任务。线程共享进程的内存和资源。单个进程可以包含同时运行的多个线程。因为在同一个进程中,所以可以通信和共享数据。Android支持Java和native线程。本章中将会介绍native多线程的技巧:

  1. Java 和 POSIX 线程对比
  2. 线程同步
  3. 控制线程生命周期
  4. 线程属性和调度策略
  5. 在native线程中和 Java交互

创建线程样例工程

在进一步深入native 多线程代码之前,先创建一个简单的测试例子。样例工程将会提供以下功能:
  1. 一个支持native代码的Android project
  2. 一个可以定义线程数量,每个线程迭代次数,一个开启线程的button和一个显示native线程运行进度的TextView
  3. 一个模拟耗时工作的native方法

本章中将会通过这个简单的应用演示native代码多线程相关的技术。

创建Android工程

![](https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/cc64d5955c2848dea7306d0262132d86~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgRnJvbTY0S0I=:q75.awebp?rk3s=f64ab15b&x-expires=1773922049&x-signature=tp53fMuPYTp8yTD%2Bu%2F9dpzVEvig%3D)

工程非常简单,UI什么的就不多介绍了。重点看一下里面的方法:

  1. onNativeMessage : 给native调用,用于传递进度的方法。由于Android不允许在子线程中操作UI,而native代码在子线程中运行,所以onNativeMessage 方法需要切换到主线程更新UI
  2. startThread :将开始的指令发送到指定线程中。在后续的章节中,会使用到线程的各种功能。这个方法会在这些例子中不停被使用
  3. nativeInit: native需要实现的代码,用于执行每个线程前的初始化工作
  4. nativeFree: native中实现的代码,在Activity销毁的时候释放native资源
  5. nativeWorker:native需要实现的代码,用于模拟耗时任务。传入两个参数,work ID和迭代次数。

实现Native代码

```c //method id可以被cache,减少后续不停find消耗 static jmethodID gOnNativeMessage = nullptr;

extern "C" JNIEXPORT void JNICALL Java_com_example_nativeexe_MainActivity_nativeInit(JNIEnv *env, jobject thiz) { //检查method id是否被cache if (nullptr == gOnNativeMessage) { //从jobject获取clazz jclass clazz = env->GetObjectClass(thiz); //获取callback方法method id gOnNativeMessage = env->GetMethodID(clazz, "onNativeMessage", "(Ljava/lang/String;)V");

    //如果相应的callback method id未定义,抛出异常
    if (nullptr == gOnNativeMessage) {
        //创建异常
        jclass exceptionClazz = env->FindClass("java/lang/RuntimeException");
        //抛出异常
        env->ThrowNew(exceptionClazz, "Unable to find method.");
    }
}

} extern "C" JNIEXPORT void JNICALL Java_com_example_nativeexe_MainActivity_nativeWorker(JNIEnv *env, jobject thiz, jint id, jint iterations) { //启动模拟耗时循环 for (int i = 0; i < iterations; ++i) { //prepare message char message[26]; sprintf(message, "Worker %d: Iteration %d", id, i); //Message from the C String jstring messageString = env->NewStringUTF(message); //Call the on native message method env->CallVoidMethod(thiz, gOnNativeMessage, messageString); //Check if an exception occurred if (nullptr != env->ExceptionOccurred()) { break; }

    //Sleep for a second
    sleep(1);
}

} extern "C" JNIEXPORT void JNICALL Java_com_example_nativeexe_MainActivity_nativeFree(JNIEnv *env, jobject thiz) {

}


native部分代码整体比较简单,这里不再赘述相关逻辑。`nativeFree` 代码将会在后续的章节中陆续实现。

<h2 id="Xpy4Z">Java线程</h2>
在Java多线程环境中运行native代码是最简单的多线程运行native代码的方式。在Java中`java.lang.Thread`中运行native 代码而不需要修改native代码。

<h3 id="TMcgN">使用Java线程完成任务</h3>
在MainActivity增加如下代码:

```kotlin
private fun javaThreads(threads: Int, iteration: Int) {
    for (i in 1..threads) {
        val thread = Thread {
            nativeWorker(i, iteration)
        }
        thread.start()
    }
}

private fun startThread(threadCount: Int, iterationCount: Int) {
     nativeInit()
    javaThreads(threadCount, iterationCount)
}

代码逻辑非常简单,创建threads个线程,并将参数传入native方法中,然后在startThread中调用运行。

使用Java线程调用native方法实现多线程的利弊

好处:
  1. 比较容易实现
  2. native代码不需要任何改变
  3. 由于Java线程是Java平台的一部分,所以不需要显式的把线程attach到JVM中。Native代码可以使用线程指定的JNIEnv 接口指针来直接和Java代码通信
  4. Java提供的java.lang.Thread可以使用Java代码实现和线程的无缝交互

缺点:

  1. Java线程实现native多线程限制了调用方只能在Java代码中发起多线程任务
  2. 这个方案假设了native代码是线程安全的,因为Java的多线程实现对于native来说是一样的
  3. Native代码无法使用信号量等多线程并发工具,因为Java Thread没有提供相关API
  4. Native代码运行在多个分隔的线程中,无法直接交互和共享资源

注意:尽管某些Java Thread的功能可以通过JNI来调用,但是由于通过JNI交互式非常耗费资源,所以也不推荐。

POSIX Threads

POSIX Threads,简称为`Pthread` ,是线程的POSIX标准。在1995年之前,有多个线程API标准。1995年发布了POSIX.1c线程扩展标准,定义了创建和操作线程的通用API。大多数操作系统,例如MicroSoft Windows、Mac OS X、BSD和Linux都提供了符合POSIX标准的的多线程支持。因为Android的基于Linux,所以也提供符合POSIX标准的线程支持。由于POSIX 线程标准包含的内容非常多,本章中只介绍Android平台完全支持的相关API。

在Native代码中使用POSIX Threads

POSIX在`phtread.h` 头文件中声明,使用之前需要首先导入该头文件:
#include <pthread.h>

Android 的POSIX 线程实现在Bionic standard C standart library中。和其他平台不一样,在编译过程中不需要link额外的library。

使用pthread_create创建线程

POSIX Threads通过`pthread_create` 方法创建线程:
int pthread_create(pthread_t* _Nonnull __pthread_ptr, 
                   pthread_attr_t const* _Nullable __attr,
                   void* _Nonnull (* _Nonnull __start_routine)(void* _Nonnull),
                   void* _Nullable);

方法接收以下参数:

  1. 一个pthread_t类型的指针,用于返回指向新创建线程的句柄
  2. 一个pthread_attr_t类型的指针,指向pthread_attr_t。这个结构体里面包含了新创建线程的Stack base,stack size,guard size,调度策略和调度优先级,后续会介绍这些属性。这个值默认可以是NULL。
  3. 一个开启线程开始方法的方法指针。这个开始方法的方法名如下:
void* _Nonnull (* _Nonnull __start_routine)(void* _Nonnull)

这个方法接受一个void指针作为线程参数,并且通过void指针返回结果。

  1. 任何传入线程开启方法的参数都应该使用void指针的形式。如果没有参数也可以是NULL

如果线程创建成功,pthread_create方法返回0,否则返回错误码。

使用POSIX Threads更新样例工程

现在可以通过`prthread_create`使用POSIX Thread来实现多线程功能。

更新MainActivity

在MainActivity中增加如下方法:
external fun posixThreads(threads:Int,iteration: Int)

javaThreads方法一样,posixThreads 也接收两个参数,线程的数量和每个线程worker迭代的次数。相应的将原来调用java线程的方法改成调用posixThreads

实现posixThreads方法

步骤如下:
  1. 导入#include <pthread.h> 头文件
  2. Java_com_example_nativeexe_MainActivity_nativeWorker 需要两个参数worker ID 和 迭代次数,但是pthread_create 线程开启方法只能传入一个void 指针参数。所以就需要将这两个参数做成一个结构体,然后再将这个结构体的指针传入开启方法。这就需要增加NatvieWorkerArgs 结构体:
struct NativeWorkerArgs {
    jint id;
    jint iterations;
};
  1. 由于POSIX线程不是Java的一部分,所以JVM是无法直接感知到这些线程的存在的。为了解决这个问题,POSIX线程需要通过JVM接口指针主动将自己attach到JVM中。一旦attach到JVM中,POSIX线程中worker部分的代码就可以通过onNativeMessage 回调更新UI。这个更新过程需要持有MainActivity 的引用。由于JNI方法提供的对象引用是local的不能被cache。所以需要创建一个全局引用,并且保存起来给后续的线程使用。在natvie代码中增加两个全局变量:
//JVM接口指针
static JavaVM* gVm = nullptr;
//对象的全局引用
static jobject gObj = nullptr;
  1. native代码有多种方式获取JVM接口指针。最简单最合适的方法就是通过JNI_OnLoad 方法。这个方法在shared library被JVM加载完成后自动调用。这个方法有一个JVM接口指针入参,那么可以通过这个方法把JVM接口指针保存在gVm 中:
//JVM接口指针
static JavaVM *gVm = nullptr;
//对象的全局引用
static jobject gObj = nullptr;

jint JNI_OnLoad(JavaVM *vm, void *reversed) {
    //保存JVM接口指针
    gVm = vm;
    return JNI_VERSION_1_6;
}
  1. MainActivity对象引用在调用onNativeMessage 回调方法时需要用到。这时需要修改Java_com_example_nativeexe_MainActivity_nativeInit 方法将MainActivity对象保存到全局引用中:
extern "C"
JNIEXPORT void JNICALL
Java_com_example_nativeexe_MainActivity_nativeInit(JNIEnv *env, jobject thiz) {

    //全局缓存MainActivity对象
    if (nullptr == gObj) {
        gObj = env->NewGlobalRef(thiz);
    }

    if (nullptr == gObj) {
        goto exit;
    }

    //检查method id是否被cache
    if (nullptr == gOnNativeMessage) {
        //从jobject获取clazz
        jclass clazz = env->GetObjectClass(thiz);
        //获取callback方法method id
        gOnNativeMessage = env->GetMethodID(clazz, "onNativeMessage", "(Ljava/lang/String;)V");

        //如果相应的callback method id未定义,抛出异常
        if (nullptr == gOnNativeMessage) {
            //创建异常
            jclass exceptionClazz = env->FindClass("java/lang/RuntimeException");
            //抛出异常
            env->ThrowNew(exceptionClazz, "Unable to find method.");
        }
    }

    exit:
    return;
}
  1. 全局引用变量需要在适当的时候被销毁,否则会造成内存泄漏。这就要用到之前的Java_com_example_nativeexe_MainActivity_nativeFree 方法,在完成任务后销毁全局变量:
extern "C"
JNIEXPORT void JNICALL
Java_com_example_nativeexe_MainActivity_nativeFree(JNIEnv *env, jobject thiz) {
    if (nullptr != gObj) {
        env->DeleteGlobalRef(gObj);
        gObj = nullptr;
    }
}
  1. 为了在POSIX 线程中运行Java_com_example_nativeexe_MainActivity_nativeWorker方法,一个中间方法需要将线程attach到JVM中,获取JVM接口指针,并且传入参数执行native worker:
static void *nativeWorkerThread(void *args) {
    JNIEnv *env = nullptr;
    //attach当前线程到JVM,并且获取JVM接口指针
    if (0 == gVm->AttachCurrentThread(&env, nullptr)) {
        //获取native worker需要的参数
        auto *nativeWorkerArgs = (NativeWorkerArgs *) args;
        //调用native worker方法
        Java_com_example_nativeexe_MainActivity_nativeWorker(env, gObj, nativeWorkerArgs->id,
                                                             nativeWorkerArgs->iterations);
        //释放native worker线程参数
        delete nativeWorkerArgs;
        //从JVM中detach当前线程
        gVm->DetachCurrentThread();
    }

    return (void *) 1;
}
  1. 现在前置条件都完成了,需要实现Java_com_example_nativeexe_MainActivity_posixThreads 方法了。通过pthread_create 创建新线程,然后将参数通过NativeWorkerAgrs 结构体传递给线程启动方法。为了处理异常,抛出一个java.lang.RuntimeException 并且结束:
extern "C"
    JNIEXPORT void JNICALL
Java_com_example_nativeexe_MainActivity_posixThreads(JNIEnv *env, jobject thiz, jint threads,
                                                     jint iteration) {
    //每一个worker创建一个线程
    for (int i = 0; i < threads; ++i) {
        //Native worker线程参数
        auto *nativeWorkerArgs = new NativeWorkerArgs();
        nativeWorkerArgs->id = i;
        nativeWorkerArgs->iterations = iteration;

        //线程句柄
        pthread_t thread;

        //创建新线程
        int result = pthread_create(&thread, nullptr, nativeWorkerThread,
                                    (void *) nativeWorkerArgs);
        if (0 != result) {
            //创建失败抛出异常
            jclass exceptionClazz = env->FindClass("java/lang/RuntimeException");
            env->ThrowNew(exceptionClazz, "Unable to create thread!");
        }
    }

}

�这样就完成了POSIX线程实现多线程。

从POSIX线程返回结果

通过创建线程的句柄,可以实现线程结束的时候可以携带返回结果。上面`Java_com_example_nativeexe_MainActivity_posixThreads`例子中,线程创建后就立刻执行其他代码了。这里也能改成等待线程执行结束然后执行其他代码。通过`pthread_join` 方法可以等待线程执行结束:
int pthread_join(pthread_t thread, void** ret_val);

这个方法有两个入参:

  1. 创建线程返回的句柄
  2. 指向线程开启方法返回结果的指针

这个方法会阻塞当前调用线程直到目标线程执行结束。如果ret_val 不是NULL的,这个方法会将线程开启方法执行的结果的指针赋值给它。如果线程执行正常,该方法返回0,否则返回其他错误码。

更新native代码使用pthread_join

为了直观的感受`phtread_join` 的作用,需要对代码做一些修改:
extern "C"
JNIEXPORT void JNICALL
Java_com_example_nativeexe_MainActivity_posixThreads(JNIEnv *env, jobject thiz, jint threads,
                                                     jint iteration) {
    //线程句柄
    auto *handles = new pthread_t[threads];

    //每一个worker创建一个线程
    for (int i = 0; i < threads; ++i) {
        //Native worker线程参数
        auto *nativeWorkerArgs = new NativeWorkerArgs();
        nativeWorkerArgs->id = i;
        nativeWorkerArgs->iterations = iteration;

        //线程句柄
        pthread_t thread;

        //创建新线程
        int result = pthread_create(&handles[i], nullptr, nativeWorkerThread,
                                    (void *) nativeWorkerArgs);
        if (0 != result) {
            //创建失败抛出异常
            jclass exceptionClazz = env->FindClass("java/lang/RuntimeException");
            env->ThrowNew(exceptionClazz, "Unable to create thread!");

            goto exit;
        }
    }

    MY_LOG_INFO("等待线程结束...");

    //等待线程结束
    for (int i = 0; i < threads; ++i) {
        void *result = nullptr;
        //join每一个线程
        if (0 != pthread_join(handles[i], &result)) {
            jclass exceptionClazz = env->FindClass("java/lang/RuntimeException");
            env->ThrowNew(exceptionClazz, "Unable to join thread");
        } else {
            //准备信息
            char message[26];
            sprintf(message, "Worker %d returned %d", i, result);
            //来自C字符串的信息
            jstring messageString = env->NewStringUTF(message);
            //回调Java对象方法
            env->CallVoidMethod(thiz, gOnNativeMessage, messageString);
            //检查是否有异常
            if (nullptr != env->ExceptionOccurred()) {
                goto exit;
            }
        }
    }

    exit:
    return;

}

上面的方法如果在主线程发起调用会造成UI阻塞,所以把worker和iteraiton的值设置得小一点。

POSIX线程同步

因为线程运行在同一个进程中,所以可以方便的共享内存和资源。但是,这也带来了两个问题:线程干涉和内存一致性。线程同步这时就显得至关重要。线程同步可以确保同时运行的线程不会同时执行同一个代码块。和Java线程类似,POSIX线程也提供了同步控制的API。本章中将会介绍POSIX线程最常用的两个同步机制:
  1. Mutexes,可以用来确保里面的代码在同一时间只能有一个线程执行
  2. Semaphores,可以对可访问的资源控制同时访问的数量。如果资源还没准备好,会阻塞当前线程直到资源处于可用状态。

使用Mutexes同步POSIX线程

POSIX线程API通过`pthread_mutex_t` 数据类型来提供native代码互斥锁功能。在进一步了解更多的API之前,需要初始化mutex变量。

初始化Mutexes

POSIX线程API提供了两种初始化mutexus的方法:
  1. pthread_mutex_init 方法
  2. PHTREAD_MUTEX_INITIALIZER

pthread_mutex_init 方法有两个入参,一个是指向待初始化的mutex的变量指针,另一个是指向pthread_mutextattr_t 类型的控制mutex属性的结构体指针。如果第二个参数是NULL,就会采用默认的属性。如果默认的属性符合需求,那么也可以用PHTREAD_MUTEX_INITIALIZER 宏。两者使用代码:

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

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

如果初始化成功,mutex处于已初始化和unlock状态,初始化的方法返回0,否则返回错误码。

Mutexes加锁

已初始化的mutex通过`pthread_mutex_lock` 方法可以用于获取互斥控制:
int pthread_mutex_lock(pthread_mutex_t* mutex);

方法有一个mutex指针入参。如果mutex已经处于lock状态,调用这个方法会阻塞当前线程,直到mutex处于解锁状态。如果方法调用成功返回0,否则返回错误码。

Mutexes解锁

在执行完临界区代码后,可以通过`pthread_mutex_unlock` 方法unlock当前mutex:
int pthread_mutex_unlock(pthread_mutex_t* mutex);

方法有一个mutex指针类型的入参,可以unlock对应的mutex。调度策略会决定正在等待的哪一个线程会获取加锁权限。如果这个方法调用成功返回0,否则返回错误码 。

销毁Mutexes

一旦mutex不再需要使用,可以通过`pthread_mutex_destroy` 方法销毁当前锁。这个方法有一个指向mutex指针的入参。尝试销毁一个处于lock状态的mutex会导致未知的行为:
int pthread_mutex_destroy(pthread_mutex_t* mutex);

在样例代码中使用Mutex

接下来将会通过Mutex在上面例子中完成一个线程完成打印任务后另外一个线程再打印,即打印任务不能同时被多个线程执行:
  1. 在native代码中增加mutex变量
//mutex 变量
static pthread_mutex_t mutex;
  1. 在使用mutex变量之前需要初始化。可以放在之前的init方法中:
if (0 != pthread_mutex_init(&mutex, nullptr)) {
    jclass clazz = env->FindClass("java/lang/RuntimeException");
    env->ThrowNew(clazz, "Unable to init mutex");
    goto exit;
}
  1. 使用结束后需要销毁mutex。这个工作可以放在之前free函数中:

if (0 != pthread_mutex_destroy(&mutex)) {
    jclass clazz = env->FindClass("java/lang/RuntimeException");
    env->ThrowNew(clazz, "Unable to destroy mutex");
}
  1. 在实际打印的工作方法代码片段开始lock初始化好的mutex,在代码片段结束unlock 相应的mutex:
//lock mutex
if (0 != pthread_mutex_lock(&mutex)) {
    jclass clazz = env->FindClass("java/lang/RuntimeException");
    env->ThrowNew(clazz, "Unable to lock mutex");
    goto exit;
}

....
//unlock mutex
if (0 != pthread_mutex_unlock(&mutex)) {
    jclass clazz = env->FindClass("java/lang/RuntimeException");
    env->ThrowNew(clazz, "Unable to unlock mutex");
    goto exit;
}

exit:
return;

可以看到打印出来的信息worker id之前没有出现交错,每一个id打印完成后随后的id才能打印。

在POSIX线程中使用Semaphore控制同步

和POSIX其他方法不同,POSIX Semaphore在其他头文件中声明`semaphore.h` :
#include <semaphore.h>

native代码通过使用sem_t数据类型来使用POSIX semaphore。在使用之前首先要初始化该semaphore变量:

初始化Semaphore

POSIX semaphore通过`sem_init` 方法来初始化semaphore:
extern int sem_init(sem_t* sem, int pshared, unsigned int value);

该方法有三个入参:

  1. 一个指向semaphore变量的指针
  2. shared flag
  3. 初始化值

方法调用成功返回0,否则返回-1;

Locking Semaphore

一旦semaphore被初始化,线程可以使用`sem_wait` 方法来增加semaphore的数值:
extern int sem_wait(sem_t* sem);

方法有一个semaphore指针作为入参。如果semaphore的值大于0,lock成功,semaphore的值也相应递减。如果semaphore的值是0,当前线程会被阻塞直到semaphore被其他线程unlock,semaphore的值增加。方法调用成功返回0,否则返回-1。

Unlocking Semaphore

一旦结束执行临界区代码,线程可以通过`sem_post` 方法解锁semaphore:
extern int sem_post(sem_t* sem);

方法有一个semaphore 指针入参,当semaphore通过这个方法unlock的时候,semaphore的值增加1。调度策略会决定正在等待信号量的哪一个线程被执行。方法调用成功返回0,否则返回-1。

销毁Semaphore

semaphore使用完成后,可以通过`sem_destory` 方法销毁semaphore:
extern int sem_destroy(sem_t* sem);

方法有一个semaphore指针入参。销毁一个正在block其他线程的semaphore会导致未定义的行为。方法调用成功返回0,否则返回-1。

POSIX线程的优先级和调度策略

线程优先级和调度策略规定了线程执行的顺序。本章将会简单介绍调度策略和线程优先级。

POSIX线程调度策略

POSIX线程规定了一些调度策略。最常用的如下:
  1. SCHED_FIFO : First in, first out策略,即先创建的线程先执行。当然根据线程的优先级可以在等待的线程list中再做一个排序。
  2. SHCED_RR : 时间片轮转策略。基本策略和上面的SCHED_FIFO策略相同,但是增加线程执行的闲置时间来防止线程独占CPU。

这些线程调度策略常量定义在sched.h头文件中。调度策略可以在使用pthread_create方法创建线程通过sched_policy 属性结构体pthread_attr_t传入,也可以在运行时使用phread_setschedparam 方法设置。

int pthread_setschedparam(pthread_t thid, 
                          int poilcy, 
                          struct sched_param const* param);

方法有三个入参:

  1. 指向目标线程的句柄指针
  2. 调度策略
  3. 调度策略的参数

POSIX线程优先级

POSIX线程API提供了调整优先级的方法。线程优先级可以通过创建线程时`pthread_create` 方法传入的`pthread_attr_t`入参中`sched_priority` 属性或者在运行时使用`pthread_setschedparam` 方法的`sched_param`入参中线程优先级来调整线程的优先级。优先级的最大值和最小值在不同的调度策略中不同,应用可以通过`sched_get_priority_max` 和 `sched_get_priority_min`方法获取。