Acl網絡協程框架編程指南

Acl 網絡協程框架編程指南

摘要

本文主要講述Acl網絡協程框架的使用,從協程的應用場景出發,以一個簡單的協程示例開始,然後逐步深入到Acl網絡協程的各個使用場景及使用細節,以及需要避免的“坑”,希望能給大家帶來實踐上的幫助。

一、概述

講到協程,大家必然會提到 Go 語言,畢竟是 Go 語言把協程的概念及使用實踐普及的;但協程並不是一個新概念,我印象中在九十年代就出現了,當時一位同事還說微軟推出了纖程(基本一個意思),可以創建成午上萬個纖程,不必象線程那樣只能創建較少的線程數量,但當時也沒明白創建這麼多纖程有啥用,只不過是一個上下文的快速切換協同而已。所以自己在寫網絡高併發服務時,主要還是以非阻塞方式來實現。
其實,Go 語言的作者之一 Russ Cox 早在 2002 年左右就用 C 實現了一個簡單的基於協程的網絡通信模型 – libtask,但其只是一個簡單的網絡協程原型,還遠達不到實踐的要求。自從 Go 語言興起後,很多基於 C/C++ 開發的協程庫也多了起來,其中 Acl 協程庫便是其中之一。
Acl 工程地址:https://github.com/acl-dev/acl
Acl 協程地址:https://github.com/acl-dev/acl/tree/master/lib_fiber

二、簡單示例

下面爲一個使用 Acl 庫編寫的簡單線程的例子:

#include <acl-lib/acl_cpp/lib_acl.hpp>

class mythread : public acl::thread
{
public:
	mythread(void) {}
	~mythread(void) {}
private:
	// 實現基類中純虛方法,當線程啓動時該方法將被回調
	// @override
	void* run(void) {
		for (int i = 0; i < 10; i++) {
			printf("thread-%lu: running ...\r\n", acl::thread::self());
		}
		return NULL;
	}
};

int main(void)
{
	std::vector<acl::thread*> threads;
	for (int i = 0; i < 10; i++) {
		acl::thread* thr = new mythread;
		threads.push_back(thr);
		thr->start();  // 啓動線程
	}
	
	for (std::vector<acl::thread*>::iterator it = threads.begin();
		it != threads.end(); ++it) {
		(*it)->wait();  // 等待線程退出
		delete *it;
	}
	return 0;	
}

上面線程例子非常簡單,接着再給一個簡單的協程的例子:

#include <acl-lib/acl_cpp/lib_acl.hpp>
#include <acl-lib/fiber/libfiber.hpp>

class myfiber : public acl::fiber
{
public:
	myfiber(void) {}
	~myfiber(void) {}
private:
	// 重現基類純虛方法,當調用 fiber::start 時,該方法將被調用
	// @override
	void run(void) {
		for (int i = 0; i < 10; i++) {
			printf("hello world! the fiber is %d\r\n", acl::fiber::self());
			acl::fiber::yield();  // 讓出CPU運行權給其它協程
		}
	}
};

int main(void)
{
	std::vector<acl::fiber*> fibers;
	for (int i = 0; i < 10; i++) {
		acl::fiber* fb = new myfiber;
		fibers.push_back(fb);
		fb->start();  // 啓動一個協程
	}
	
	acl::fiber::schedule();  // 啓用協程調度器
	
	for (std::vector<acl::fiber*>::iterator it = fibers.begin();
		it != fibers.end(); ++it) {
		delete *it;
	}
}

上面示例演示了協程的創建、啓動及運行的過程,與前一個線程的例子非常相似,非常簡單(簡單實用是 Acl 庫的目標之一)。
協程調度其實是應用層面多個協程之間通過上下文切換形成的協作過程,如果一個協程庫僅是實現了上下文切換,其實並不具備太多實用價值,當與網絡事件綁定後,其價值纔會顯現出來。下面一個簡單的使用協程的網絡服務程序:

#include <acl-lib/acl_cpp/lib_acl.hpp>
#include <acl-lib/fiber/libfiber.hpp>

