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 JNICALL
Java_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 = 0
10-17 14:11:07.506 15561-15659/com.johan.player E/player: Test C Thread : name = Thread2, i = 0
10-17 14:11:08.506 15561-15658/com.johan.player E/player: Test C Thread : name = Thread1, i = 1
10-17 14:11:08.506 15561-15659/com.johan.player E/player: Test C Thread : name = Thread2, i = 1
10-17 14:11:09.507 15561-15658/com.johan.player E/player: Test C Thread : name = Thread1, i = 2
10-17 14:11:09.507 15561-15659/com.johan.player E/player: Test C Thread : name = Thread2, i = 2
10-17 14:11:10.507 15561-15658/com.johan.player E/player: Test C Thread : name = Thread1, i = 3
10-17 14:11:10.507 15561-15659/com.johan.player E/player: Test C Thread : name = Thread2, i = 3
10-17 14:11:11.507 15561-15658/com.johan.player E/player: Test C Thread : name = Thread1, i = 4
10-17 14:11:11.508 15561-15659/com.johan.player E/player: Test C Thread : name = Thread2, i = 4
10-17 14:11:12.508 15561-15658/com.johan.player E/player: Test C Thread : name = Thread1, i = 5
10-17 14:11:12.508 15561-15659/com.johan.player E/player: Test C Thread : name = Thread2, i = 5
10-17 14:11:13.508 15561-15658/com.johan.player E/player: Test C Thread : name = Thread1, i = 6
10-17 14:11:13.509 15561-15659/com.johan.player E/player: Test C Thread : name = Thread2, i = 6
10-17 14:11:14.508 15561-15658/com.johan.player E/player: Test C Thread : name = Thread1, i = 7
10-17 14:11:14.509 15561-15659/com.johan.player E/player: Test C Thread : name = Thread2, i = 7
10-17 14:11:15.509 15561-15658/com.johan.player E/player: Test C Thread : name = Thread1, i = 8
10-17 14:11:15.509 15561-15659/com.johan.player E/player: Test C Thread : name = Thread2, i = 8
10-17 14:11:16.509 15561-15658/com.johan.player E/player: Test C Thread : name = Thread1, i = 9
10-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:属性,暂时为 NULL
pthread_mutex_init(pthread_mutex_t* __mutex, const pthread_mutexattr_t* __attr);
- pthread_mutex_destroy
// 销毁线程锁
// 参数1:线程锁 ID
pthread_mutex_destroy(pthread_mutex_t* __mutex);
- pthread_mutex_lock
// 加锁
// 参数1:线程锁 ID
pthread_mutex_lock(pthread_mutex_t* __mutex);
- pthread_mutex_unlock
// 解锁
// 参数1:线程锁 ID
pthread_mutex_unlock(pthread_mutex_t* __mutex);
当然还有其他函数,可以自己了解一下。
互斥锁一般会与条件变量一起使用,我们接下来看看条件变量。
条件变量
- pthread_cond_init
// 创建条件变量
// 参数1:条件变量 ID,类似于线程 ID
// 参数2:属性,暂时为 NULL
pthread_cond_init(pthread_cond_t* __cond, const pthread_condattr_t* __attr);
- pthread_cond_destroy
// 销毁条件变量
// 参数1:条件变量 ID
pthread_cond_destroy(pthread_cond_t* __cond);
- pthread_cond_wait
// 线程等待,并释放线程锁
// 参数1:条件变量 ID
// 参数2:线程锁 ID
pthread_cond_wait(pthread_cond_t* __cond, pthread_mutex_t* __mutex);
- pthread_cond_signal(pthread_cond_t* __cond);
// 通知线程
// 参数1:条件变量 ID
pthread_cond_signal(pthread_cond_t* __cond);
条件变量也还有其他函数,自己可以了解一下!
到这里,你会发现,其实和 Java 的 ReentrantLock 很相似!!!
生产者与消费者
为了方便理解互斥锁和条件变量的使用,我们使用这种机制来模拟生产者与消费者模式,直接上码。
先定义一个队列:
队列头文件 queue.h
#include <sys/types.h>
#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 <stdlib.h>
/**
* 初始化队列
* @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 JNICALL
Java_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 : 0
10-18 17:25:16.320 29058-29145/com.johan.player E/player: consumer2 consume element : 0
10-18 17:25:17.320 29058-29143/com.johan.player E/player: producer1 produce element : 1
10-18 17:25:17.321 29058-29145/com.johan.player E/player: consumer2 consume element : 1
10-18 17:25:18.321 29058-29143/com.johan.player E/player: producer1 produce element : 2
10-18 17:25:18.321 29058-29145/com.johan.player E/player: consumer2 consume element : 2
10-18 17:25:19.321 29058-29143/com.johan.player E/player: producer1 produce element : 3
10-18 17:25:19.321 29058-29144/com.johan.player E/player: consumer1 consume element : 3
10-18 17:25:20.322 29058-29143/com.johan.player E/player: producer1 produce element : 4
10-18 17:25:20.322 29058-29145/com.johan.player E/player: consumer2 consume element : 4
10-18 17:25:21.322 29058-29143/com.johan.player E/player: producer1 produce element : 5
10-18 17:25:21.322 29058-29145/com.johan.player E/player: consumer2 consume element : 5
10-18 17:25:22.322 29058-29143/com.johan.player E/player: producer1 produce element : 6
10-18 17:25:22.323 29058-29145/com.johan.player E/player: consumer2 consume element : 6
10-18 17:25:23.323 29058-29143/com.johan.player E/player: producer1 produce element : 7
10-18 17:25:23.323 29058-29145/com.johan.player E/player: consumer2 consume element : 7
10-18 17:25:24.323 29058-29143/com.johan.player E/player: producer1 produce element : 8
10-18 17:25:24.323 29058-29145/com.johan.player E/player: consumer2 consume element : 8
10-18 17:25:25.324 29058-29143/com.johan.player E/player: producer1 produce element : 9
10-18 17:25:25.324 29058-29145/com.johan.player E/player: consumer2 consume element : 9
10-18 17:25:25.324 29058-29144/com.johan.player E/player: consumer1 consume finish
10-18 17:25:26.324 29058-29143/com.johan.player E/player: producer1 produce finish
10-18 17:25:26.325 29058-29145/com.johan.player E/player: consumer2 consume finish
10-18 17:25:26.326 29058-29058/com.johan.player E/player: destroy ---
小结
发现自己对 C 很不熟悉,接下来可以加强一下 C 语言的了解!
这里还没有实现子线程播放音频,是因为想放到后面一起来实现!