Android NDK——必知必会之Native线程操作及线程同步全面详解(六)

版权声明:本文为CrazyMo_原创,转载请在显著位置注明原文链接 https://blog.csdn.net/CrazyMo_/article/details/82498581

引言

无论是做Java开发还是Android开发相信大家对于多线程都不会陌生,与Java一样Native 也有多线程以及相关机制,在Java层创建多线程很简单,主要是因为Java 封装得足够好,而要创建Native线程则逻辑相对复杂些,需要依赖POSIX线程。

一、Pthreads概述

POSIX线程(POSIX threads)又简称Pthreads是线程的POSIX标准,该标准定义了创建和操纵线程的一整套API,在类Unix操作系统(Unix、Linux、Mac OS X等)中都使用Pthreads作为操作系统的线程,Windows操作系统也有其移植版pthreads-win32。简而言之该标准定义内部API创建和操纵线程, Pthreads定义了一套 C程序语言类型、函数与常量,它以 pthread.h 头文件和一个线程库实现,所以在Android Studio使用时直接在C/C++ 文件中**#include < pthread.h >**引入即可,再VisualStudio 使用则麻烦些,需要先下载把对应的库添加到系统的库文件目录(32位拷贝pthreadVC2.dll 到windows/syswow64目录;64位拷贝pthreadVC2.dll 到windows/system32目录)中然后在VisualStudio中还需要配置Cmake脚本,才能引用

cmake_minimum_required (VERSION 3.8)
include_directories("XXX/pthreads-w32-2-9-1-release/Pre-built.2/include")
#设置变量为x64 or x86
if(CMAKE_CL_64)
    set(platform x64)
else()
    set(platform x86)
endif()
link_directories("XXX/pthreads-w32-2-9-1-release/Pre-built.2/lib/${platform}")
#如果出现 “timespec”:struct” 类型重定义 设置
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DHAVE_STRUCT_TIMESPEC")
# 将源添加到此项目的可执行文件。
add_executable (lsn6example "lsn6_example.cpp" "lsn6_example.h")
target_link_libraries(lsn6example pthreadVC2)

二、Pthreads重要的函数

函数名称 参数(从左到右) 返回值 说明
int pthread_create(pthread_t* thread, pthread_attr_t const* attr, void* (__start_routine)(void), void*) 传入用于保存指向pthread_t的long型指针,该方法执行完毕之后就会自动把对应的地址赋到这个参数里;线程属性结构体用于指定线程属性,一般传0即可;定义Native 线程要执行任务逻辑的函数指针,其中函数指针变量即为传递的参数,相当于是Java的run方法;传入函数指针的实参 返回一个整形值,用于标识是否执行成功,按照C/C++的逻辑,一般是大于等于0为成功,-1或者负数为失败 创建一个native线程
int pthread_join(pthread_t __pthread, void** __return_value_ptr) __pthread为当前线程等待执行完毕的线程Id,传入0即可 同上 阻塞当前的线程,直到另外一个线程id为__pthread运行结束
int pthread_mutex_init(pthread_mutex_t* __mutex, const pthread_mutexattr_t* __attr) 使用pthread_mutex_t lock定义互斥锁之后,需要传入到这个方法中进行初始化;一般可以传NULL或者0 同上 使用初始化互斥锁
int pthread_mutex_destroy(pthread_mutex_t *mutex) 要删除的互斥锁 同上 为了避免内存泄漏,不再使用的时候应该删除互斥锁,一般与init 配套使用
int pthread_mutex_lock(pthread_mutex_t *mutex) 互斥锁变量 同上 占有互斥锁(阻塞操作),执行这句代码之后,后面的代码都会被阻塞直到调用了unlock
int pthread_mutex_trylock(pthread_mutex_t *mutex) 互斥锁变量 同上 试图占有互斥锁(不阻塞操作)。即当互斥锁空闲时,将占有该锁;否则,立即返回。
pthread_mutex_unlock(pthread_mutex_t *mutex) 互斥锁变量 同上 释放互斥锁
int pthread_cond_init(pthread_cond_t* __cond, const pthread_condattr_t* __attr) pthread_cond_t condi定义条件变量之后,需要传入到这个方法中进行初始化;一般可以传NULL或者0 同上 初始化条件变量
int pthread_cond_destroy(pthread_cond_t* __cond) 要销毁的条件变量 同上 销毁条件变量
int pthread_cond_signal(pthread_cond_t* __cond) 同上 唤醒第一个调用pthread_cond_wait()而进入睡眠的线程
int pthread_cond_wait(pthread_cond_t* __cond, pthread_mutex_t* __mutex) 同上 等待条件变量的特殊条件发生
int pthread_attr_setdetachstate(pthread_attr_t* __attr, int __state) 线程属性变量;传入PTHREAD_CREATE_DETACHED 分离或PTHREAD_CREATE_JOINABLE 非分离 同上 线程创建默认是非分离的,当pthread_join()函数返回时,创建的线程终止,释放自己占用的系统资源分离线程不能被其他线程等待,pthread_join无效,线程自己玩自己的。
int pthread_cancel() 同上 中断另外一个线程的运行
int pthread_attr_init() 同上 初始化线程的属性,线程是具有属性,用 pthread_attr_t 表示,可以设置是否分离、调度优先级等属性
int pthread_attr_setschedpolicy(pthread_attr_t* __attr, int __policy) 线程属性变量;传入SCHED_FIFO 实时调度策略,先到先服务 一旦占用cpu则一直运行。一直运行直到有更高优先级任务到达或自己放弃。或者SCHED_RR实时调度策略,时间轮转 系统分配一个时间段,在时间段内执行本线程 同上 配置线程的调度策略
int pthread_attr_setschedparam(pthread_attr_t* __attr, const struct sched_param* __param) 设置线程优先级,获得对应策略的最小、最大优先级,空通过sched_get_priority_max(SCHED_FIFO)获取最大优先级和sched_get_priority_min(SCHED_FIFO);获取最小优先并复制给sched_param param复制,如param.sched_priority = max最后在传到pthread_attr_setschedparam函数中
int pthread_attr_destroy(pthread_attr_t* __attr) 同上 删除线程的属性
int pthread_detach(pthread_t __pthread) 同上 分离
int pthread_exit(void* __return_value) 同上 终止当前线程

