該原創文章首發于微信公眾號:字節流動
為什么會用到錯幀同步?
一般 Android 系統相機的最高幀率在 30 FPS 左右,當幀率低于 20 FPS 時,用戶可以明顯感覺到相機畫面卡頓和延遲。
我們在做相機預覽和視頻流處理時,對每幀圖像處理時間過長(超過 30 ms)就很容易造成畫面卡頓,這個場景就需要用到錯幀同步方法去提升畫面的流暢度。
錯幀同步,簡單來說就是把當前的幾幀緩沖到子線程中處理,主線程直接返回子線程之前的處理結果,屬于典型的以空間換時間策略。
錯幀同步策略也有不足之處,它不能在子線程中緩沖太多的幀,否則造成畫面延遲。另外,每個子線程分配的任務也要均衡(即每幀在子線程中的處理時間大致相同),不然會因為 CPU 線程調度的時間消耗適得其反。
錯幀同步的原理如上圖所示,我們開啟三個線程:一個主線程,兩個工作線程,每一幀圖像的處理任務分為 2 步,第一個工作線程完成第一步處理,第二個工作線程完成第二步處理,每一幀都要經過這兩步的處理。
當主線程輸入第 n + 1 幀到第一個工作線程后,主線程會等待第二個工作線程中第 n 幀的處理結果然后返回,這種情況下你肯定會問第 0 幀怎么辦?第 0 幀就直接返回就行了。
這些步驟下來,可以看成第 n+1 幀和第 n 幀在 2 個工作線程中同時處理,若忽略 CPU 線程調度時間,2 線程錯幀可以提升一倍的性能(性能提升情況,下面會給出實測數據)。
錯幀同步的簡單實現
錯幀同步在實現上類似于“生產者-消費者”模式,我們借助于 C 語言信號量 #include <semaphore.h>
可以很方便的實現錯幀同步模型。
C 的信號量常用的幾個 API :
--------------------------------------------------------------------
int sem_init(sem_t *sem, int pshared, unsigned int value);
功能:初始化信號量
參數:
sem:指定要初始化的信號量
pshared:0:應用于多線程
非 0:多進程
value:指定了信號量的初始值
返回值:0 成功
-1 失敗
----------------------------------------------------------------------
int sem_destroy(sem_t *sem);
功能:銷毀信號量
參數:sem:指定要銷毀的信號量
返回值:0 成功
-1 錯誤
----------------------------------------------------------------------
int sem_post(sem_t *sem);
功能:信號量的值加 1 操作
參數:
sem:指定的信號量,就是這個信號量加 1
返回值:0 成功
-1 錯誤
-----------------------------------------------------------------------
int sem_wait(sem_t *sem);
功能:信號量的值減 1 , 如果信號量的值為 0 , 阻塞等待
參數:
sem:指定的信號量, 如果信號量的值為 0, 阻塞等待, 否則信號量的值減 1
返回值:0 成功
-1 錯誤
在這里為了簡化代碼邏輯,我們用字符串來表示視頻幀,每個工作線程對輸入的字符串進行標記,表示工作線程對視頻幀做了處理,最后的輸出(第 0 幀除外)都是經過工作線程標記過的字符串。
//初始化
void AsyncFramework::Init() {
LOGCATE("AsyncFramework::Init");
memset(work_buffers, 0, sizeof(work_buffers));
work_thread_running = true;
main_thread_running = true;
index = 0;
// 初始化 3 個信號量
sem_init(&main_sem, 0, 0);
sem_init(&first_thread_sem, 0, 0);
sem_init(&second_thread_sem, 0, 0);
// WORK_THREAD_NUM = 2 ,為 2 個工作線程申請 2 塊 buffer
for (int i = 0; i < WORK_THREAD_NUM; ++i) {
work_buffers[i] = static_cast<char *>(malloc(WORK_BUFFER_SIZE));
}
// 啟動三個線程
main_thread = new thread(MainThreadProcess);
first_thread = new thread(FirstStepProcess);
second_thread = new thread(SecondStepProcess);
}
// 反初始化
void AsyncFramework::UnInit() {
LOGCATE("AsyncFramework::UnInit");
//等待三個線程結束
main_thread_running = false;
main_thread->join();
delete main_thread;
main_thread = nullptr;
work_thread_running = false;
sem_post(&first_thread_sem);
sem_post(&second_thread_sem);
first_thread->join();
second_thread->join();
delete first_thread;
first_thread = nullptr;
delete second_thread;
second_thread = nullptr;
//銷毀信號量
sem_destroy(&main_sem);
sem_destroy(&first_thread_sem);
sem_destroy(&second_thread_sem);
//釋放緩沖區
for (int i = 0; i < WORK_THREAD_NUM; ++i) {
if (work_buffers[i]) {
free(work_buffers[i]);
work_buffers[i] = nullptr;
}
}
}
主線程的邏輯就是不斷地生成“視頻幀”,將“視頻幀”傳給第一個工作線程進行第一步處理,然后等待第二個工作線程的處理結果。
void AsyncFramework::MainThreadProcess() {
LOGCATE("AsyncFramework::MainThreadProcess start");
while (main_thread_running) {
memset(work_buffers[index % WORK_THREAD_NUM], 0, WORK_BUFFER_SIZE);
sprintf(work_buffers[index % WORK_THREAD_NUM], "FrameIndex=%d ", index);
//通知第一個工作線程處理
sem_post(&first_thread_sem);
if (index == 0) {
//第 0 幀直接返回,不交給工作線程處理
LOGCATE("AsyncFramework::MainThreadProcess %s", work_buffers[index % WORK_THREAD_NUM]);
index++;
continue;
} else {
//等待第二個工作線程的處理結果
sem_wait(&main_sem);
}
LOGCATE("AsyncFramework::MainThreadProcess %s", work_buffers[(index - 1) % WORK_THREAD_NUM]);
index++;
if (index == 100) break;//生成100幀
}
LOGCATE("AsyncFramework::MainThreadProcess end");
}
2個工作線程的處理邏輯類似,第一個工作線程收到主線程發來的信號,然后進行第一步處理,處理完成后通知第二個工作線程進行第二步處理,等到第二步處理完成后再通知主線程結束等待,取出處理結果。
void AsyncFramework::FirstStepProcess() {
LOGCATE("AsyncFramework::FirstStepProcess start");
int index = 0;
while (true) {
//等待主線程發來的信號
sem_wait(&first_thread_sem);
if(!work_thread_running) break;
LOGCATE("AsyncFramework::FirstStepProcess index=%d", index);
strcat(work_buffers[index % WORK_THREAD_NUM], "FirstStep ");
//休眠模擬處理耗時
this_thread::sleep_for(chrono::milliseconds(200));
//處理完成后通知第二個工作線程進行第二步處理
sem_post(&second_thread_sem);
index++;
}
LOGCATE("AsyncFramework::FirstStepProcess end");
}
void AsyncFramework::SecondStepProcess() {
LOGCATE("AsyncFramework::SecondStepProcess start");
int index = 0;
while (true) {
//等待第一個工作線程發來的信號
sem_wait(&second_thread_sem);
if(!work_thread_running) break;
LOGCATE("AsyncFramework::SecondStepProces index=%d", index);
strcat(work_buffers[index % WORK_THREAD_NUM], "SecondStep");
//休眠模擬處理耗時
this_thread::sleep_for(chrono::milliseconds(200));
//第二步處理完成后通知主線程結束等待
sem_post(&main_sem);
index++;
}
LOGCATE("AsyncFramework::SecondStepProcess end");
}
主線程打印的處理結果(第 0 幀直接返回,沒被處理):
我們設定視頻幀的 2 步處理一共耗時 400 ms (各休眠 200 ms),由于采用錯幀同步方式,主線程耗時只有 200 ms 左右,性能提升一倍。
聯系與交流
技術交流可以添加我的微信:Byte-Flow