x265-1.7版本-common/threadpool.cpp注释

本文深入探讨x265编码器在多线程环境下的并发处理机制,详细解释了核心组件如WorkerThread、ThreadPool、JobProvider的工作流程与交互。着重介绍了线程间协作、资源分配、任务调度等关键概念,以及如何通过系统调用来优化线程亲缘性,提升编码效率。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

/*****************************************************************************
 * Copyright (C) 2013 x265 project
 *
 * Authors: Steve Borho <steve@borho.org>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02111, USA.
 *
 * This program is also available under a commercial proprietary license.
 * For more information, contact us at license @ x265.com
 *****************************************************************************/

#include "common.h"
#include "threadpool.h"
#include "threading.h"

#include <new>

#if X86_64

#ifdef __GNUC__

#define SLEEPBITMAP_CTZ(id, x)     id = (unsigned long)__builtin_ctzll(x)
#define SLEEPBITMAP_OR(ptr, mask)  __sync_fetch_and_or(ptr, mask)
#define SLEEPBITMAP_AND(ptr, mask) __sync_fetch_and_and(ptr, mask)

#elif defined(_MSC_VER)

#define SLEEPBITMAP_CTZ(id, x)     _BitScanForward64(&id, x)
#define SLEEPBITMAP_OR(ptr, mask)  InterlockedOr64((volatile LONG64*)ptr, (LONG)mask)
#define SLEEPBITMAP_AND(ptr, mask) InterlockedAnd64((volatile LONG64*)ptr, (LONG)mask)

#endif // ifdef __GNUC__

#else

/* use 32-bit primitives defined in threading.h */
#define SLEEPBITMAP_CTZ CTZ
#define SLEEPBITMAP_OR  ATOMIC_OR
#define SLEEPBITMAP_AND ATOMIC_AND

#endif

#if MACOS
#include <sys/param.h>
#include <sys/sysctl.h>
#endif
#if HAVE_LIBNUMA
#include <numa.h>
#endif

namespace x265 {
// x265 private namespace

class WorkerThread : public Thread
{
private:

    ThreadPool&  m_pool;
    int          m_id;
    Event        m_wakeEvent;

    WorkerThread& operator =(const WorkerThread&);

public:

    JobProvider*     m_curJobProvider;
    BondedTaskGroup* m_bondMaster;

    WorkerThread(ThreadPool& pool, int id) : m_pool(pool), m_id(id) {}
    virtual ~WorkerThread() {}

    void threadMain();
    void awaken()           { m_wakeEvent.trigger(); }
};
/** 函数功能             :??在tryWakeOne中返回当前机器任意一个正在睡眠的核,tryBondPeers返回当前线程拥有核且正在睡眠的核
/*  调用范围             :??只在 JobProvider::tryWakeOne()和ThreadPool::tryBondPeers函数中被调用
* \参数 firstTryBitmap   :??为Job的m_ownerBitmap (如4核机器:一般传入是-1~15的数据)或者直接指定为-1
* \参数 secondTryBitmap  :只有0 和 -1 两个值能够传进来,tryBondPeers为0 tryWakeOne() 为-1(全为1,mask值)
* \返回                  :??在tryWakeOne中返回当前机器任意一个正在睡眠的核,tryBondPeers返回当前线程拥有核且正在睡眠的核 * */
void WorkerThread::threadMain()
{
    //此函数(有几个核,进入几次)
    THREAD_NAME("Worker", m_id);

#if _WIN32
    SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_BELOW_NORMAL);//设置线程优先级
#else
    __attribute__((unused)) int val = nice(10);
#endif

    m_pool.setCurrentThreadAffinity();  //设置线程间能够在不同的核运行,而不会同时占用同一个核

    sleepbitmap_t idBit = (sleepbitmap_t)1 << m_id; //内核编号:如id(0,1,2,3) 对应(1,10,100,1000)
    m_curJobProvider = m_pool.m_jpTable[0];        //初始化当前任务,选择第一个FrameEncoder[0]
    m_bondMaster = NULL;

