Android(安卓)FFmpeg系列——3 C多线程使用
Android FFmpeg系列——0 编译.so库
Android FFmpeg系列——1 播放视频
Android FFmpeg系列——2 播放音频
Android FFmpeg系列——3 C多线程使用
Android FFmpeg系列——4 子线程播放音视频
Android FFmpeg系列——5 音视频同步播放
Android FFmpeg系列——6 Java 获取播放进度
Android FFmpeg系列——7 实现快进/快退功能
在 Android FFmpeg系列——2 播放音频 中,在主线程播放音频会导致ANR,虽然我们可以在 Java 层启动一个线程来播放,由于接下来我们要实现完整播放视频,需要在 C 层达到控制效果,所以我们还是在 C 层启动新线程来播放音频。
这一节,我们来学习 C 层多线程的使用。
pthread
pthread 是 C 语言实现多线程的库,我们要了解这个库的3个相关函数。
- pthread_create
// 创建线程// typedef long pthread_t;// 参数1:线程 ID,pthread_t* 其实就是 long 类型// 参数2:线程属性,目前置为 NULL,有兴趣可以自己了解一下// 参数3:线程要执行的函数,void* 类似就是 Java 中泛型或者 Object// 参数4:线程要执行函数的参数pthread_create(pthread_t* __pthread_ptr, pthread_attr_t const* __attr, void* (*__start_routine)(void*), void*);
- pthread_join
// 阻塞线程// 参数1:线程 ID// 参数2:变量指针,用来存储被等待线程的返回值// 这个函数是阻塞函数,等待线程函数执行完毕,获取返回值pthread_join(pthread_t __pthread, void** __return_value_ptr);
- pthread_exit
// 退出线程,一般在线程函数里面调用// 参数1:退出返回值pthread_exit(void* __return_value);
简单使用多线程
直接上码:
/** * 子线程执行函数 * 相当于 Java Runnable 的 run 函数 * @param arg * @return */void* run(void* arg) { char *name = (char*) arg; for (int i = 0; i < 10; i++) { LOGE("Test C Thread : name = %s, i = %d", name, i); sleep(1); } return NULL;}/** * 测试子线程 */extern "C"JNIEXPORT void JNICALLJava_com_johan_player_Player_testCThread(JNIEnv *env, jobject instance) { // 线程 ID pthread_t tid1, tid2; // 创建新线程并启动 pthread_create(&tid1, NULL, run, (void*) "Thread1"); pthread_create(&tid2, NULL, run, (void*) "Thread2"); // 阻塞线程 // pthread_join(tid1, NULL);}
我们启动了两个新线程,run 是线程执行方法,分别打印 0-9 数字,Thread1 和 Thread2 是我传入 run 方法的值,打印结果如下:
10-17 14:11:07.506 15561-15658/com.johan.player E/player: Test C Thread : name = Thread1, i = 010-17 14:11:07.506 15561-15659/com.johan.player E/player: Test C Thread : name = Thread2, i = 010-17 14:11:08.506 15561-15658/com.johan.player E/player: Test C Thread : name = Thread1, i = 110-17 14:11:08.506 15561-15659/com.johan.player E/player: Test C Thread : name = Thread2, i = 110-17 14:11:09.507 15561-15658/com.johan.player E/player: Test C Thread : name = Thread1, i = 210-17 14:11:09.507 15561-15659/com.johan.player E/player: Test C Thread : name = Thread2, i = 210-17 14:11:10.507 15561-15658/com.johan.player E/player: Test C Thread : name = Thread1, i = 310-17 14:11:10.507 15561-15659/com.johan.player E/player: Test C Thread : name = Thread2, i = 310-17 14:11:11.507 15561-15658/com.johan.player E/player: Test C Thread : name = Thread1, i = 410-17 14:11:11.508 15561-15659/com.johan.player E/player: Test C Thread : name = Thread2, i = 410-17 14:11:12.508 15561-15658/com.johan.player E/player: Test C Thread : name = Thread1, i = 510-17 14:11:12.508 15561-15659/com.johan.player E/player: Test C Thread : name = Thread2, i = 510-17 14:11:13.508 15561-15658/com.johan.player E/player: Test C Thread : name = Thread1, i = 610-17 14:11:13.509 15561-15659/com.johan.player E/player: Test C Thread : name = Thread2, i = 610-17 14:11:14.508 15561-15658/com.johan.player E/player: Test C Thread : name = Thread1, i = 710-17 14:11:14.509 15561-15659/com.johan.player E/player: Test C Thread : name = Thread2, i = 710-17 14:11:15.509 15561-15658/com.johan.player E/player: Test C Thread : name = Thread1, i = 810-17 14:11:15.509 15561-15659/com.johan.player E/player: Test C Thread : name = Thread2, i = 810-17 14:11:16.509 15561-15658/com.johan.player E/player: Test C Thread : name = Thread1, i = 910-17 14:11:16.510 15561-15659/com.johan.player E/player: Test C Thread : name = Thread2, i = 9
互斥锁
互斥锁也在 pthread 库中:
- pthread_mutex_init
// 创建互斥锁// 参数1:线程锁 ID,类似于线程 ID// 参数2:属性,暂时为 NULLpthread_mutex_init(pthread_mutex_t* __mutex, const pthread_mutexattr_t* __attr);
- pthread_mutex_destroy
// 销毁线程锁// 参数1:线程锁 IDpthread_mutex_destroy(pthread_mutex_t* __mutex);
- pthread_mutex_lock
// 加锁// 参数1:线程锁 IDpthread_mutex_lock(pthread_mutex_t* __mutex);
- pthread_mutex_unlock
// 解锁// 参数1:线程锁 IDpthread_mutex_unlock(pthread_mutex_t* __mutex);
当然还有其他函数,可以自己了解一下。
互斥锁一般会与条件变量一起使用,我们接下来看看条件变量。
条件变量
- pthread_cond_init
// 创建条件变量// 参数1:条件变量 ID,类似于线程 ID// 参数2:属性,暂时为 NULLpthread_cond_init(pthread_cond_t* __cond, const pthread_condattr_t* __attr);
- pthread_cond_destroy
// 销毁条件变量// 参数1:条件变量 IDpthread_cond_destroy(pthread_cond_t* __cond);
- pthread_cond_wait
// 线程等待,并释放线程锁// 参数1:条件变量 ID// 参数2:线程锁 IDpthread_cond_wait(pthread_cond_t* __cond, pthread_mutex_t* __mutex);
- pthread_cond_signal(pthread_cond_t* __cond);
// 通知线程// 参数1:条件变量 IDpthread_cond_signal(pthread_cond_t* __cond);
条件变量也还有其他函数,自己可以了解一下!
到这里,你会发现,其实和 Java 的 ReentrantLock 很相似!!!
生产者与消费者
为了方便理解互斥锁和条件变量的使用,我们使用这种机制来模拟生产者与消费者模式,直接上码。
先定义一个队列:
队列头文件 queue.h
#include #ifndef PLAYER_QUEUE_H#define PLAYER_QUEUE_H// 队列最大值#define QUEUE_MAX_SIZE 50// 节点数据类型typedef uint NodeElement;// 节点typedef struct _Node { // 数据 NodeElement data; // 下一个 struct _Node* next;} Node;// 队列typedef struct _Queue { // 大小 int size; // 队列头 Node* head; // 队列尾 Node* tail;} Queue;/** * 初始化队列 * @param queue */void queue_init(Queue* queue);/** * 销毁队列 * @param queue */void queue_destroy(Queue* queue);/** * 判断是否为空 * @param queue * @return */bool queue_is_empty(Queue* queue);/** * 判断是否已满 * @param queue * @return */bool queue_is_full(Queue* queue);/** * 入队 * @param queue * @param element * @param tid * @param cid */void queue_in(Queue* queue, NodeElement element);/** * 出队 (阻塞) * @param queue * @param tid * @param cid * @return */NodeElement queue_out(Queue* queue);#endif //PLAYER_QUEUE_H
队列实现 queue.cpp
#include "queue.h"#include /** * 初始化队列 * @param queue */void queue_init(Queue* queue) { queue->size = 0; queue->head = NULL; queue->tail = NULL;}/** * 销毁队列 * @param queue */void queue_destroy(Queue* queue) { Node* node = queue->head; while (node != NULL) { queue->head = queue->head->next; free(node); node = queue->head; } queue->head = NULL; queue->tail = NULL;}/** * 判断是否为空 * @param queue * @return */bool queue_is_empty(Queue* queue) { return queue->size == 0;}/** * 判断是否已满 * @param queue * @return */bool queue_is_full(Queue* queue) { return queue->size == QUEUE_MAX_SIZE;}/** * 入队 (阻塞) * @param queue * @param element */void queue_in(Queue* queue, NodeElement element) { if (queue->size >= QUEUE_MAX_SIZE){ return; } Node* newNode = (Node*) malloc(sizeof(Node)); newNode->data = element; newNode->next = NULL; if (queue->head == NULL) { queue->head = newNode; queue->tail = queue->head; } else { queue->tail->next = newNode; queue->tail = newNode; } queue->size += 1;}/** * 出队 (阻塞) * @param queue * @return */NodeElement queue_out(Queue* queue) { if (queue->size == 0 || queue->head == NULL) { return NULL; } Node* node = queue->head; NodeElement element = node->data; queue->head = queue->head->next; free(node); queue->size -= 1; return element;}
一些全局变量:
// 线程锁pthread_mutex_t mutex_id;// 条件变量pthread_cond_t produce_condition_id, consume_condition_id;// 队列Queue queue;// 生产数量#define PRODUCE_COUNT 10// 目前消费数量int consume_number = 0;
生产者函数:
/** * 生产者函数 * @param arg * @return */void* produce(void* arg) { char* name = (char*) arg; for (int i = 0; i < PRODUCE_COUNT; i++) { // 加锁 pthread_mutex_lock(&mutex_id); // 如果队列满了 等待并释放锁 // 这里为什么要用 while 呢 // 因为 C 的锁机制有 "惊扰" 现象 // 没有达到条件会触发 所以要循环判断 while (queue_is_full(&queue)) { pthread_cond_wait(&produce_condition_id, &mutex_id); } LOGE("%s produce element : %d", name, i); // 入队 queue_in(&queue, (NodeElement) i); // 通知消费者可以继续消费 pthread_cond_signal(&consume_condition_id); // 解锁 pthread_mutex_unlock(&mutex_id); // 模拟耗时 sleep(1); } LOGE("%s produce finish", name); return NULL;}
消费者函数:
/** * 消费函数 * @param arg * @return */void* consume(void* arg) { char* name = (char*) arg; while (1) { // 加锁 pthread_mutex_lock(&mutex_id); // 如果队列为空 等待 // 使用 while 的理由同上 while (queue_is_empty(&queue)) { // 如果消费到生产最大数量 不再等待 if (consume_number == PRODUCE_COUNT) { break; } pthread_cond_wait(&consume_condition_id, &mutex_id); } // 如果消费到生产最大数量 // 1.通知还在等待的线程 // 2.解锁 if (consume_number == PRODUCE_COUNT) { // 通知还在等待消费的线程 pthread_cond_signal(&consume_condition_id); // 解锁 pthread_mutex_unlock(&mutex_id); break; } // 出队 NodeElement element = queue_out(&queue); consume_number += 1; LOGE("%s consume element : %d", name, element); // 通知生产者可以继续生产 pthread_cond_signal(&produce_condition_id); // 解锁 pthread_mutex_unlock(&mutex_id); // 模拟耗时 sleep(1); } LOGE("%s consume finish", name); return NULL;}
多线程操作:
extern "C"JNIEXPORT void JNICALLJava_com_johan_player_Player_testCThread(JNIEnv *env, jobject instance) { // 创建队列 queue_init(&queue); // 线程 ID pthread_t tid1, tid2, tid3; // 创建线程锁 pthread_mutex_init(&mutex_id, NULL); // 创建条件变量 pthread_cond_init(&produce_condition_id, NULL); pthread_cond_init(&consume_condition_id, NULL); LOGE("init --- "); // 创建新线程并启动 // 1个生产线程 2个消费线程 pthread_create(&tid1, NULL, produce, (void*) "producer1"); pthread_create(&tid2, NULL, consume, (void*) "consumer1"); pthread_create(&tid3, NULL, consume, (void*) "consumer2"); // 阻塞线程 pthread_join(tid1, NULL); pthread_join(tid2, NULL); pthread_join(tid3, NULL); // 销毁条件变量 pthread_cond_destroy(&produce_condition_id); pthread_cond_destroy(&consume_condition_id); // 销毁线程锁 pthread_mutex_destroy(&mutex_id); // 销毁队列 queue_destroy(&queue); LOGE("destroy --- ");}
1个生产线程,2个消费线程,模拟生产者与消费者模式,代码已经注释得比较清楚,相信大家看得懂!
打印结果:
10-18 17:25:16.320 29058-29058/com.johan.player E/player: init --- 10-18 17:25:16.320 29058-29143/com.johan.player E/player: producer1 produce element : 010-18 17:25:16.320 29058-29145/com.johan.player E/player: consumer2 consume element : 010-18 17:25:17.320 29058-29143/com.johan.player E/player: producer1 produce element : 110-18 17:25:17.321 29058-29145/com.johan.player E/player: consumer2 consume element : 110-18 17:25:18.321 29058-29143/com.johan.player E/player: producer1 produce element : 210-18 17:25:18.321 29058-29145/com.johan.player E/player: consumer2 consume element : 210-18 17:25:19.321 29058-29143/com.johan.player E/player: producer1 produce element : 310-18 17:25:19.321 29058-29144/com.johan.player E/player: consumer1 consume element : 310-18 17:25:20.322 29058-29143/com.johan.player E/player: producer1 produce element : 410-18 17:25:20.322 29058-29145/com.johan.player E/player: consumer2 consume element : 410-18 17:25:21.322 29058-29143/com.johan.player E/player: producer1 produce element : 510-18 17:25:21.322 29058-29145/com.johan.player E/player: consumer2 consume element : 510-18 17:25:22.322 29058-29143/com.johan.player E/player: producer1 produce element : 610-18 17:25:22.323 29058-29145/com.johan.player E/player: consumer2 consume element : 610-18 17:25:23.323 29058-29143/com.johan.player E/player: producer1 produce element : 710-18 17:25:23.323 29058-29145/com.johan.player E/player: consumer2 consume element : 710-18 17:25:24.323 29058-29143/com.johan.player E/player: producer1 produce element : 810-18 17:25:24.323 29058-29145/com.johan.player E/player: consumer2 consume element : 810-18 17:25:25.324 29058-29143/com.johan.player E/player: producer1 produce element : 910-18 17:25:25.324 29058-29145/com.johan.player E/player: consumer2 consume element : 910-18 17:25:25.324 29058-29144/com.johan.player E/player: consumer1 consume finish10-18 17:25:26.324 29058-29143/com.johan.player E/player: producer1 produce finish10-18 17:25:26.325 29058-29145/com.johan.player E/player: consumer2 consume finish10-18 17:25:26.326 29058-29058/com.johan.player E/player: destroy ---
小结
发现自己对 C 很不熟悉,接下来可以加强一下 C 语言的了解!
这里还没有实现子线程播放音频,是因为想放到后面一起来实现!
更多相关文章
- SpringBoot 2.0 中 HikariCP 数据库连接池原理解析
- 【Android】java.lang.AssertionError use looper thread, must
- Andriod中的Handler机制
- Android(安卓)AsyncTask基础知识整理
- Android(安卓)Ril库总结
- Android解耦库EventBus的使用和源码分析
- Android(安卓)SurfaceFlinger服务启动过程源码分析1
- Android(安卓)NDK开发学习(四)
- android通过线程实现逐行显示信息