并发任务的处理(一)

并发任务的处理

  1. 并发任务

目前主流操作系统都是多用户多任务的架构,其实现了多个用户可以同时操作一台计算机,多个应用程序可以同时运行,极大的提高了资源的利用率。

每个正在运行的应用程序(对于Windows操作系统来说,一个程序是一个.exe文件)就是一个进程。进程是程序的运行实例,是一个能独立运行的基本单位,同时也是系统分配资源和调度的独立单位即每个进程拥有独立的地址空间,而不同的进程是可以同时存在运转,因此要实现进程间的资源共享需要通过一些特殊的技术来实现。

每一个进程中至少包含一个线程,线程是程序执行流的最小单元(对于C++程序来说,这个单元是一个函数),是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己除了拥有一点在运行中必不可少的资源外不再拥有其它系统资源,但它可与同属于一个进程的其它线程共享进程所拥有的全部资源。

在应用程序中同时运行多个进程完成不同的工作,称为多进程;在单个程序中同时运行多个线程完成不同的工作,称为多线程。两种方案都能够实现任务的并发执行,各有千秋,具体采用哪一种需根据实际情况来定。

一般根据业务逻辑划分进程,由于进程的独立性,其可以根据具体的业务采用不同的编程语言实现,还可以在不同的机器上实现,并且一个进程崩溃了,一般不会牵连到其它的进程。

但是线程也有线程的好处:

  1. 创建一个新线程花费的时间少;
  2. 两个线程(在同一进程中的)的切换时间少;
  3. 由于同一个进程内的线程共享内存和文件,所以线程之间互相通信不必调用内核;

根据业务划分进程,进程数一般比较稳定,不会频繁的创建和销毁进程,而业务内可能由于用户数目等因素,不断地改变并发任务量,采用线程就比较合适。并且业务模块内各任务间的数据耦合一般比较紧密,在业务内部采用多线程将具有更好的性能。

  1. 多线程

由于不同的线程可以同时执行以完成不同的任务,同时不同线程间实现资源共享简单等特点,广泛用于处理任务重或需要实现任务并发处理的场合。但正由于线程的特点,使得多个线程可以同时访问同一资源(比如内存空间),造成资源访问冲突。比如有一个线程要修改某一个变量,而其它的线程需要在同一时刻读取这个变量,那么其它线程的读取结果是不可预料的,这就是竞争。因此在多线程的使用中,线程间的同步是一个重点和难点。

常用的线程同步技术有“关键代码段”、“互斥量”、“事件”等。其中“关键代码段”和“互斥量”都是在执行一段可能产生线程竞争的代码之前,对该段代码进行加锁,使其它的线程无法同时访问该代码段,这一部分代码段执行完毕后,再将锁解除,以使其它的线程能够执行该代码段。“事件”很像一个布尔量,具有激活、未激活两种状态,程序中一般将其作为标志来控制程序执行流,比如是否可以退出线程等,当然可以用它来指示是否有其它线程正在访问某变量从而避免竞争。具体实现方法可以参考相关书籍。

在不同的应用场合中,根据任务间的关系的不同,各线程间的关系也会不同:

例如对一个大文件进行压缩处理,首先把待压缩的数据分割成若干个大小合适的数据块,然后程序主线程启动相应数目的压缩线程,分别对不同的数据块同时进行压缩处理,这样也提高了整个文件的压缩处理速度。例子中的各个压缩线程实现完全一致,只是处理的数据不同,为并列状态;

再例如实时视频采集处理传输系统,对于每一帧图像都需要先获取然后进行一些处理最后通过网络发送出去。程序主线程启动一个数据获取线程用于不断获取视频数据,再启动若干个处理线程对视频帧进行不同的处理,还启动一个发送线程通过网络发送处理好的视频帧。此时就像工厂中的产品加工流水线一样,不同的线程处于递接状态。

随着任务复杂程度的增加,线程间的关系复杂程度也会增加,并呈现出不同的样貌。因此多线程程序的实现不能一概而论,需根据实际情况合理规划。

2.1 “IP流媒体服务器”中的视频处理多线程

“IP流媒体服务器”中需要实现多路视频的采集、处理、传输工作,并且各过程都能够由程序主线程控制,在实现上采用了多线程模式。

2.1.1 多线程的结构

本节分线程池结构线程的个数线程间的缓存线程间缓存访问的保护线程状态的控制等5个部分说明多线程的结构。

1、线程池结构