    SLEEPBITMAP_OR(&m_curJobProvider->m_ownerBitmap, idBit);//更新当前job拥有的内核号
    SLEEPBITMAP_OR(&m_pool.m_sleepBitmap, idBit);           //更新sleepmap
    m_wakeEvent.wait();     
    /* 等待触发,只有触发后才进行下面语句(虽然m_curJobProvide一开始对应的是FrameEncoder[0],但是lookachead会先触发) 
       用于标记当前是否触发,触发只在两个函数中会触发:tryWakeOne()和ThreadPool::tryBondPeers
    **/

    while (m_pool.m_isActive)
    {
        if (m_bondMaster)
        {
            m_bondMaster->processTasks(m_id);       //完成相应任务PMODE、WeightAnalysis、PME、CostEstimateGroup、PreLookaheadGroup
            m_bondMaster->m_exitedPeerCount.incr(); //完成一个任务加1
            m_bondMaster = NULL;                    //当前任务被释放
        }

        do
        {
            /* do pending work for current job provider */
            m_curJobProvider->findJob(m_id);       // 运行子类 Lookachead:运行slicetypeDecide() ; Wpp: ???

            /* if the current job provider still wants help, only switch to a
             * higher priority provider (lower slice type). Else take the first
             * available job provider with the highest priority */
            int curPriority = (m_curJobProvider->m_helpWanted) ? m_curJobProvider->m_sliceType :
                                                                 INVALID_SLICE_PRIORITY + 1;
            int nextProvider = -1;
            for (int i = 0; i < m_pool.m_numProviders; i++)
            {
                if (m_pool.m_jpTable[i]->m_helpWanted &&
                    m_pool.m_jpTable[i]->m_sliceType < curPriority)
                {
                    nextProvider = i;
                    curPriority = m_pool.m_jpTable[i]->m_sliceType;
                }
            }
            if (nextProvider != -1 && m_curJobProvider != m_pool.m_jpTable[nextProvider])
            {
                SLEEPBITMAP_AND(&m_curJobProvider->m_ownerBitmap, ~idBit);
                m_curJobProvider = m_pool.m_jpTable[nextProvider];
                SLEEPBITMAP_OR(&m_curJobProvider->m_ownerBitmap, idBit);
            }
        }
        while (m_curJobProvider->m_helpWanted);

        /* While the worker sleeps, a job-provider or bond-group may acquire this
         * worker's sleep bitmap bit. Once acquired, that thread may modify 
         * m_bondMaster or m_curJobProvider, then waken the thread */
        SLEEPBITMAP_OR(&m_pool.m_sleepBitmap, idBit);
        m_wakeEvent.wait();
    }

