X265中的线程模型

一. 线程和线程池

  1. X265中创建线程只有一个地方,Thread::start()函数(调用CreateThread),Thread::stop等待线程结束。因此,所有的线程都需要从Thread继承。
  2. 线程池:主要的成员就是1:工作线程数组,2:JobProvider数组。
  3. 线程池与JobProvider的对应关系是一对多

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,再唤醒它。

}

  • 其实际的任务由继承者设定,并在findJob中完成。只有两个类继承自JobProvider,Lookahead和WaveFront,说明Lookahead和WaveFront需要工作线程来完成其任务
  • 任务设定好后,JobProvider调用tryWakeOne函数唤醒其所在线程池中的一个睡眠线程来执行,如果所在线程池中没有睡眠的线程,则设置m_helpWanted,表示让其它执行中的线程在完成 任务后来执行此任务。如果找到了睡眠的线程,则将睡眠线程的JobProvider设为this,唤醒它
  • 每个JobProvider实际上在任一时刻只会拥有一个线程,每次唤醒一个线程来执行任务时,总是优先找它拥有的线程,原因在于同一线程连续执行同一每个JobProvider的任务,线程缓存可能会匹配更好。如果它拥有的线程再在干活(没有睡眠),则找线程池中的别的线程,如果找到,则并修改其m_ownerBitmap为新找到的线程。
  • 上述JobProvider任务分发过程说明一个JobProvider在分发任务时,可能上一个任务还没有完成,就又在分发下一个任务,因此,JobProvider的数据结构需要是多线程并发的,在 void findJob(int workerThreadId)时通过workerThreadId来判断是哪个线程在执行任务


 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个工作

  • 生成低分辨率图像
  • 如果使用了m_bAdaptiveQuant,计算tld.calcAdaptiveQuantFrame(preFrame, m_lookahead.m_param);
  • tld.lowresIntraEstimate(preFrame->m_lowres);

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取出,



你可能感兴趣的:(X265中的线程模型)