// 客戶端協程處理類,用來回顯客戶發送的內容,每一個客戶端連接綁定一個獨立的協程
class fiber_echo : public acl::fiber
{
public:
	fiber_echo(acl::socket_stream* conn) : conn_(conn) {}
private:
	acl::socket_stream* conn_;
	~fiber_echo(void) { delete conn_; }
	// @override
	void run(void) {
		char buf[8192];
		while (true) {
			// 從客戶端讀取數據(第三個參數爲false表示不必填滿整個緩衝區才返回)
			int ret = conn_->read(buf, sizeof(buf), false);
			if (ret == -1) {
				break;
			}
			// 向客戶端寫入讀到的數據
			if (conn_->write(buf, ret) != ret) {
				break;
			}
		}
		delete this; // 自銷燬動態創建的協程對象
	}
};

// 獨立的協程過程,接收客戶端連接,並將接收的連接與新創建的協程進行綁定
class fiber_listen : public acl::fiber
{
public:
	fiber_listen(acl::server_socket& listener) : listener_(listener) {}
private:
	acl::server_socket& listener_;
	~fiber_listen(void) {}
	// @override
	void run(void) {
		while (true) {
			acl::socket_stream* conn = listener_.accept();  // 等待客戶端連接
			if (conn == NULL) {
				printf("accept failed: %s\r\n", acl::last_serror());
				break;
			}
			// 創建並啓動單獨的協程處理客戶端連接
			acl::fiber* fb = new fiber_echo(conn);
			// 啓動獨立的客戶端處理協程
			fb->start();
		}
		delete this;
	}
};

int main(void)
{
	const char* addr = "127.0.0.1:8800";
	acl::server_socket listener;
	// 監聽本地地址
	if (listener.open(addr) == false) {
		printf("listen %s error %s\r\n", addr, acl::last_serror());
		return 1;
	}

	// 創建並啓動獨立的監聽協程,接受客戶端連接
	acl::fiber* fb = new fiber_listen(listener);
	fb->start();

	// 啓動協程調度器
	acl::fiber::schedule();
	return 0;
}

這是一個簡單的支持回顯功能的網絡協程服務器,可以很容易修改成線程模式。使用線程或線程處理網絡通信都可以採用順序思維模式,不必象非阻塞網絡編程那樣複雜,但使用協程的最大好處可以創建大量的協程來處理網絡連接,而要創建大量的線程顯示是不現實的(線程數非常多時,會導致操作系統的調度能力下降)。如果你的網絡服務應用不需要支持大併發,使用協程的意義就沒那麼大了

三、編譯安裝

在編譯前,需要先從 github https://github.com/acl-dev/acl 下載源碼,國內用戶可以選擇從 gitee https://gitee.com/acl-dev/acl 下載源碼。

3.1、Linux/Unix 平臺上編譯安裝

在 Linux/Unix 平臺上的編譯非常簡單,可以選擇使用 make 方式或 cmake 方式進行編譯。

  • make 方式編譯:
    在 acl 項目根目錄下運行:make && make packinstall,則會自動執行編譯與安裝過程,安裝目錄默認爲系統目錄:libacl_all.a, libfiber_cpp.a, libfiber.a 將被拷貝至 /usr/lib/ 目錄,頭文件將被拷貝至 /usr/include/acl-lib/ 目錄。
  • cmake 方式編譯:
    在 acl 項目根目錄下創建 build 目錄,然後:cd build && cmake … && make
  • 將 acl 庫加入至你的工程(以 make 方式爲例)
    先在代碼中加入頭文件包含項:
#include <acl-lib/acl_cpp/lib_acl.hpp>
#include <acl-lib/fiber/libfiber.hpp>

然後修改你的 Makefile 文件,示例如下:

mytest: mytest.cpp
	g++ -o mytest mytest.cpp -lfiber_cpp -lacl_all -lfiber -ldl -lpthread -lz

注意在 Makefile 中各個庫的依賴順序: libfiber_cpp.a 依賴於 libacl_all.a 和 libfiber.a,其中 libacl_all.a 爲 acl 的基礎庫,libfiber.a 爲 C 語言協程庫(其不依賴於 libacl_all.a),libfiber_cpp.a 用 C++ 語言封裝了 libfiber.a,且使用了 libacl_all.a 中的一些功能。