三、创建Native 线程

  • 引入pthread.h头文件支持
  • 定义一个“线程Id”变量:这是个long整形变量,为什么要加上双引号呢,因为它这个所谓的Id有点特别,实际上在Linux系统源码中是通过一个数组来管理线程的,而这个所谓的线程Id实际上是对应数组的下标,这也是为什么一个long型变量就可以代表一个线程(实际的线程对象本身是极其复杂的,暂且这样简单理解,更深层次的不在此文章讨论之列)
  • 声明并实现用于在线程内执行任务的指向函数的指针,相当于是实现了Java线程中的run方法。
  • 调用pthread_create函数创建并运行Native线程
#include <jni.h>
#include <pthread.h>

//定义指向函数的指针
void *pthreadTask(void* args) {
    int* num = static_cast<int*>(args);
    LOGE("在Native线程中执行num=%d",*num);
    return 0;
}

extern "C"
JNIEXPORT void JNICALL
Java_com_crazymo_ndk_jni_JNIHelper_createNativeThread(JNIEnv *env, jobject instance) {
    pthread_t pid;
    int num=2008;
    pthread_create(&pid, 0, pthreadTask, &num);//创建并启动Native线程
}

//本地Java接口方法
public native void createNativeThread();

这里写图片描述

四、Native线程的同步

多线程在访问同一共享资源的时候有可能造成冲突,从而导致线程安全的问题。

1、互斥锁的应用

  • 定义互斥锁变量
  • 初始化互斥锁,可以通过pthread_mutex_init函数进行初始化,也可以直接使用PTHREAD_MUTEX_INITIALIZER进行赋值,通过后面这种形式就不能改变对应的属性
  • 调用 pthread_mutex_lock 函数占有锁(并非只能调用这个函数,下同)
  • 调用pthread_mutex_unlock函数释放锁
  • 调用pthread_mutex_destroy销毁锁
#include <pthread.h>
#include <android/log.h>
#include <queue>
#include <jni.h>

using namespace std;

#define  LOGE(...) __android_log_print(ANDROID_LOG_ERROR,"CrazyMoJNI",__VA_ARGS__);

queue<int> q;//多个线程共享的资源
static pthread_mutex_t mutex;//定义互斥锁
void *pop(void* args) {
    //若线程未同步导致的多线程安全问题可能会有重复的数据取出并出现异常
    pthread_mutex_lock(&mutex); // 申请占有锁 类似Java中的sychronized 属于悲观锁是将pthread_mutex_lock与pthread_mutex_unlock进行保护
    if (!q.empty())
    {
        LOGE("子线程取出数据:%d\n", q.front());
        q.pop();
    }
    else {
        LOGE("无数据\n");
    }
    // 释放锁
    pthread_mutex_unlock(&mutex);
    return 0;
}

extern "C"
JNIEXPORT jint JNICALL
Java_com_crazymo_ndk_jni_JNIHelper_testUnSync(JNIEnv *env, jobject instance)
{
    //初始化互斥锁,这只是一种形式
    pthread_mutex_init(&mutex, 0);
    for (size_t i = 0; i < 5; i++)
    {
        q.push(i);
    }
    pthread_t pid[10];
    for (size_t i = 0; i < 10; i++)
    {
        pthread_create(&pid[i], 0, pop, &q);//创建Native线程
    }
    //需要释放
    for (size_t i = 0; i < 10; i++)
    {
        pthread_join(pid[i], 0);
    }
    pthread_mutex_destroy(&mutex);
    return 0;
}