    SLEEPBITMAP_OR(&m_pool.m_sleepBitmap, idBit);
}
/** 函数功能             :Lookahead::addPicture 中: 在获取帧数大于m_fullQueueSize才会进入   获得当前可用核,并触发线程,更新ownermap
                           FrameEncoder::compressFrame()中:
                           FrameEncoder::processRowEncoder中:??在tryWakeOne中返回当前机器任意一个正在睡眠的核,tryBondPeers返回当前线程拥有核且正在睡眠的核
/*  调用范围             :只在 Lookahead::addPicture和FrameEncoder::compressFrame()、FrameEncoder::processRowEncoder 函数中被调用
* \返回                  :null * */
void JobProvider::tryWakeOne()
{
    int id = m_pool->tryAcquireSleepingThread(m_ownerBitmap, ALL_POOL_THREADS);//找到一个当前正在sleep的核即可
    if (id < 0)
    {
        m_helpWanted = true;//如果没有可用的核,置true并返回(需要核)
        return;
    }

    WorkerThread& worker = m_pool->m_workers[id];//获取当前job
    if (worker.m_curJobProvider != this) /* poaching */
    {
        sleepbitmap_t bit = (sleepbitmap_t)1 << id;//获取当前id占用的map,相应位置为1
        SLEEPBITMAP_AND(&worker.m_curJobProvider->m_ownerBitmap, ~bit);//当前job不该拥有不属于自己的核,所以去除当前核
        worker.m_curJobProvider = this;// 更新当前job指针
        SLEEPBITMAP_OR(&worker.m_curJobProvider->m_ownerBitmap, bit);//真正的job获取当前的核更新m_ownerBitmap
    }
    worker.awaken();//触发当前work
}
/** 函数功能             :在tryWakeOne中返回当前机器任意一个正在睡眠的核,tryBondPeers返回当前线程拥有核且正在睡眠的核
/*  调用范围             :只在 JobProvider::tryWakeOne()和ThreadPool::tryBondPeers函数中被调用
* \参数 firstTryBitmap   :为Job的m_ownerBitmap (如4核机器:一般传入是-1~15的数据)或者直接指定为-1
* \参数 secondTryBitmap  :只有0 和 -1 两个值能够传进来,tryBondPeers为0 tryWakeOne() 为-1(全为1,mask值)
* \返回                  :在tryWakeOne中返回当前机器任意一个正在睡眠的核,tryBondPeers返回当前线程拥有核且正在睡眠的核 * */
int ThreadPool::tryAcquireSleepingThread(sleepbitmap_t firstTryBitmap, sleepbitmap_t secondTryBitmap)
{
    unsigned long id;

    sleepbitmap_t masked = m_sleepBitmap & firstTryBitmap;//表示当前拥有并且sleep的核数
    while (masked)
    {
        SLEEPBITMAP_CTZ(id, masked);//从低位开始返回第一个为1的位置
        //如: mask=1  id = 0
        //     Mask= 2 10 id =1
        //    Mask = 4 100 id = 2
        //   这里id刚好就是第几个核0,1,2,3
        sleepbitmap_t bit = (sleepbitmap_t)1 << id;//获取当前位置的map,其它位置为0
        //如当前:mask 为15 1111, id = 0,bit = 1<<id = 1 ~bit = 1110 m_sleepBitmap = 1111
        //经过下列语句,m_sleepBitmap = 1110 SLEEPBITMAP_AND(&m_sleepBitmap, ~bit) 返回m_sleepBitmap原来的数据 更新m_sleepBitmap = (m_sleepBitmap& ~bit)
        //If语句保证当前核是sleep状态   if的and语句是加锁状态的语句,防止当前m_sleepBitmap已经被修改
        if (SLEEPBITMAP_AND(&m_sleepBitmap, ~bit) & bit)// m_sleepBitmap剔除当前位置为1的地方,改为0 可能进入else
            return (int)id;//返回当前可用id

        masked = m_sleepBitmap & firstTryBitmap;//更新masked(防止期间值已经被修改)
    }

    masked = m_sleepBitmap & secondTryBitmap;//在tryBondPeers中secondTryBitmap为0,mask一定为0,在tryWakeOne()中mask一定为15,全部为1
    while (masked)//只会在tryWakeOne()中进入
    {
        SLEEPBITMAP_CTZ(id, masked);// 获取mask中第一个id

        sleepbitmap_t bit = (sleepbitmap_t)1 << id;//获取当前位置的map,其它位置为0
        if (SLEEPBITMAP_AND(&m_sleepBitmap, ~bit) & bit)//一般不进入else
            return (int)id;

        masked = m_sleepBitmap & secondTryBitmap;//更新masked状态,很少运行下面语句
    }

    return -1;
}