3.2、Windows 平臺上編譯

在 Windows 平臺的編譯也非常簡單,可以用 vc2008/2010/2012/2013/2015/2017 打開相應的工程文件進行編譯,如:可以用 vc2012 打開 acl_cpp_vc2012.sln 工程進行編譯。

3.3 Mac 平臺上編譯

除可以使用 Unix 統一方式(命令行方式)編譯外,還可以用 Xcode 打開工程文件進行編譯。

3.4 Android 平臺上編譯

目前可以使用 Android Studio3.x 打開 acl\android\acl 目錄下的工程文件進行編譯。

3.5 使用 MinGW 編譯

如果想要在 Windows 平臺上編譯 Unix 平臺上的軟件,可以借用 MinGW 套件進行編譯,爲此 Acl 庫還提供了此種編譯方式,但一般不建議用戶使用這種編譯方式,一方面是執行效率低,另一方面可能會存在某些不兼容問題。

3.6 小結

爲了保證 Acl 工程無障礙使用,本人在編譯 Acl 庫方面下了很大功夫,支持幾乎在所有平臺上使用原生編譯環境進行編譯使用,真正達到了一鍵編譯。甚至爲了避免因依賴第三方庫而導致的編譯問題(如:有的模塊需要 zlib 庫,有的需要 polassl 庫,有的需要 mysql/postgresql/sqlite 庫),將這些依賴第三方庫的模塊都寫成動態加載第三方庫的方式,畢竟不是所有人都需要這些第三方庫所提供的功能。

四、使用多核

Acl 協程的調度過程是基於單CPU的(雖然也可以修改成多核調度,但考慮到很多原因,最終還是採用了單核調度模式),即創建一個線程,所創建的所有協程都在這個線程空間中運行。爲了使用多核,充分使用CPU資源,可以創建多個線程(也可以創建多個進程),每個線程爲一個獨立的協程運行容器,各個線程之間的協程相互隔離,互不影響。
下面先修改一下上面的例子,改成多線程的協程方式:

#include <acl-lib/acl_cpp/lib_acl.hpp>
#include <acl-lib/fiber/libfiber.hpp>

// 客戶端協程處理類,用來回顯客戶發送的內容,每一個客戶端連接綁定一個獨立的協程
class fiber_echo : public acl::fiber
{
public:
	fiber_echo(acl::socket_stream* conn) : conn_(conn) {}
private:
	acl::socket_stream* conn_;
	~fiber_echo(void) { delete conn_; }
	// @override
	void run(void) {
		char buf[8192];
		while (true) {
			int ret = conn_->read(buf, sizeof(buf), false);
			if (ret == -1) {
				break;
			}
			if (conn_->write(buf, ret) != ret) {
				break;
			}
		}
		delete this; // 自銷燬動態創建的協程對象
	}
};

// 獨立的協程過程,接收客戶端連接,並將接收的連接與新創建的協程進行綁定
class fiber_listen : public acl::fiber
{
public:
	fiber_listen(acl::server_socket& listener) : listener_(listener) {}
private:
	acl::server_socket& listener_;
	~fiber_listen(void) {}
	// @override
	void run(void) {
		while (true) {
			acl::socket_stream* conn = listener_.accept();  // 等待客戶端連接
			if (conn == NULL) {
				printf("accept failed: %s\r\n", acl::last_serror());
				break;
			}
			// 創建並啓動單獨的協程處理客戶端連接
			acl::fiber* fb = new fiber_echo(conn);
			fb->start();
		}
		delete this;
	}
};

// 獨立的線程調度類
class thread_server : public acl::thread
{
public:
	thread_server(acl::server_socket& listener) : listener_(listener) {}
	~thread_server(void) {}
private:
	acl::server_socket& listener_;
	// @override
	void* run(void) {
		// 創建並啓動獨立的監聽協程,接受客戶端連接
		acl::fiber* fb = new fiber_listen(listener);
		fb->start();
		// 啓動協程調度器
		acl::fiber::schedule(); // 內部處於死循環過程
		return NULL;
	}
};

