【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整體的核心運作流程就如上所說,多線程之間的協同用好了很難,不過模塊的動態加載與運行這個還是很值得借鑑的。

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