【GamingAnywhere源码分析之知识补充六】Windows多线程信号通信与GA整体框架修改

        关于GamingAnywhere整体框架的修改也已经结束了一段时间了,这段时间云游戏项目暂时停滞了,原因是:别的项目组人员不足,上级领导又被boss催的紧,直属上级领导都调去别的组了,so,我也不能幸免。但是,已经做了的东西,一定要把自己的思路与理解记录下来,不然等自己回过头来看的时候又会花很多时间去理解原本已经明白的事情。这一篇涉及到GamingAnywhere整体大的框架,虽然改动的不多,而且现在回过头来看,对它的整个实现框架也很清晰明了了。

        在改篇文章中,将会涉及以下内容:

        1> GamingAnywhere整体框架修改需求

        2> GamingAnywhere整体框架运作流程与修改思路

        3> Windows多线程信号通信介绍(互斥锁、条件变量)

第一部分:GamingAnywhere整体框架修改需求

        GamingAnywhere整体框架修改的目的是实现捕捉方式的动态可配置性,即GamingAnywhere原有的捕捉方式和当前我们使用的采用nvidia显卡进行捕捉的方式的动态可配置性,具体来说就是我从一款游戏的配置文件中通过设置,可以决定它的捕捉方式,采用或不采用nvidia进行捕捉。

第二部分:GamingAnywhere整体框架运作流程与修改思路

        下面我认为是GamingAnywhere最核心的运作流程图(不包含具体捕捉和协议层的通信):

                                             

        GamingAnywhere中的核心模块拆开来看无非就是这么几个:窗口挂载钩子实现游戏画面捕捉模块、图像格式转换(由rgb转为yuv)模块、数据进行h.264压缩模块以及涉及到协议的通信模块,其中游戏画面捕捉后源数据的存放、图像格式的转换,以及数据压缩的实现就是由上面这几个线程实现的。

        下面简单描述一下这几个模块的运作方式以及是如何协同工作的,GA中模块都是进行动态加载的,即:分别经历load_modules()、init_modules()以及run_modules()实现动态加载。load_modules()完成的工作是:1) 加载对应模块的dll文件。2) 获取模块中实现函数的函数指针,GamingAnywhere中的模块一般就实现两个函数,即初始化以及线程处理函数,以filter_RGB2YUV模块为例:它实现了filter_RGB2YUV_init()和filter_RGB2YUV_threadproc()函数。load_modules的核心代码,一看便知:

struct ga_module *
ga_load_module(const char *modname, const char *prefix) {
	char fn[1024];
	struct ga_module m, *pm;
#ifdef WIN32
	snprintf(fn, sizeof(fn), "%s.dll", modname);
#elif defined __APPLE__
	snprintf(fn, sizeof(fn), "%s.dylib", modname);
#else
	snprintf(fn, sizeof(fn), "%s.so", modname);
#endif
	if((m.handle = dlopen(fn, RTLD_NOW|RTLD_LOCAL)) == NULL) {
		ga_error("ga_load_module: load module (%s) failed - %s.\n", fn, dlerror());
		return NULL;
	}
	//
	m.init = (int (*)(void*)) ga_module_loadfunc(m.handle, prefix, "init");
	m.threadproc = (void* (*)(void*)) ga_module_loadfunc(m.handle, prefix, "threadproc");;
	m.deinit = (void (*)(void*)) ga_module_loadfunc(m.handle, prefix, "deinit");
	m.notify = (int (*)(void*,int)) ga_module_loadfunc(m.handle, prefix, "notify");
	// nothing exports?
	if(m.init == NULL
	&& m.threadproc == NULL
	&& m.deinit == NULL
	&& m.notify == NULL) {
		ga_error("ga_load_module: [%s] does not export nothing.\n", fn);
		ga_unload_module(&m);
		return NULL;
	}
	//
	if((pm = (struct ga_module*) malloc(sizeof(m))) == NULL) {
		ga_error("ga_load_module: [%s] malloc failed - %s\n", fn, strerror(errno));
		return NULL;
	}
	bcopy(&m, pm, sizeof(m));
	mlist[pm] = pm;
	//
	return pm;
}
         init_modules()负责执行模块的初始化工作,核心代码很简单,就是根据init的函数指针执行init的函数:

int
ga_init_single_module(const char *name, struct ga_module *m, void *arg) {
	if(m->init == NULL)
		return 0;
	if(m->init(arg) < 0) {
		ga_error("%s init failed.\n", name);
		return -1;
	}
	return 0;
}
        run_modules()就是实现模块的实际运行,实质上它是创建一个线程,然后去执行模块中的threadproc函数,即:

int
ga_run_single_module(const char *name, void * (*threadproc)(void*), void *arg) {
	pthread_t t;
	if(threadproc == NULL)
		return 0;
	if(pthread_create(&t, NULL, threadproc, arg) != 0) {
		ga_error("cannot create %s thread\n", name);
		return -1;
	}
	pthread_detach(t);
	return 0;
}
        GamingAnywhere就是通过这样来实现模块的动态加载、初始化以及运行的,整体运作流程为:游戏窗口挂上钩子后就进入资源初始化以及实际画面的捕捉流程,在资源初始化的时候会创建一个原始数据的pipe:image-0,每个pipe会维护一个由8个数据块组成的循环链表,当游戏画面的数据捕捉到后,数据会先被存放在原始通道image-0中,此时会执行一个notify_all()的操作,该操作会循环遍历一个map类型的变量,即:std::map<long,pthread_cond_t*> condmap; 它里面保存了线程ID以及它对应的条件变量,notify_all()执行以下操作:

void
pipeline::notify_all() {
	map<long,pthread_cond_t*>::iterator mi;
	pthread_mutex_lock(&condMutex);
	for(mi = condmap.begin(); mi != condmap.end(); mi++) {
		pthread_cond_signal(mi->second);
	}
	pthread_mutex_unlock(&condMutex);
	return;
}
        即遍历该map结构,并为所有线程发送信号(pthread_cond_signal())。当filter_rgb2yuv线程在初始化的时候,它也会创建一个pipe,即:filter-0,也被初始化为8个数据块的循环链表,当filter_rgb2yuv线程wait到自己的信号时,它会从数据源即pipe:image-0中循环取出数据并进行rgb到yuv数据格式的转换,然后存放到自己的pipe:filter-0中,每存放一个数据块也会执行notify_all()来通知其它线程,此时encoder-video线程wait到自己的信号时,会从filter-0中取出格式转换后的数据,然后进行h.264的压缩,接着直接将它发送出去,如此循环整个工程就这样运转起来啦。这里要留意一下notify_all(),它是对所有的线程发送信号,那么如何保证encoder-video线程就是在filter-rgb2yuv之后运行呢?即只有在图像格式转换完成后encoder-video才进行数据压缩和发送,GA实现的方式是,在encoder-video中使用:pthread_cond_timewait(),如果此时有数据则立即取出数据进行压缩发送,如果此时没有数据则最多等待一秒钟,不然就一直continue等待。

        而这里利用nvidia进行捕捉与利用原始方式进行捕捉的区别是,利用nvidia捕捉到的数据原本就是进行格式转换和h.264压缩后的,所以不需要再次进行格式转换和压缩,获取到之后直接发送就可以了,而pipe也由原始的两个:image-0和filter-0变为只有image-0了,从这点来看,利用nvidia进行捕捉要比原始的捕捉方式要方便的多。如果真正理解了它的运作方式,修改起来很快,可以读取配置文件然后有选择地加载filter_rgb2yuv和encoder-video模块就可以了,当然在捕捉完数据后也需要做修改,即直接发送。

第三部分:Windows多线程信号通信介绍(互斥量和条件变量)

        多线程之间的通信需求一般会发生在以下两种情况中:1)多个线程对共享资源进行访问,但不希望共享资源被破坏;2)一个线程完成了任务,要通知其它的线程。这里我们理解的更多的情况应该是第二种,即捕捉到数据放到pipe:image-0中后通知filter-rgb2yuv线程从pipe:image-0中取出数据进行图像格式的转换并存入pipe:filter-0中;当filter-rgb2yuv完成一个数据块的转换后就通知encoder-video线程从pipe:filter-0中取出数据进行h.264的压缩以及数据包的发送。

        注意:条件变量的使用总是和一个互斥锁结合在一起。

        这里涉及到的变量类型以及函数分别有:pthread_cond_t , pthread_mutex_t , pthread_cond_signal() , pthread_mutex_lock , pthread_mutex_unlock

总结:

         GamingAnywhere整体的核心运作流程就如上所说,多线程之间的协同用好了很难,不过模块的动态加载与运行这个还是很值得借鉴的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章