int main(void)
{
	const char* addr = "127.0.0.1:8800";
	acl::server_socket listener;
	// 監聽本地地址
	if (listener.open(addr) == false) {
		printf("listen %s error %s\r\n", addr, acl::last_serror());
		return 1;
	}

	std::vector<acl::thread*> threads;
	// 創建多個獨立的線程對象,每個線程啓用獨立的協程調度過程
	for (int i = 0; i < 4; i++) {
		acl::thread* thr = thread_server(listener);
		threads.push_back(thr);
		thr->start();
	}
	for (std::vector<acl::thread*>::iterator it = threads.begin();
		it != threads.end(); ++it) {
		(*it)->wait();
		delete *it;
	}
	return 0;
}

經過修改,上面的例子即可以支持大併發,又可以使用多核。

五、多核同步

上面的例子中涉及到了通過創建多線程使用多核的過程,但肯定會有人問,在多個線程中的協程之間如果想要共享某個資源怎麼辦?Acl 協程庫提供了可以跨線程使用同步原語:線程協程事件同步及條件變量。
首先介紹一下事件同步對象類:acl::fiber_event,該類提供了三個方法:

	/**
	 * 等待事件鎖
	 * @return {bool} 返回 true 表示加鎖成功,否則表示內部出錯
	 */
	bool wait(void);

	/**
	 * 嘗試等待事件鎖
	 * @return {bool} 返回 true 表示加鎖成功,否則表示鎖正在被佔用
	 */
	bool trywait(void);

	/**
	 * 事件鎖擁有者釋放事件鎖並通知等待者
	 * @return {bool} 返回 true 表示通知成功,否則表示內部出錯
	 */
	bool notify(void);

下面給出一個例子,看看在多個線程中的協程之間如何進行互斥的:

#include <acl-lib/acl_cpp/lib_acl.hpp>
#include <acl-lib/fiber/libfiber.hpp>

class myfiber : public acl::fiber
{
public:
	myfiber(acl::fiber_event& lock, int& count): lock_(lock), count_(count) {}
private:
	~myfiber(void) {}
	// @override
	void run(void) {
		for (int i = 0; i < 100; i++) {
			lock_.wait();
			count_++;
			lock_.notify();
			//acl::fiber::delay(1);  // 本協程休息1毫秒
		}
		delete this;
	}
private:
	acl::fiber_event& lock_;
	int& count_;
};

class mythread : public acl::thread
{
public:
	mythread(acl::fiber_event& lock, int& count): lock_(lock), count_(count) {}
	~mythread(void) {}
private:
	// @override
	void* run(void) {
		for (int i = 0; i < 100; i++) {
			acl::fiber* fb = new myfiber(lock_, count_);
			fb->start();
		}
		acl::fiber::schedule();
		return NULL;
	}
private:
	acl::fiber_event& lock_;
	int& count_;
};

int main(void)
{
	acl::fiber_event lock;  // 可以用在多個線程之間、各個線程中的協程之間的同步過程
	int count = 0;
	std::vector<acl::thread*> threads;
	for (int i = 0; i < 4; i++) {
		acl::thread* thr = new mythread(lock, count);
		threads.push_back(thr);
		thr->start();
	}
	for (std::vector<acl::thread*>::iterator it = threads.begin();
		it != threads.end(); ++it) {
		(*it)->wait();
		delete *it;
	}

	printf("all over, count=%d\r\n", count);
	return 1;
}

acl::fiber_event 常被用在多個線程中的協程之間的同步,當然也可以用在多個線程之間的同步,這在很大程度彌補了 Acl 協程框架在使用多核上的不足。

六、消息傳遞

通過組合 acl::fiber_event(協程事件)和 acl::fiber_cond(協程條件變量),實現了協程間進行消息傳遞的功能模塊:acl::fiber_tbox,fiber_tbox 不僅可以用在同一線程內的協程之間傳遞消息,而且還可以用在不同線程中的協程之間,不同線程之間,線程與協程之間傳遞消息。fiber_tbox 爲模板類,因而可以傳遞各種類型對象。以下給出一個示例:

#include <acl-lib/acl_cpp/lib_acl.hpp>
#include <acl-lib/fiber/libfiber.hpp>

class myobj
{
public:
	myobj(void) : i_(0) {}
	~myobj(void) {}
	void set(int i) {
		i_ = i;
	}
	void test(void) {
		printf("hello world, i=%d\r\n", i_);
	}
private:
	int i_;
};

// 消費者協程,從消息管道中讀取消息
class fiber_consumer : public acl::fiber
{
public:
	fiber_consumer(acl::fiber_tbox<myobj>& box) : box_(box) {}
private:
	~fiber_consumer(void) {}
private:
	acl::fiber_tbox<myobj>& box_;
	// @override
	void run(void) {
		while (true) {
			myobj* o = box_.pop();
			// 如果讀到空消息,則結束
			if (o == NULL) {
				break;
			}
			o->test();
			delete o;
		}
		delete this;
	}
};

// 生產者協程,向消息管道中放置消息
class fiber_producer : public acl::fiber
{
public:
	fiber_producer(acl::fiber_tbox<myobj>& box) : box_(box) {}
private:
	~fiber_producer(void) {}
private:
	acl::fiber_tbox<myobj>& box_;
	// @override
	void run(void) {
		for (int i = 0; i < 10; i++) {
			myobj* o = new myobj;
			o->set(i);
			// 向消息管道中放置消息
			box_.push(o);
		}
		// 放置空消息至消息管道中,從而通知消費者協程結束
		box_.push(NULL);
		delete this;
	}
};

int main(void)
{
	acl::fiber_tbox<myobj> box;
	// 創建並啓動消費者協程
	acl::fiber* consumer = new fiber_consumer(box);
	consumer->start();
	// 創建並啓動生產者協程
	acl::fiber* producer = new fiber_producer(box);
	producer->start();
	// 啓動協程調度器
	acl::fiber::schedule();
	return 0; 
}

上面例子展示了同一線程中的兩個協程之間的消息傳遞過程,因爲 acl::fiber_tbox 是可以跨線程的,所以它的更大價值是用在多個線程中的不同協程之間進行消息傳遞。

#include <acl-lib/acl_cpp/lib_acl.hpp>
#include <acl-lib/fiber/libfiber.hpp>

class myobj
{
public:
	myobj(void) : i_(0) {}
	~myobj(void) {}
	void set(int i) {
		i_ = i;
	}
	void test(void) {
		printf("hello world, i=%d\r\n", i_);
	}
private:
	int i_;
};

// 消費者協程,從消息管道中讀取消息
class fiber_consumer : public acl::fiber
{
public:
	fiber_consumer(acl::fiber_tbox<myobj>& box) : box_(box) {}
private:
	~fiber_consumer(void) {}
private:
	acl::fiber_tbox<myobj>& box_;
	// @override
	void run(void) {
		while (true) {
			myobj* o = box_.pop();
			// 如果讀到空消息,則結束
			if (o == NULL) {
				break;
			}
			o->test();
			delete o;
		}
		delete this;
	}
};

// 生產者線程,向消息管道中放置消息
class thread_producer : public acl::thread
{
public:
	thread_producer(acl::fiber_tbox<myobj>& box) : box_(box) {}
	~thread_producer(void) {}
private:
	acl::fiber_tbox<myobj>& box_;
	void* run(void) {
		for (int i = 0; i < 10; i++) {
			myobj* o = new myobj;
			o->set(i);
			box_.push(o);
		}
		box_.push(NULL);
		return NULL;
	}
};

int main(void)
{
	acl::fiber_tbox<myobj> box;
	// 創建並啓動消費者協程
	acl::fiber* consumer = new fiber_consumer(box);
	consumer->start();
	// 創建並啓動生產者線程
	acl::thread* producer = new thread_producer(box);
	producer->start();
	// 啓動協程調度器
	acl::fiber::schedule();
	// schedule() 過程返回後,表示該協程調度器結束。
	// 等待生產者線程退出
	producer->wait();
	delete producer;
	return 0; 
}