/** 函数功能             : 返回当前可用核数,并在threadmain中触发相应processtask
/*  调用范围             : 只在slicetypeDecide、CostEstimateGroup::finishBatch()、CostEstimateGroup::estimateFrameCost、predInterSearch、compressFrame()和compressInterCU_dist函数中被调用
* \返回                  : 返回当前可用核数 * */
int ThreadPool::tryBondPeers(int maxPeers, sleepbitmap_t peerBitmap, BondedTaskGroup& master)
{
    /*
    在slicetypeDecide函数中PreLookaheadGroup会调用                     : maxPeers为需要初始化的帧数,peerBitmap为-1,master为相应子类PreLookaheadGroup
    在CostEstimateGroup::finishBatch()函数中CostEstimateGroup会调用    :maxPeers为需要计算frame-cost的帧数,peerBitmap为-1,master为相应子类CostEstimateGroup
    在CostEstimateGroup::estimateFrameCost函数中CostEstimateGroup会调用:maxPeers为当前帧需要执行的slice个数,peerBitmap为-1,master为相应子类CostEstimateGroup
    在predInterSearch函数中PME会调用
    在compressFrame()函数中WeightAnalysis会调用                        :maxPeers为1,peerBitmap为当前任务拥有的核,master为相应子类WeightAnalysis
    在compressInterCU_dist函数中pmode会调用
    **/
    int bondCount = 0;  //初始化计数器
    do
    {
        int id = tryAcquireSleepingThread(peerBitmap, 0); 
        //如果是PreLookaheadGroup、CostEstimateGroup则会只要找到一个当前正在sleep的核即可
        //如果是WeightAnalysis、   ??则会返回当前任务拥有核且正在睡眠的核
        if (id < 0)
            return bondCount; //当前没有可用核直接返回

        m_workers[id].m_bondMaster = &master; //此时m_bondMaster不再为null在threadmain中可以运行了
        m_workers[id].awaken();               //触发当前核的线程
        bondCount++;
    }
    while (bondCount < maxPeers);            

    return bondCount;    //返回可用的核数
}

ThreadPool* ThreadPool::allocThreadPools(x265_param* p, int& numPools)
{
    enum { MAX_NODE_NUM = 127 };
    int cpusPerNode[MAX_NODE_NUM + 1];

    memset(cpusPerNode, 0, sizeof(cpusPerNode));
    int numNumaNodes = X265_MIN(getNumaNodeCount(), MAX_NODE_NUM);
    int cpuCount = getCpuCount();
    bool bNumaSupport = false;

#if defined(_WIN32_WINNT) && _WIN32_WINNT >= _WIN32_WINNT_WIN7 
    bNumaSupport = true;
#elif HAVE_LIBNUMA
    bNumaSupport = numa_available() >= 0;
#endif


    for (int i = 0; i < cpuCount; i++)
    {
#if defined(_WIN32_WINNT) && _WIN32_WINNT >= _WIN32_WINNT_WIN7 
        UCHAR node;
        if (GetNumaProcessorNode((UCHAR)i, &node))
            cpusPerNode[X265_MIN(node, (UCHAR)MAX_NODE_NUM)]++;
        else
#elif HAVE_LIBNUMA
        if (bNumaSupport >= 0)
            cpusPerNode[X265_MIN(numa_node_of_cpu(i), MAX_NODE_NUM)]++;
        else
#endif
            cpusPerNode[0]++;
    }

    if (bNumaSupport && p->logLevel >= X265_LOG_DEBUG)
        for (int i = 0; i < numNumaNodes; i++)
            x265_log(p, X265_LOG_DEBUG, "detected NUMA node %d with %d logical cores\n", i, cpusPerNode[i]);

    /* limit nodes based on param->numaPools */
    if (p->numaPools && *p->numaPools)
    {
        const char *nodeStr = p->numaPools;
        for (int i = 0; i < numNumaNodes; i++)
        {
            if (!*nodeStr)
            {
                cpusPerNode[i] = 0;
                continue;
            }
            else if (*nodeStr == '-')
                cpusPerNode[i] = 0;
            else if (*nodeStr == '*')
                break;
            else if (*nodeStr == '+')
                ;
            else
            {
                int count = atoi(nodeStr);
                cpusPerNode[i] = X265_MIN(count, cpusPerNode[i]);
            }

            /* consume current node string, comma, and white-space */
            while (*nodeStr && *nodeStr != ',')
               ++nodeStr;
            if (*nodeStr == ',' || *nodeStr == ' ')
               ++nodeStr;
        }
    }

    numPools = 0;
    for (int i = 0; i < numNumaNodes; i++)
    {
        if (bNumaSupport)
            x265_log(p, X265_LOG_DEBUG, "NUMA node %d may use %d logical cores\n", i, cpusPerNode[i]);
        if (cpusPerNode[i])
            numPools += (cpusPerNode[i] + MAX_POOL_THREADS - 1) / MAX_POOL_THREADS;
    }

    if (!numPools)
        return NULL;

    if (numPools > p->frameNumThreads)
    {
        x265_log(p, X265_LOG_DEBUG, "Reducing number of thread pools for frame thread count\n");
        numPools = X265_MAX(p->frameNumThreads / 2, 1);
    }

    ThreadPool *pools = new ThreadPool[numPools];
    if (pools)
    {
        int maxProviders = (p->frameNumThreads + 1 + numPools - 1) / numPools; /* +1 is Lookahead */
        int node = 0;
        for (int i = 0; i < numPools; i++)
        {
            while (!cpusPerNode[node])
                node++;
            int cores = X265_MIN(MAX_POOL_THREADS, cpusPerNode[node]);
            if (!pools[i].create(cores, maxProviders, node))
            {
                X265_FREE(pools);
                numPools = 0;
                return NULL;
            }
            if (numNumaNodes > 1)
                x265_log(p, X265_LOG_INFO, "Thread pool %d using %d threads on NUMA node %d\n", i, cores, node);
            else
                x265_log(p, X265_LOG_INFO, "Thread pool created using %d threads\n", cores);
            cpusPerNode[node] -= cores;
        }
    }
    else
        numPools = 0;
    return pools;
}