对于每路视频,其处理方式都相同,因此为每一路视频开辟一个线程池,由线程池完成相应处理工作。各线程池处于并列状态,其工作状态由主线程控制,结构如图2.1所示。

                                 

图2.1 多路视频处理线程间结构

2、线程的个数

对于某一路视频的处理有视频数据获取、编码处理、发送等过程(如图2.2),处理任务大,而视频处理具有实时性的要求,总共处理时间有限,因此采用多线程,将不同的处理任务交给不同的线程来完成(如图2.3),就像生产车间中的产品流水线一样,使得不同的处理任务可以同时进行,缩短了整个处理流程所需要的时间。

                      

图2.2 “IP流媒体服务器”处理任务

 

图2.3 “IP流媒体服务器”线程模式

通过将一序列串行处理分解为几个不同的部分,然后每个部分分别采用一个线程来实现,前一处理过程的处理结果交给下一处理过程继续处理,这样就实现不同处理任务同时进行,缩短整体处理时间,提高处理效率的效果。这就是“IP流媒体服务器”中多线程的基本思想。

线程间的缓存

由于不同的线程具体任务量不同,处理一帧视频数据所需的时间也就不同。如“视频数据获取线程”和“视频数据编码线程”:

  1. 视频数据获取线程中视频数据获取的工作量很小,所需时间很短,因此很快就可以把一帧数据获取到并丢给“视频数据编码线程”;
  2. 视频数据编码线程中视频数据编码需要对视频图像进行裁剪、格式转换、数据打包等工作,所需时间较长,需要较久才能处理完一帧图像数据。

如果不考虑视频帧速率,即新的视频帧不断地产生,那么在“视频数据获取线程”获取一个视频帧并丢给“视频数据编码线程”后又获取了一帧数据时,“视频数据编码线程”还未处理完上一帧数据。因此需要在相邻的线程间加入一个缓存(如图2.4),这样“视频数据获取线程”可以把其获取的数据放入缓存中,“视频数据编码线程”则在处理完一帧数据后从缓存中获取下一帧数据,解决了不同线程处理速度不一致的问题。

由于视频帧处理的顺序性,该缓存要具有先进先出的特性,因此缓存采用数据结构“队列”实现。

 

图2.4 “IP流媒体服务器”线程模式(加入缓存)

对于一帧视频数据,需要预先为其开辟好一块内存空间,线程A获取一帧数据后把数据填入内存,然后把内存地址放入队列B就可以了,线程B从队列B中取出内存地址,对内存中的一帧数据做处理,处理结束把内存地址放入队列C中,然后线程C从队列C中取出内存地址,将地址指向的一帧数据通过网络发送,然后把内存释放掉。

由于视频帧不断产生,就需要不断开辟内存、释放内存,然而开辟内存会消耗一定的时间,不断开辟内存还会导致系统内存中出现大量内存碎片,使内存的开辟更加困难,这对高任务量处理来说是很不妥当的。因此需要改进上面的线程间内存使用模式,将线程C要释放的内存转交给线程A继续使用,实现内存的重复使用。

实际工作中,线程A的工作节拍由摄像机的帧速率决定,即摄像机每捕获一帧画面,线程A才能获取一帧数据压入队列A。而线程C发送完一帧数据后,线程A可能还不没有获取到新的一帧数据,还不需要线程C废弃的内存空间,因此在线程C与线程A之间再加入一个缓存队列,暂存线程C废弃的内存空间地址,如图2.5。

 

图2.5 “IP流媒体服务器”线程模式(内存重复使用)

线程间缓存访问的保护

相邻两个线程间的数据传递通过缓存队列完成,由于线程的并发性,两个线程可能同时访问队列,如线程A要将数据压入队列B,线程B要从队列B中取数据。这就是数据访问的冲突,也属于线程间同步的问题。在这里采用关键代码段(临界区Critical Section)的保护方法来消除数据访问冲突:

为每个队列添加一个CRITICAL_SECTION(关键代码段、临界区),当需要访问某个队列时,先要进入其CRITICAL_SECTION,如果此时有其它的线程也在访问,该线程就会等待,直到其它的线程访问结束。该线程访问结束后,就离开该队列的CRITICAL_SECTION,以便其它的线程访问该队列。

线程状态的控制

为了使整个处理过程可控,需要能够控制各线程的状态如启动、暂停、退出等,这里为每个线程分配一个线程参数数据结构,里面有一些控制事件(EVENT),线程执行循环中不断检查各控制事件的状态来决定执行何操作。

2.1.2 多线程的实现