在該示例中,生產者爲一個獨立的線程,消費者爲另一個線程中的協程,二者通過 acl::fiber_tbox 進行消息通信。但有一點需要注意,fiber_tbox 一般可用在“單生產者-單消費者或多生產者-單消費者”的應用場景中,不應用在多消費者的場景中,雖然用在多個消費者場景時不會造成消費丟失或內存崩潰,但當消費者數量較多時卻有可能出現驚羣現象,所以應避免將一個 acl::fiber_tbox 用在大量的多消費者場景中。
下面再給一個應用場景的例子,也是我們平時經常會遇到的。

#include <unistd.h>
#include <acl-lib/acl_cpp/lib_acl.hpp>
#include <acl-lib/fiber/libfiber.hpp>

class mythread : public acl::thread
{
public:
	mythread(acl::fiber_tbox<int>& box) :box_(box) {}
	~mythread(void) {}
private:
	acl::fiber_tbox<int>& box_;
	// @override
	void* run(void) {
		int i;
		for (i = 0; i < 5; i++) {
			/* 假設這是一個比較耗時的操作*/
			printf("sleep one second\r\n");
			sleep(1);
		}
		int* n = new int(i);
		// 將計算結果通過消息管道傳遞給等待者協程
		box_.push(n);
		return NULL; 
	}
};

class myfiber : public acl::fiber
{
public:
	myfiber(void) {}
	~myfiber(void) {}
private:
	// @override
	void run(void) {
		acl::fiber_tbox<int> box;
		mythread thread(box);
		thread.set_detachable(true);
		thread.start();  // 啓動獨立的線程計算耗時運算
		int* n = box.pop();  // 等待計算線程返回運算結果,僅會阻塞當前協程
		printf("n is %d\r\n", *n);
		delete n;
	}
};

int main(void)
{
	myfiber fb;
	fb.start();
	acl::fiber::schedule();
	return 0;
}

協程一般用在網絡高併發環境中,但協程並不是萬金油,協程並不適合計算密集型應用,因爲線程纔是操作系統的最小調度單元,而協程不是,所以當遇到一些比較耗時的運算時,爲了不阻塞當前協程所在的協程調度器,應將該耗時運算過程中拋給獨立的線程去處理,然後通過 acl::fiber_tbox 等待線程的運算結果。

七、HOOK API

爲了使現有的很多網絡應用和網絡庫在儘量不修改的情況下協程化,Acl 協程庫 Hook 了很多與 IO 和網絡通信相關的系統 API,目前已經 Hook 的系統 API 有:

內容項 API
網絡相關 socket/listen/accept/connect
IO相關 read/readv/recv/recvfrom/recvmsg/write/writev/send/sendto/sendmsg/sendfile64
域名相關 gethostbyname(_r)/getaddrinfo/freeaddrinfo
事件相關 select/poll/epoll_create/ epoll_ctl/epoll_wait
其它 close/sleep

八、域名解析

使用協程方式編寫網絡通信程序,域名解析是不能繞過的,記得有一個協程庫說爲了支持域名解析,甚至看了相關實現代碼,然後說通過 Hook _poll API 就可以了,實際上這並不是通用的做法,至少在我的環境裏通過 Hook _poll API 是沒用的,所以最穩妥的做法還是應該將 DNS 查詢協議實現了,在 acl 的協程庫中,域名解析模塊實際是集成了第三方 DNS 庫,參見:https://github.com/wahern/dns , 畢竟,實現一個較爲完整的 DNS 解析庫還是比較麻煩的。

九、使第三方網絡庫協程化

通常網絡通信庫都是阻塞式的,因爲非阻塞式的通信庫的通用性不高(使用各自的事件引擎,很難達到應用層的使用一致性),如果把這些第三方通信庫(如:mysql 客戶端庫,Acl 中的 Redis 庫)使用協程所提供的 IO 及網絡 API 重寫一遍則工作量太大,不太現實,好在 Acl 協程庫 Hook 了很多系統 API,從而使阻塞式的網絡通信庫協程化變得簡單。所謂網絡庫協程化就是使這些網絡庫可以應用在協程環境中,從而可以很容易編寫出支持高併發的網絡程序。
先寫一個將 Acl Redis 客戶端庫協程化的例子:

