一. 线程和线程池
class ThreadPool
{
sleepbitmap_t m_sleepBitmap;
int m_numProviders;
int m_numWorkers;
void* m_numaMask; // node mask in linux, cpu mask in windows
#if defined(_WIN32_WINNT) && _WIN32_WINNT >= _WIN32_WINNT_WIN7
GROUP_AFFINITY m_groupAffinity;
#endif
bool m_isActive;
JobProvider** m_jpTable;
WorkerThread* m_workers;
}
二. 实际线程类
直接继承自Thread的类有两个WorkerThread,FrameEncoder
1. 工作线程
class WorkerThread : public Thread
{
ThreadPool& m_pool;
int m_id;此ID表明其在pool中的位置
Event m_wakeEvent;
JobProvider* m_curJobProvider;
BondedTaskGroup* m_bondMaster;
}
由于可见,工作线程是在线程的基础上记录了其本身是属于哪个线程池,并增加了线程函数中需要用到的事件,以及任务来源
1.1 工作线程的工作过程:
a) 初始化时设置自己的JobProvider,然后进行睡眠状态,等待唤醒;
b). 每次唤醒后,根据pool.avtice判断是否退出;
c) .如果不退出,依次做以下工作:
* BondedTaskGroup的工作
* 当前JobProvider的工作
* pool中的所有JobProvider中是否有需要帮助(m_helpWanted)的工作,并按优先级处理完,如有需要帮助的工作,会改变当前JobProvider。
由此可见,每次一个线程被唤醒后,会将线程池中任务队列中的任务按优先级全部做完才进行睡眠。
2. FrameEncoder
FrameEncoder自身有一个后台线程,其后台线程的任务应该是进行任务调用,将任务分发到工作线程,此外还会处理一些流程(猜想如此,实际还需要再细看代码)
三 任务类
X265中任务一共有两种:JobProvider和BondedTaskGroup
1 工作线程的任务提供者JobProvider
class JobProvider
{
ThreadPool* m_pool; 由此可见,一个JobProvider是对应到一个线程池的
sleepbitmap_t m_ownerBitmap;
int m_jpId;
int m_sliceType; 此标志为工作的优先级
bool m_helpWanted;此标志表示需要有工作线程来完成任务,进而调用findJob来处理任务,实际任务由派生类来实现,但findJob的实现需要在完成一个任务后判断是否还有其它任务要做,从而设置此标志
bool m_isFrameEncoder; /* rather ugly hack, but nothing better presents itself */
virtual void findJob(int workerThreadId) = 0;派生类需要实现此函数
void tryWakeOne();此函数会在线程池中查找一个处于睡眠状态的线程,优先找它拥有的线程,不行再找全部线程,设置其JobProvider为this,再唤醒它。
}
2 工作线程的联合任务组BondedTaskGroup
class BondedTaskGroup
{
Lock m_lock;
ThreadSafeInteger m_exitedPeerCount;
int m_bondedPeerCount;
int m_jobTotal;
int m_jobAcquired;
}
2.1 任务分配
BondedTaskGroup通过tryBondPeers在线程池中找到一个或多个睡眠线程来执行任务processTasks,线程池会一直找睡眠线程,直到找够需要的数目,因此,这里实现时应该注意,如果一次bond的线程数过多,没有足够的线程来执行,会导致tryBondPeers一直循环,等待其它工作线程进行睡眠,影响性能。
tryBondPeers将找到的多个线程的m_bondMaster设为当前相同的BondedTaskGroup,因此,通过processTasks(id)来执行任务,因此BondedTaskGroup的数据结构也应该是多线程并发的,且通过id来区分不同任务
2.2 任务执行
BondedTaskGroup通过派生类设定好需要多个线程并行执行的任务后,通过调用tryBondPeers来执行任务(此函数会导致线程池调用tryBondPeers来找到指定数目的睡眠线程并唤醒执行),然后调用waitForExit等待完成
3 BondedTaskGroup的派生类,即实际的需要联合执行的任务类PMODE,WeightAnalysis,ParallelFilter,PME,PreLookaheadGroup,CostEstimateGroup
派生类需要实现processTasks函数来实现具体任务
四 派生的BondedTaskGroup
1. PreLookaheadGroup 此任务仅在Lookahead::slicetypeDecide()函数中作为局部变量创建,slicetypeDecide函数是在findJob函数中被调用,因此,PreLookaheadGroup 实际是在工作线程进行slicetypeDecide时,再次将任务分解成联合任务,用多个工作线程来执行。
class PreLookaheadGroup : public BondedTaskGroup
{
Frame* m_preframes[X265_LOOKAHEAD_MAX];此处放的是待处理的帧,需要多个联合线程来生成低分辨率帧
Lookahead& m_lookahead;
}
PreLookaheadGroup 的processTasks函数仅在待分析的帧没有低分辨率时被调用,用于生成低分辨率帧。在联合任务执行时,除了被唤醒的多个线程以自身ID为标志处理processTasks函数时,自身所在线程也以-1为ID会调用processTasks函数执行任务。根据ID的不同,使用不同的LookaheadTLD
PreLookaheadGroup 在tryBondPeers(*m_pool, pre.m_jobTotal)时不一定就唤醒了m_jobTotal个工作线程,但不重要,多个线程将m_jobTotal个帧轮流处理完就行
PreLookaheadGroup::processTasks任务中主要做3个工作
2. 其它的派生类过程与PreLookaheadGroup 类似
五 派生的JobProvider
1. Lookahead
主要的数据成员
class Lookahead : public JobProvider
{
PicList m_inputQueue; // input pictures in order received,实际应为显示序
PicList m_outputQueue; // pictures to be encoded, in encode order ,实际应为解码序,即编码序,Lookahead对m_inputQueue中的帧分析,得到一个完整的GOP后放入m_outputQueue
Lock m_inputLock;
Lock m_outputLock;
Event m_outputSignal;
LookaheadTLD* m_tld; 每个Lookahead对应一个线程池,m_tld的维数为线程池中线程数+1,多出来的1个位置供调用线程使用
x265_param* m_param;
Lowres* m_lastNonB;
int* m_scratch;
}
Encoder在调用Lookahead::addPicture时,如果input队列满了,会tryWakeOne唤醒一个工作线程来做slicetypeDecide。
此任务类的工作是做slicetypeDecide,其中首先会通过PreLookaheadGroup 完成初始化工作,再调用slicetypeAnalyse来确定帧类型(在使用自适应B帧数目或场景检测时,帧类型不是固定的结构)
slicetypeAnalyse是核心函数,作用是从input队列中每次确定一个miniGOP的结构,并将确定后的帧按编码序放入output队列,供编码器通过getDecidedPicture来取。
2. WaveFront 虚类,被FrameEncoder继承,可把其任务理解为FrameEncoder分派出去的任务
class WaveFront : public JobProvider
{
uint32_t volatile *m_internalDependencyBitmap;
uint32_t volatile *m_externalDependencyBitmap;
int m_numWords;
int m_numRows;被初始化为2倍CTU行数,理由是 2 times of numRows because both Encoder and Filter in same queue,其中编码用偶数行,滤波用奇数行
}
注:WaveFront 由于有个纯虚函数processRow使得它不能直接使用,因此,实际被使用的类是FrameEncoder。
WaveFront 本身的工作是处理row的进出内外两个队列的关系,实际的任务处理每一行processRow是在findJob中通过工作线程来执行,每处理一行后都会先退出,再通过m_helpWanted表示还需要再处理又再进入。这样处理的理由是可以在处理两行之间转去处理更高优先级的任务
WaveFront 本身接口中并没有调用tryWakeOne,因此不会唤醒工作线程处理任务。因此只能是在FrameEncoder中来tryWakeOne
3. FrameEncoder
FrameEncoder是帧级并行编码类,在Encoder中根据m_param->frameNumThreads创建指定个数的FrameEncoder
在初始化后通过调用start开启后台线程,后台线程在初始化m_tld后进入循环,每次被唤醒后调用compressFrame编码一帧,直到m_threadActive变为false后退出(Encoder::stopJobs时会把所有FrameEncoder::m_threadActive改为false)
调用者通过调用FrameEncoder::startCompressFrame(Frame* curFrame)唤醒后台线程,让后台线程编码一帧;通过调用FrameEncoder::getEncodedPicture(NALList& output)等待当前帧编码结束
在FrameEncoder::compressFrame()中,会根据如果使用了WPP,则每行会通过 enableRowEncoder(row)准备好一行WPP,然后通过tryWakeOne唤醒一个工作线程来处理这行。否则(不使用WPP),直接调用processRowEncoder(i, m_tld[m_localTldIdx])处理一行,还需要m_frameFilter.processRow(i - m_filterRowDelay)滤波
由于FrameEncoder本身有线程,又有JobProvider,可以分析出,其自身的线程用作任务分解,设置,再将设置好的任务作为JobProvider去线程池中找工作线程来完成
Encoder中可能创建了多个线程池,多个FrameEncoder被轮流分配到不同的线程池
六 主编码器Encoder
class Encoder : public x265_encoder
x265_encoder仅是一个空结构体,用于转义成空指针给调用者。实际编码器是Encoder
Encoder 中与线程有关的成员:
ThreadPool* m_threadPool; //编码器中创建的线程池,供需要工作线程的任务使用
FrameEncoder* m_frameEncoder[X265_MAX_FRAME_THREADS]; //编码器创建几个帧并行编码单元,每个有自己独立的后台线程,等待编码分配过来的帧
Lookahead* m_lookahead; //任务成员,创建后不带线程,Encoder 通过其接口addPicture来驱动它唤醒工作线程来处理帧队列,生成待编码器由调用者通过getDecidedPicture取出,