程序中为每路视频的处理任务分配了一个线程池,一个线程池中采用3个线程来实时处理一路视频数据。线程池包含了整个处理流程,在面向对象编程(OOP)中,由一个类(CImgProcess)来实现一个线程池,在类中封装了3个处理线程函数及相关参数、数据。

本节分线程池有关的数据结构线程池中的数据线程池中的函数线程池的使用步骤等4个部分说明如何实现一个线程池。

线程池有关的数据结构

  1. 数据包结构

由线程间结构中分析可知,线程池中需要的数据主要为三个线程间依次传递的数据包,包含视频帧图像数据内存空间指针、为处理而准备的缓存空间指针等,该数据包结构定义类似为:

// 图像处理数据结构,该结构用于线程间数据(包)交换,根据实际需要,增加修改内容

typedef struct _ST_PROC_DATA

{

    LPRGBQUAD lpRGB;    // 原始图像数据 LPRGBQUAD是MFC库定义的一个颜色结构体

    LPBYTE  pbyBufA;    // 处理缓存A LPBYTE为MFC库定义的字节型指针相当于unsigned char* 

    LPBYTE  pbyBufB;    // 处理缓存B

    // 临时缓存...

    // 需要的参数处理结果...

}ST_PROC_DATA, *PST_PROC_DATA;

  1. 线程参数结构

为了控制每个线程的工作状态,定义线程参数数据结构,包括线程指针、控制事件、缓存队列(为数据结构指针类型,其中如果是线程A的参数,则其中包含的队列就是队列A)及队列访问保护的CRITICAL_SECTION,该数据结构定义类似为:

// 线程参数,包含控制事件、源队列    主要用于控制线程状态

typedef struct _ST_THD_PARA

{

    CWinThread* pThd;       // 线程指针

    HANDLE hStart;          // 启动事件

    HANDLE hCanExit;        // 允许退出事件

    HANDLE hHasExit;        // 已经退出事件

    CRITICAL_SECTION crtDqOp;        // 缓存队列操作 临界区

    deque< PST_PROC_DATA > dqCache;  // 缓存队列,队列的节点类型就是上面定义的数据包结构

}ST_THD_PARA, *PST_THD_PARA;

线程池中的数据

有了相关的数据结构,就可以在类中定义要使用到的数据和参数,在这之前先在类CImgProcess头文件中定义几个用来标识线程的宏:

#define THREAD_CNT                        3   // 线程的总数,主要用于循环次数限制等

#define THREAD_GETDATA               0   // 获取数据线程的标号

#define THREAD_ENCODE                  1   // 图像处理线程的标号

#define THREAD_SEND                      2   // 数据发送线程的标号

然后在类中定义数据和参数成员:

private:

    // 图像处理数据数组

         ST_PROC_DATA m_staProcData[THREAD_CNT];

    // 线程参数数组,使用线程标号访问

         ST_THD_PARA m_staThdPara[THREAD_CNT];

在后面使用时,就可以使用线程的标号作为线程参数数组下标来访问与其对应的线程参数了。这里把图像处理数据包数定义成与线程数相同,由流水线的工作方式分析可知,基本某一时刻每个线程使用一个数据包。当然数据包数可以大于线程数,但可能造成一定内存空间的浪费;数据包数也可以小于线程数,这样的话由于数据空间资源的不足,可能会造成线程间的相互等待,降低程序效率。

线程池中的函数

  1. 线程函数

每个线程就是一个函数,不过该函数的类型具有一定的限制,只能是全局函数或是类的静态成员函数。这是因为要在编译链接的时候就要确定线程函数的入口地址,而类的普通成员函数只有在程序运行时生成类的实体时才确定,全局函数和类的静态成员函数却能满足要求。鉴于此,将各线程函数作为类的静态成员函数

由于类的静态成员函数不能直接访问类实体的普通的成员变量、成员函数,这为线程中对实体资源的访问造成一定的障碍,解决方法是通过类实体的指针来访问这些资源,即在创建线程时,将实体的指针通过参数传入线程,在线程中再调用该实体的普通成员函数完成相关处理。这样处理类中还需要与线程处理函数相对应的普通成员函数,这些函数由对应的线程函数调用,完成处理任务。相关函数定义如下:

private:

    // 获取数据

         BOOL GetData( PST_PROC_DATA pstProcData );

    // 编码

         BOOL Encode( PST_PROC_DATA pstProcData );

    // 发送

         BOOL Send( PST_PROC_DATA pstProcData );

    // 获取数据线程,内部调用GetData完成功能

         static UINT   ThdGetDataProc( LPVOID lpVoid );

    // 编码线程内部调用Encode完成功能

         static UINT   ThdEncodeProc( LPVOID lpVoid );

    // 发送线程,内部调用Send完成功能

         static UINT   ThdSendProc( LPVOID lpVoid );

其中线程函数的参数LPVOID  lpVoid就是用来传递当前线程池的指针的,从而在线程内部调用对应的处理函数完成相关处理,比如获取数据线程,启动时:

// 获取数据线程:优先级- 0

m_staThdPara[ THREAD_GETDATA ].pThd = AfxBeginThread( ThdGetDataProc, (LPVOID)this );

if ( m_staThdPara[ THREAD_GETDATA ].pThd == NULL )

    return FALSE;

启动线程传入的指针“this”就是当前线程池的指针。在获取数据线程内部,访问相应队列、调用“获取数据”函数过程为:

// 指针转换为线程池指针

CImgProcess* pImgProc = ( CImgProcess* )lpVoid;

...

// 没有缓存则继续

if ( pImgProc->m_staThdPara[ THREAD_GETDATA ].dqCache.empty() )

    continue;

...

// 获取数据

pImgProc->GetData( pstProcData );

// ...

  1. 辅助控制函数

类的实体创建后,需要开辟内存、初始化有关参数和启动个处理线程,这里添加一个初始化函数成员函数来完成这一功能;各线程的工作状态由相应事件来控制,而事件的状态维护和翻转由相应的普通成员函数完成,如启动、暂停、结束(退出)。其中“退出”函数中还需要完成相关资源的释放等。函数定义如下:

public:

    // 线程操作

         //  线程初始化,完成标志的初始化,内存资源的开辟,事件的创建,启动所有处理线程

         virtual BOOL Init();

    //  所有线程启动

         virtual void Start();

    //  所有线程暂停

         virtual void Pause();

    //  (所有)线程退出

         virtual void Exit();

有了这些函数,在程序主线程中就可以通过这些接口来控制线程池的运行状态了。

线程池的使用步骤

使用线程池类完成一路视频处理的步骤如下:

Step1 为一路视频定义一个线程池实体。

// 定义一个线程池,传入当前视图的指针用于报错、记录日志等

CImgProcess* pImgProc = new CImgProcess( this );

Step2 初始化线程池。调用线程池接口函数CImgProcess::Init()完成初始化:

// 初始化线程池

pImgProc->Init();

在初始化函数内部,要完成线程参数的设置、内存空间的开辟等,其流程如图2.6:

  1. 创建“获取数据事件”:“获取数据事件”是线程池类中的一个成员,其初始化为复位状态,由视频库在准备好一帧视频数据后置位,标志着有新的一帧数据,在获取数据线程中不断判断该标志来确定是否取回数据并做处理。
  2. 初始化线程参数:主要是逐个初始化线程参数中的缓存队列及与其对应的关键代码段、创建线程控制事件并且初始为复位状态。
  3. 为数据包分配内存空间:根据实际需要计算出处理过程中内存空间需求可能的最大值,以此为数据包内部指针分配空间,满足处理任务的需要;初始化完一个数据包后将其放入缓存队列中(相当于图2.5中的队列A)以供获取数据线程使用。
  4. 创建线程:分别创建三个处理线程并判断是否创建成功。只要有一个线程创建失败了,线程池初始化就是失败的。

图2.6 线程池的初始化

初始化代码主要如下:

// 线程池初始化

BOOL CImgProcess::Init()