#include <acl-lib/acl_cpp/lib_acl.hpp>
#include <acl-lib/fiber/libfiber.hpp>

class fiber_redis : public acl::fiber
{
public:
	fiber_redis(acl::redis_client_cluster& cluster) : cluster_(cluster) {}
private:
	~fiber_redis(void) {}
private:
	acl::redis_client_cluster& cluster_;
	// @override
	void run(void) {
		const char* key = "hash-key";
		for (int i = 0; i < 100; i++) {
			acl::redis cmd(&cluster_);
			acl::string name, val;
			name.format("hash-name-%d", i);
			val.format("hash-val-%d", i);
			if (cmd.hset(key, name, val) == -1) {
				printf("hset error: %s, key=%s, name=%s\r\n",
					cmd.result_error(), key, name.c_str());
				break;
			}
		}
		delete this;
	}
};

int main(void)
{
	const char* redis_addr = "127.0.0.1:6379";
	acl::redis_client_cluster cluster;
	cluster.set(redis_addr, 0);
	for (int i = 0; i < 100; i++) {
		acl::fiber* fb = new fiber_redis(cluster);
		fb->start();
	}
	acl::fiber::schedule();
	return 0;
}

讀者可以嘗試將上面的代碼拷貝到自己機器上,編譯後運行一下。另外,這個例子是隻有一個線程,所以會發現 acl::redis_client_cluster 的使用方式和在線程下是一樣的。如果將 acl::redis_client_cluster 在多個線程調度器上共享會怎樣?還是有一點區別,如下:

#include <acl-lib/acl_cpp/lib_acl.hpp>
#include <acl-lib/fiber/libfiber.hpp>

// 每個協程共享相同的 cluster 對象,向 redis-server 中添加數據
class fiber_redis : public acl::fiber
{
public:
	fiber_redis(acl::redis_client_cluster& cluster) : cluster_(cluster) {}
private:
	~fiber_redis(void) {}
private:
	acl::redis_client_cluster& cluster_;
	// @override
	void run(void) {
		const char* key = "hash-key";
		for (int i = 0; i < 100; i++) {
			acl::redis cmd(&cluster_);
			acl::string name, val;
			name.format("hash-name-%d", i);
			val.format("hash-val-%d", i);
			if (cmd.hset(key, name, val) == -1) {
				printf("hset error: %s, key=%s, name=%s\r\n",
					cmd.result_error(), key, name.c_str());
				break;
			}
		}
		delete this;
	}
};

// 每個線程運行一個獨立的協程調度器
class mythread : public acl::thread
{
public:
	mythread(acl::redis_client_cluster& cluster) : cluster_(cluster) {}
	~mythread(void) {}
private:
	acl::redis_client_cluster& cluster_;
	// @override
	void* run(void) {
		for (int i = 0; i < 100; i++) {
			acl::fiber* fb = new fiber_redis(cluster_	);
			fb->start();
		}
		acl::fiber::schedule();
		return NULL;
	}
};

int main(void)
{
	const char* redis_addr = "127.0.0.1:6379";
	acl::redis_client_cluster cluster;
	cluster.set(redis_addr, 0);
	cluster.bind_thread(true);

	// 創建多個線程,共享 redis 集羣連接池管理對象:cluster,即所有線程中的
	// 所有協程共享同一個 cluster 集羣管理對象
	std::vector<acl::thread*> threads;
	for (int i = 0; i < 4; i++) {
		acl::thread* thr = new mythread(cluster);
		threads.push_back(thr);
		thr->start();
	}
	for (std::vector<acl::thread*>::iterator it = threads.begin();
		it != threads.end(); ++it) {
		(*it)->wait();
		delete *it;
	}
	return 0;
}

在這個多線程多協程環境裏使用 acl::redis_client_cluster 對象時與前面的一個例子有所不同,在這裏調用了:cluster.bind_thread(true);
爲何要這樣做?原因是 Acl Redis 的協程調度器是單線程工作模式,網絡套接字句柄在協程環境裏不能跨線程使用,當調用 bind_thread(true) 後,Acl 連接池管理對象會自動給每個線程分配一個連接池對象,每個線程內的所有協程共享這個綁定於本線程的連接池對象。

