线程可以在同一个进程中同时执行多个任务。线程共享进程的内存和资源。单个进程可以包含同时运行的多个线程。因为在同一个进程中,所以可以通信和共享数据。Android支持Java和native线程。本章中将会介绍native多线程的技巧:
- Java 和 POSIX 线程对比
- 线程同步
- 控制线程生命周期
- 线程属性和调度策略
- 在native线程中和 Java交互
创建线程样例工程
在进一步深入native 多线程代码之前,先创建一个简单的测试例子。样例工程将会提供以下功能:- 一个支持native代码的Android project
- 一个可以定义线程数量,每个线程迭代次数,一个开启线程的button和一个显示native线程运行进度的TextView
- 一个模拟耗时工作的native方法
本章中将会通过这个简单的应用演示native代码多线程相关的技术。
创建Android工程
工程非常简单,UI什么的就不多介绍了。重点看一下里面的方法:
onNativeMessage: 给native调用,用于传递进度的方法。由于Android不允许在子线程中操作UI,而native代码在子线程中运行,所以onNativeMessage方法需要切换到主线程更新UIstartThread:将开始的指令发送到指定线程中。在后续的章节中,会使用到线程的各种功能。这个方法会在这些例子中不停被使用nativeInit: native需要实现的代码,用于执行每个线程前的初始化工作nativeFree: native中实现的代码,在Activity销毁的时候释放native资源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方法实现多线程的利弊
好处:- 比较容易实现
- native代码不需要任何改变
- 由于Java线程是Java平台的一部分,所以不需要显式的把线程attach到JVM中。Native代码可以使用线程指定的
JNIEnv接口指针来直接和Java代码通信 - Java提供的
java.lang.Thread可以使用Java代码实现和线程的无缝交互
缺点:
- Java线程实现native多线程限制了调用方只能在Java代码中发起多线程任务
- 这个方案假设了native代码是线程安全的,因为Java的多线程实现对于native来说是一样的
- Native代码无法使用信号量等多线程并发工具,因为Java Thread没有提供相关API
- 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);
方法接收以下参数:
- 一个pthread_t类型的指针,用于返回指向新创建线程的句柄
- 一个pthread_attr_t类型的指针,指向
pthread_attr_t。这个结构体里面包含了新创建线程的Stack base,stack size,guard size,调度策略和调度优先级,后续会介绍这些属性。这个值默认可以是NULL。 - 一个开启线程开始方法的方法指针。这个开始方法的方法名如下:
void* _Nonnull (* _Nonnull __start_routine)(void* _Nonnull)
这个方法接受一个void指针作为线程参数,并且通过void指针返回结果。
- 任何传入线程开启方法的参数都应该使用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方法
步骤如下:- 导入
#include <pthread.h>头文件 Java_com_example_nativeexe_MainActivity_nativeWorker需要两个参数worker ID 和 迭代次数,但是pthread_create线程开启方法只能传入一个void 指针参数。所以就需要将这两个参数做成一个结构体,然后再将这个结构体的指针传入开启方法。这就需要增加NatvieWorkerArgs结构体:
struct NativeWorkerArgs {
jint id;
jint iterations;
};
- 由于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;
- 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;
}
- 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;
}
- 全局引用变量需要在适当的时候被销毁,否则会造成内存泄漏。这就要用到之前的
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;
}
}
- 为了在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;
}
- 现在前置条件都完成了,需要实现
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);
这个方法有两个入参:
- 创建线程返回的句柄
- 指向线程开启方法返回结果的指针
这个方法会阻塞当前调用线程直到目标线程执行结束。如果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线程最常用的两个同步机制:- Mutexes,可以用来确保里面的代码在同一时间只能有一个线程执行
- Semaphores,可以对可访问的资源控制同时访问的数量。如果资源还没准备好,会阻塞当前线程直到资源处于可用状态。
使用Mutexes同步POSIX线程
POSIX线程API通过`pthread_mutex_t` 数据类型来提供native代码互斥锁功能。在进一步了解更多的API之前,需要初始化mutex变量。初始化Mutexes
POSIX线程API提供了两种初始化mutexus的方法:pthread_mutex_init方法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在上面例子中完成一个线程完成打印任务后另外一个线程再打印,即打印任务不能同时被多个线程执行:- 在native代码中增加mutex变量
//mutex 变量
static pthread_mutex_t mutex;
- 在使用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;
}
- 使用结束后需要销毁mutex。这个工作可以放在之前free函数中:
if (0 != pthread_mutex_destroy(&mutex)) {
jclass clazz = env->FindClass("java/lang/RuntimeException");
env->ThrowNew(clazz, "Unable to destroy mutex");
}
- 在实际打印的工作方法代码片段开始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);
该方法有三个入参:
- 一个指向semaphore变量的指针
- shared flag
- 初始化值
方法调用成功返回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线程规定了一些调度策略。最常用的如下:SCHED_FIFO: First in, first out策略,即先创建的线程先执行。当然根据线程的优先级可以在等待的线程list中再做一个排序。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);
方法有三个入参:
- 指向目标线程的句柄指针
- 调度策略
- 调度策略的参数