{

    // 获取数据事件,在视频库中置位,在获取数据线程中检测其状态来判断是否有新数据

    // 事件初始化为 手动改变状态,初始状态为复位(FALSE)

    m_hHasData = CreateEvent( NULL, TRUE, FALSE, NULL );

    // 线程参数初始化,逐一初始化每个线程参数中的事件、队列

    for ( BYTE i = 0; i < THREAD_CNT; i++ )

    {

        // 创建(源)队列访问临界区

        InitializeCriticalSection( &m_staThdPara[i].crtDqOp );

        // 清空(源)队列

        m_staThdPara[i].dqCache.clear();

        // 事件初始化为 手动改变状态,初始状态为复位(FALSE)

        m_staThdPara[i].hStart = CreateEvent( NULL, TRUE, FALSE, NULL );

        m_staThdPara[i].hCanExit = CreateEvent( NULL, TRUE, FALSE, NULL );

        m_staThdPara[i].hHasExit = CreateEvent( NULL, TRUE, FALSE, NULL );

    }

    // 逐一为数据包结构内的指针分配内存,大小为可能需要的最大大小

    for ( BYTE i = 0; i < THREAD_CNT; i++ )

    {

        // 原始图像,RESO_XY为定义的宏,为图像画面的最大像素数

        m_staProcData[i].lpRGB = new RGBQUAD[ RESO_XY ];

        // 中间内存A

        m_staProcData[i].pbyBufA = new BYTE[ RESO_XY*sizeof(double) ];

        // 中间内存B

        m_staProcData[i].pbyBufB = new BYTE[ RESO_XY*sizeof(double) ];

        // ...

        // 加入获取数据队列,以供第一次启动所有线程后获取数据线程使用

        m_staThdPara[THREAD_GETDATA].dqCache.push_front( &m_staProcData[i] );

    }

    // 根据实际需要,初始化其它相关参数,变量等

    // ...

    // 启动线程

    // 获取数据线程:优先级- 0  根据线程任务量确定线程优先级

    m_staThdPara[ THREAD_GETDATA ].pThd =

        AfxBeginThread( ThdGetDataProc, (LPVOID)this );

    // 判断是否启动成功,失败则整个线程池的初始化失败

    if ( m_staThdPara[ THREAD_GETDATA ].pThd == NULL )

        return FALSE;

    // 编码线程优先级- 2  任务量最重,所以提高优先级已获得更多的处理时间片

    m_staThdPara[ THREAD_ENCODE ].pThd =

        AfxBeginThread( ThdEncodeProc, (LPVOID)this, THREAD_PRIORITY_HIGHEST );

    if ( m_staThdPara[ THREAD_ENCODE ].pThd == NULL )

        return FALSE;

    // 发送线程优先级- 0

    m_staThdPara[ THREAD_SEND ].pThd =

        AfxBeginThread( ThdSendProc, (LPVOID)this );

    if ( m_staThdPara[ THREAD_SEND ].pThd == NULL )

        return FALSE;

    return TRUE;

}

Step3 启动线程池。

线程创建之后由于内部操作(参见Step4),线程将会挂起,只有手动启动线程,各线程才会执行任务。调用CImgProcess::Start()即可启动各处理线程。在CImgProcess::Start()中将三个线程的“启动事件”置位,以让三个线程无阻碍执行线程循环。函数代码如下:

// 线程启动

void CImgProcess::Start()

{

    // 输出调试信息

    TRACE( "Start Threads.\n" );

    // 启动,用线程数宏作为循环次数,置位所有启动事件

         for ( BYTE i = 0; i < THREAD_CNT; i++ )

        SetEvent( m_staThdPara[i].hStart );

}

Step4 线程池执行任务处理。

虽然三个线程函数具体功能实现不太一样,但其运行流程却是一致的,线程函数的流程如图2.7。

其中“源队列”就是要从其中取出数据包的队列,而“宿队列”就是存放处理后的数据包的队列。

线程函数在线程循环中完成处理任务,每次循环都会检测相关事件以确定否继续运行、是否需要退出等状况:

  1. 启动事件。检测“启动事件”来确定是否挂起线程:“启动事件”置位,程序则正常执行后续指令;“启动事件”复位,该线程则挂起,直到“启动事件”置位;
  2. 退出事件。如果检测到“退出事件”被置位,线程函数就会退出线程循环进而结束线程;否则继续线程循环;
  3. 已退出事件。当线程退出线程循环后置位该事件,用于告诉程序主线程线程已退出,可以释放相关资源了。

因为“启动事件”初始化为“复位”状态,因此初始化完线程后,线程函数运行到检测启动事件位置便挂起,直到“启动事件”被置位也就是运行了启动函数CImgProcess::Start()。

线程函数中检测完控制事件后就会判断“源队列”是否有数据包可处理,如果没有则进入下次线程循环;有的话就取出数据包,对数据包进行处理,然后将处理完的数据包放入“宿队列”中。

线程循环中第一条语句将线程挂起SLEEP_TIME ms,这是因为数据包到来的频率由图像采集硬件决定的,大约每秒30帧左右,在两帧图像之间的时间内线程函数将执行许多次无用的循环,占用大量CPU资源。在线程进行完一次线程循环后休息SLEEP_TIME ms,以减少一些无谓的循环,释放一定CPU资源。

                                                

图2.7 线程函数的流程