十、Windows 界面編程協程化

在Windows下寫過界面程序的程序員都經歷過使通信模塊與界面結合的痛苦過程,因爲 Windows 界面過程是基於 win32 消息引擎驅動的,所以在編寫通信模塊時一般有兩個選擇:要麼使用 Windows 提供的異步非阻塞 API,要麼把通信模塊放在獨立於界面的單獨線程中然後通過窗口消息將結果通知窗口界面過程。
Acl 協程庫的事件引擎支持 win32 消息引擎,所以很容易將界面過程的通信過程協程化,採用這種方式,一方面程序員依然可以採用順序編程方式,另一方面通信協程與界面過程運行於相同的線程空間,則二者在相互訪問對方的成員對象時不必加鎖,從而使編寫通信過程變得更加簡單。
下面以一個簡單的對話框爲例說明界面網絡通信協程化過程:

  1. 首先使用嚮導程序生成一個對話框界面程序,需要指定支持 socket 通信;
  2. 然後在 OnInitDialog() 方法尾部添加如下代碼:
	// 設置協程調度的事件引擎,同時將協程調度設爲自動啓動模式
	acl::fiber::init(acl::FIBER_EVENT_T_WMSG, true);
	// HOOK ACL 庫中的網絡 IO 過程
	acl::fiber::acl_io_hook();
  1. 創建一個按鈕,並使其綁定一個事件方法,如:OnBnClickedListen,然後在這個方法裏添加一些代碼:
	// 創建一個協程用來監聽指定地址,接收客戶端連接請求
	m_fiberListen = new CFiberListener("127.0.0.1:8800");
	// 啓動監聽協程
	m_fiberListen->start();
  1. 實現步驟 3 中指定的監聽協程類
class CFiberListener : public acl::fiber
{
public:
	CFiberListener(const char* addr) : m_addr(addr) {}
private:
	~CFiberListener(void) {}
private:
	acl::string m_addr;
	acl::server_socket m_listener;
	// @override
	void run(void) {
		// 綁定並監聽指定的本地地址
		if (m_listener.open(m_addr) == false) {
			return;
		}
		while (true) {
			// 等待客戶端連接
			acl::socket_stream* conn = m_listener.accept();
			if (conn == NULL) {
				break;
			}
			// 創建獨立的協程處理該客戶端的請求
			acl::fiber* fb = new CFiberClient(conn);
			fb->start(); // 啓動客戶端處理協程
		}
		delete this;
	}
};
  1. 實現步驟 4 中指定的客戶端響應協程類
class CFiberClient : public acl::fiber
{
public:
	CFiberClient(acl::socket_stream* conn) : m_conn(conn) {}
private:
	~CFiberClient(void) { delete m_conn; }
private:
	acl::socket_stream* m_conn;
	// @override
	void run(void) {
		char buf[8192];
		while (true) {
			// 從客戶端讀取數據
			int ret = m_conn->read(buf, sizeof(buf), false);
			if (ret == -1) {
				break;
			}
			// 將讀到的數據回寫給客戶端
			if (m_conn->write(buf, ret) != ret) {
				break;
			}
		}
		delete this;
	}
};

通過以上步驟就可爲 win32 界面程序添加基於協程模式的通信模塊,上面的兩個協程類的處理過程都是“死循環”的,而且又與界面過程同處同一線程運行空間,卻爲何卻不會阻塞界面消息過程呢?其原因就是當通信協程類對象在遇到網絡 IO 阻塞時,會自動將自己掛起,將線程的運行權交給其它協程或界面過程。原理就是這麼簡單,但內部實現還有點複雜度的,感興趣的可以看看 Acl 協程庫的實現源碼(https://github.com/acl-dev/acl/tree/master/lib_fiber/ )。
此外,上面示例的完整代碼請參考:https://github.com/acl-dev/acl/tree/master/lib_fiber/samples/WinEchod

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