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 语言的了解!

这里还没有实现子线程播放音频,是因为想放到后面一起来实现!