每个线程函数的具体实现可参见小结中的“完整实现代码”。

Step5 线程池暂停任务处理。

实际工作中,会出现暂时不需要进行视频处理一段时间后再继续处理任务的情况,此时就需要将整个流水线停下来。这也由“启动事件”来实现,调用函数CImgProcess::Pause()将三个线程的“启动事件”复位,当线程循环检测到“启动事件”处于复位状态,线程就挂起,停止代码执行。线程暂停函数实现如下:

// 线程暂停

void CImgProcess::Pause()

{

    // 暂停,用线程数宏作为循环次数,复位所有启动事件

    for ( BYTE i = 0; i < THREAD_CNT; i++ )

        ResetEvent( m_staThdPara[i].hStart );

    // 输出调试信息

    TRACE( "Pause Threads.\n" );

}

当需要回复处理任务时,只需要调用启动函数CImgProcess::Start(),三个处理线程都被唤醒,继续处理任务。

Step6 结束线程池。

当不再需要对一路视频做处理时,就可以结束线程池了。在程序主线程中需要先退出线程池中的线程,然后释放相关资源,最后删除线程池。代码如下(pImgProc为之前创建的一个线程池):

// 线程退出

pImgProc->Exit();

// 删除线程池

delete pImgProc;

pImgProc = NULL;

在CImgProcess::Exit()中除了退出各线程外还要释放初始化时开辟的系统资源,其流程如图2.8:

  1. 置位所有线程的“退出事件”。通知该线程池中所有线程:跳出线程循环,退出线程。线程循环中检测到该事件置位就会退出线程循环进而安全结束线程。
  2. 启动线程。因为线程有可能处于暂停状体,这样的话就不会去检测“退出事件”的状态了,因此做了一步启动线程的操作。
  3. 检查线程是否退出。对各线程逐个检查“已退出事件”确定该线程是否已安全退出,如果没有安全退出则等待直到退出,之后清空对应的缓存队列、释放CRITICAL_SECTION。
  4. 删除获取数据事件。释放改事件句柄。
  5. 释放数据包。把每个数据包内开辟的内存空间资源释放掉。

图2.8 线程退出流程

代码如下:

void CImgProcess::Exit()

{

    // 通知退出 置位所有的“允许退出事件”

         for ( BYTE i = 0; i < THREAD_CNT; i++ )

        SetEvent( m_staThdPara[i].hCanExit );

    // 启动线程,保证线程能够执行检测“允许退出事件”

         Start();

    // 等待所有线程退出

         for ( BYTE i = 0; i < THREAD_CNT; i++ )

    {

        // 检测当前线程是否退出,没有的话等待,直到安全退出

        WaitForSingleObject( m_staThdPara[i].hHasExit, INFINITE );

        // 清空(源)队列

                   m_staThdPara[i].dqCache.clear();

        // 删除队列访问临界区

                   DeleteCriticalSection( &m_staThdPara[i].crtDqOp );

        // 关闭事件

                   CloseHandle( m_staThdPara[i].hStart );

        CloseHandle( m_staThdPara[i].hCanExit );

        CloseHandle( m_staThdPara[i].hHasExit );

    }

    // 关闭新数据事件

         CloseHandle( m_hHasData );

    // 释放内存

         for ( BYTE i = 0; i < THREAD_CNT; i++ )

    {

        // 原始图像,ReleaseArray自定义的宏函数,用于释放一个数组的内存

                   ReleaseArray( m_staProcData[i].lpRGB );

                   // 中间A

                   ReleaseArray( m_staProcData[i].pbyBufA );

                   // 中间B

                   ReleaseArray( m_staProcData[i].pbyBufB );

                   // 根据实际情况,释放其它等相关内存

                   // ...

         }

         // 根据实际情况,释放其它资源…

         TRACE( "Exit Threads.\n" );

}

2.1.3 小结

“IP流媒体服务器”中的视频图像处理多线程结构采用了线程池的概念解决多路视频的处理问题,每个线程池内部各线程采用流水作业的方式提高视频图像处理的效率,缩小处理时间,实现视频图像地实时处理。

移植过程中根据实际任务量合理划分出任务模块,各模块任务量最好大致相同并且处理最大任务量的模块所需时间满足要求。然后为各模块在线程池中一对一分配线程函数和处理函数。再根据各线程间数据传递的需要设计数据包的结构。最后编写各处理函数。移植过程中不可避免地需要修改线程池类中的成员变量、成员函数以适应实际需求,这些是不影响整个线程的结构的。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值