这里写图片描述

2、锁与信号量结合使用实现

条件变量condition线程间进行同步的一种机制,相较于while(true){sleep()}消耗资源更少,主要包括两个动作:一个线程等待"条件变量的条件成立"而挂起(即哪个线程调用了pthread_cond_wait函数则哪个线程被挂起);另一个线程使"条件成立",从而唤醒挂起线程。(即通过pthread_cond_signal唤醒被挂起的线程),条件变量的使用步骤:

  • 定义条件变量并初始化
  • 任一线程内调用pthread_cond_wait函数,则对应的线程则被挂起,直到相应条件的被唤醒
  • 在任一线程内调用pthread_cond_signal函数发出唤醒信号,唤醒第一个调用pthread_cond_wait而被挂起的线程(如果多个线程都使用了相同的条件变量的话)也可以通过pthread_cond_broadcast函数广播发出信号,唤醒所有使用了对应条件变量的线程
  • 调用pthread_cond_destory函数销毁条件变量

接下来结合锁和信号量来实现一个线程安全的生产-消费者模型,使用模板类存放数据T,并使用互斥锁保证对queue的操作是线程安全的。如果在取出数据的时候,queue为空,则一直等待,直到下一次enqueue加入数据,再加入条件变量使 “dequeue” 挂起,直到由其他地方唤醒

单独使用互斥锁对队列的操作进行同步如下:

#pragma once
#include <queue>
using namespace std;
template <class T>
class SafeQueue {
public:
	SafeQueue() {
		pthread_mutex_init(&mutex,0);//在构造函数中初始化互斥锁
	}
	~SafeQueue() {
		pthread_mutex_destory(&mutex);//在析构函数中销毁互斥锁
	}
	void enqueue(T t) {
		pthread_mutex_lock(&mutex);
		q.push(t);
		pthread_mutex_unlock(&mutex);
	}
	int dequeue(T& t) {
		pthread_mutex_lock(&mutex);
		if (!q.empty())
		{
			t = q.front();
			q.pop();
			pthread_mutex_unlock(&mutex);
			return 1;
		}
		pthread_mutex_unlock(&mutex);
		return 0;
	}

private:
	queue<T> q;//声明用于存放数据的队列
	pthread_mutex_t mutex;//声明互斥锁用于确保确保操作队列时线程安全
};

注意到上述出队函数dequeue中,当队列为空的时候就啥事也没有做也没有取到任何数据,通常这在生产-消费者模型中是不太合理的,应该确保每一次出队操作都能拿到有效数据,即若queue为空,则对应的消费者线程应该一直挂起直到下一次入队操作完成,这就需要结合信号量来实现了

#pragma once
#include <queue>
using namespace std;

template <class T>
class SafeQueue {
public:
	SafeQueue() {
		pthread_mutex_init(&mutex,0);//初始化互斥锁
		pthread_cond_init(&cond, 0);//初始化信号量
	}
	~SafeQueue() {
		pthread_mutex_destory(&mutex);
		pthread_cond_destory(&cond);
	}
	void enqueue(T t) {
		/**Java对应的写法
		* synchronized(mutex){notifyall()}
		*/
		pthread_mutex_lock(&mutex);
		q.push(t);
		//pthread_cond_signal(&cond);//发出信号,由系统通知挂起的线程唤醒,无法控制具体是哪一条线程被唤醒
		pthread_cond_broadcast(&cond);// 广播 对应多个消费者的时候 多个线程等待唤醒所有
		pthread_mutex_unlock(&mutex);
	}
	int dequeue(T& t) {
		/**Java对应的写法
		* synchronized(mutex){wait()}
		*/
		pthread_mutex_lock(&mutex);
		//因为可能会被意外唤醒 所以while循环
		while (q.empty())
		{
			pthread_cond_wait(&cond, &mutex);//如果队列无数据则一直阻塞调用出队操作的消费者线程
		}
		t = q.front();
		q.pop();
		pthread_mutex_unlock(&mutex);
		return 1;
	}

private:
	queue<T> q;
	pthread_mutex_t mutex;
	pthread_cond_t cond;
};

简单使用

#include <thread>
#include <pthread.h>

using namespace std;
#include "safe_queue.h"

SafeQueue<int> q;

void *get(void* args) {
	while (1) {
		int i;
		q.dequeue(i);
		cout << "消费:"<< i << endl;
	}
	return 0;
}
void *put(void* args) {
	while (1)
	{
		int i;
		cin >> i;
		q.enqueue(i);
	}
	return 0;
}
int main()
{
	pthread_t pid1, pid2;
	pthread_create(&pid1, 0, get, &q);
	pthread_create(&pid2, 0, put, &q);
	pthread_join(pid2,0);
	system("pause");
	return 0;
}

在这里插入图片描述
未完待续……

猜你喜欢

转载自blog.csdn.net/CrazyMo_/article/details/82498581