ThreadPool::ThreadPool()
{
    memset(this, 0, sizeof(*this));
}

bool ThreadPool::create(int numThreads, int maxProviders, int node)
{
    X265_CHECK(numThreads <= MAX_POOL_THREADS, "a single thread pool cannot have more than MAX_POOL_THREADS threads\n");

    m_numaNode = node;
    m_numWorkers = numThreads;

    m_workers = X265_MALLOC(WorkerThread, numThreads);
    /* placement new initialization */
    if (m_workers)
        for (int i = 0; i < numThreads; i++)
            new (m_workers + i)WorkerThread(*this, i);

    m_jpTable = X265_MALLOC(JobProvider*, maxProviders);
    m_numProviders = 0;

    return m_workers && m_jpTable;
}

bool ThreadPool::start()
{
    m_isActive = true;
    for (int i = 0; i < m_numWorkers; i++)
    {
        if (!m_workers[i].start())
        {
            m_isActive = false;
            return false;
        }
    }
    return true;
}

void ThreadPool::stopWorkers()
{
    if (m_workers)
    {
        m_isActive = false;
        for (int i = 0; i < m_numWorkers; i++)
        {
            while (!(m_sleepBitmap & ((sleepbitmap_t)1 << i)))
                GIVE_UP_TIME();
            m_workers[i].awaken();
            m_workers[i].stop();
        }
    }
}

ThreadPool::~ThreadPool()
{
    if (m_workers)
    {
        for (int i = 0; i < m_numWorkers; i++)
            m_workers[i].~WorkerThread();
    }

    X265_FREE(m_workers);
    X265_FREE(m_jpTable);
}
/** 函数功能             :设置线程间能够在不同的核运行,而不会同时占用同一个核
/*  调用范围             :只在 WorkerThread::threadMain()和FrameEncoder::threadMain()函数中被调用
* \参数 numaNode         :表示当前处于node的位置,如果没有配置,则只有一个就是0
* \返回                  :null * */
void ThreadPool::setCurrentThreadAffinity()
{
    setThreadNodeAffinity(m_numaNode);
}

/* static */
/** 函数功能             :设置线程间能够在不同的核运行,而不会同时占用同一个核
/*  调用范围             :只在 ThreadPool::setCurrentThreadAffinity()函数中被调用
* \参数 numaNode         :表示当前处于node的位置,如果没有配置,则只有一个就是0
* \返回                  :null * */
void ThreadPool::setThreadNodeAffinity(int numaNode)
{
#if defined(_WIN32_WINNT) && _WIN32_WINNT >= _WIN32_WINNT_WIN7 
    GROUP_AFFINITY groupAffinity;//结构体{mask,group,reserve[3]}
    if (GetNumaNodeProcessorMaskEx((USHORT)numaNode, &groupAffinity))//获取数据在单机(4核机器)上测试全为{15,0,{0,0,0}}
    {
        if (SetThreadAffinityMask(GetCurrentThread(), (DWORD_PTR)groupAffinity.Mask))
            return;
        //通过调用SetThreadAffinityMask,就能为各个线程设置亲缘性屏蔽:
        //DWORD_PTR SetThreadAffinityMask(HANDLE hThread, DWORD_PTR dwThreadAffinityMask);  
        //该函数中的hThread参数用于指明要限制哪个线程, dwThreadAffinityMask用于指明该线程能够在哪个CPU上运行。dwThreadAffinityMask必须是进程的亲缘性屏蔽的相应子集。返回值是线程的前一个亲缘性屏蔽。
        //因此,若要将3个线程限制到CPU1、2和3上去运行,可以这样操作:
        //Thread 0 can only run on CPU 0.    
        //SetThreadAffinityMask(hThread0, 0x00000001); //第0位是1    
        //Threads 1, 2, 3 run on CPUs 1, 2, 3.//第1 2 3位是1    
        //SetThreadAffinityMask(hThread1, 0x00000002);    
        //SetThreadAffinityMask(hThread2, 0x00000003);    
        //SetThreadAffinityMask(hThread3, 0x00000004);
    }
    x265_log(NULL, X265_LOG_ERROR, "unable to set thread affinity to NUMA node %d\n", numaNode);
#elif HAVE_LIBNUMA
    if (numa_available() >= 0)
    {
        numa_run_on_node(numaNode);
        numa_set_preferred(numaNode);
        numa_set_localalloc();
        return;
    }
    x265_log(NULL, X265_LOG_ERROR, "unable to set thread affinity to NUMA node %d\n", numaNode);
#else
    (void)numaNode;
#endif
}

/* static */
int ThreadPool::getNumaNodeCount()
{
#if defined(_WIN32_WINNT) && _WIN32_WINNT >= _WIN32_WINNT_WIN7 
    ULONG num = 1;
    if (GetNumaHighestNodeNumber(&num))
        num++;
    return (int)num;
#elif HAVE_LIBNUMA
    if (numa_available() >= 0)
        return numa_max_node() + 1;
    else
        return 1;
#else
    return 1;
#endif
}

/* static */
int ThreadPool::getCpuCount()
{
#if _WIN32
    SYSTEM_INFO sysinfo;
    GetSystemInfo(&sysinfo);
    return sysinfo.dwNumberOfProcessors;
#elif __unix__
    return sysconf(_SC_NPROCESSORS_ONLN);
#elif MACOS
    int nm[2];
    size_t len = 4;
    uint32_t count;

    nm[0] = CTL_HW;
    nm[1] = HW_AVAILCPU;
    sysctl(nm, 2, &count, &len, NULL, 0);

    if (count < 1)
    {
        nm[1] = HW_NCPU;
        sysctl(nm, 2, &count, &len, NULL, 0);
        if (count < 1)
            count = 1;
    }

    return count;
#else
    return 2; // default to 2 threads, everywhere else
#endif
}

} // end namespace x265


注:问号以及未注释部分 会在x265-1.8版本内更新

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值