新聞中心
探索 Linux 網(wǎng)絡編程:socket 多線程

創(chuàng)新互聯(lián)從2013年創(chuàng)立,先為公主嶺等服務建站,公主嶺等地企業(yè),進行企業(yè)商務咨詢服務。為公主嶺企業(yè)網(wǎng)站制作PC+手機+微官網(wǎng)三網(wǎng)同步一站式服務解決您的所有建站問題。
Linux 是基于開源的操作系統(tǒng),它的網(wǎng)絡編程能力非常強大。為了更好地理解 Linux 網(wǎng)絡編程,我們需要探索 Socket 多線程技術(shù),這種技術(shù)可以提高網(wǎng)絡應用的性能和并發(fā)連接的能力。
今天,本文將介紹 Socket 多線程的工作原理和實現(xiàn)方法,在實際項目中如何運用 Socket 多線程技術(shù),以及 Socket 多線程的優(yōu)勢和限制。
什么是 Socket 多線程?
Socket 是一種網(wǎng)絡通信協(xié)議,可以用于實現(xiàn)客戶端和服務器之間的通信。我們可以使用 Linux 的 Socket 編程接口進行網(wǎng)絡編程。多線程是一種并發(fā)編程方式,我們可以通過創(chuàng)建多個線程來并發(fā)執(zhí)行任務,從而提高應用程序的性能和效率。
因此,Socket 多線程是指在 Linux 環(huán)境下,通過創(chuàng)建多個線程來實現(xiàn) Socket 通信的并發(fā)連接能力,以提高網(wǎng)絡應用的性能和效率。
Socket 多線程的工作原理
Socket 多線程的工作原理主要分為兩個階段:初始化和執(zhí)行。
初始化階段:
1.創(chuàng)建 Socket:在服務器端創(chuàng)建一個 Socket,并監(jiān)聽指定端口,等待客戶端連接。
2.創(chuàng)建線程池:在服務器端創(chuàng)建一個線程池,用于存放多個工作線程。
3.創(chuàng)建工作線程:在線程池中創(chuàng)建多個工作線程,等待任務分配。
4.等待客戶端連接:服務器進入等待狀態(tài),等待客戶端連接。
執(zhí)行階段:
1.客戶端連接:當一個客戶端連接到服務器端時,服務器接受連接請求,并將客戶端 Socket 指派到一個工作線程。
2.工作線程處理任務:工作線程從任務隊列中獲取任務,并執(zhí)行任務,處理客戶端請求。
3.任務完成:當工作線程完成任務后,將任務返回給線程池,并等待下一個任務分配。
4.斷開連接:當客戶端下線時,服務器端斷開連接,釋放資源。
Socket 多線程的實現(xiàn)方法
在 Linux 環(huán)境下,我們可以使用 C/C++ 等編程語言實現(xiàn) Socket 多線程。下面是一個基于 C++ 實現(xiàn) Socket 多線程的示例:
“`c++
class Thread {
public:
Thread();
virtual ~Thread();
virtual void Run();
void Start();
void Stop();
void Join();
bool IsRunning() const;
pthread_t GetThreadId() const;
protected:
bool m_running;
pthread_t m_threadId;
static void* StartThread(void* arg);
};
class ThreadPool {
public:
ThreadPool(int size);
~ThreadPool();
void AddTask(Task* task);
private:
int m_size;
bool m_stop;
pthread_mutex_t m_mutex;
pthread_cond_t m_cond;
std::vector m_tasks;
std::vector m_threads;
void Initialize();
void Finalize();
void CreateThreads();
void DestroyThreads();
};
class Task {
public:
virtual void Run() = 0;
};
class SocketTask : public Task {
public:
SocketTask(int sockfd);
virtual ~SocketTask();
virtual void Run();
private:
int m_sockfd;
};
“`
上述代碼中,我們定義了三個類:Thread、Task 和 ThreadPool。Thread 類是一個封裝了 POSIX 線程的類,它可以啟動一個新線程,并執(zhí)行線程回調(diào)函數(shù);Task 類是一個封裝了執(zhí)行任務的類,它必須繼承 Run() 函數(shù);ThreadPool 類是一個封裝了一個線程池的類,它可以添加任務、初始化線程池、創(chuàng)建多個工作線程等。
在實際項目中如何運用 Socket 多線程技術(shù)
在實際項目中,我們可以通過以下步驟來運用 Socket 多線程技術(shù):
1.在服務器端創(chuàng)建 Socket,并監(jiān)聽指定端口,等待客戶端連接。
2.當客戶端連接服務器時,將客戶端 Socket 分配給一個工作線程。
3.工作線程從任務隊列中獲取任務,并執(zhí)行任務,處理客戶端請求。
4.當客戶端下線時,服務器端斷開連接,釋放資源。
通過 Socket 多線程技術(shù),我們可以提高服務器端的并發(fā)處理能力,加快網(wǎng)絡應用的響應速度,同時提高系統(tǒng)的可靠性和穩(wěn)定性。
Socket 多線程的優(yōu)勢和限制
Socket 多線程技術(shù)的優(yōu)勢和限制如下:
優(yōu)勢:
1.提高應用程序的性能和效率:多線程可以并發(fā)執(zhí)行任務,提高應用程序的性能和效率,減少響應時間。
2.提高并發(fā)連接能力:通過多線程技術(shù),服務器可以處理多個連接,提高應用程序的并發(fā)連接能力。
3.實現(xiàn)靈活:線程池可以動態(tài)管理線程,保證了應用程序的靈活性。
限制:
1.線程安全問題:由于多個線程同時操作共享數(shù)據(jù),可能會產(chǎn)生線程安全問題。
2.資源占用問題:多線程需要占用更多的系統(tǒng)資源,包括 CPU、內(nèi)存資源等,可能會導致系統(tǒng)資源緊張。
3.實現(xiàn)復雜:線程池需要對多個線程進行管理,實現(xiàn)起來較為復雜。
成都網(wǎng)站建設公司-創(chuàng)新互聯(lián)為您提供網(wǎng)站建設、網(wǎng)站制作、網(wǎng)頁設計及定制高端網(wǎng)站建設服務!
socket編程在windows和linux下的區(qū)別
下面大概分幾個方面進行羅列:
Linux要包含
#include
#include
#include
#include
等頭文件,而windows下則是包含
#include
。
Linux中socket為整形,Windows中為一個SOCKET。
Linux中關(guān)閉socket為close,Windows中為closesocket。
Linux中有變量socklen_t,Windows中直接為int。
因為linux中的socket與普通的fd一樣,所以可以在TCP的socket中,發(fā)送與接收數(shù)據(jù)時,直接使用read和write。而windows只能使用recv和send。
設置socet選項,比如設置socket為非阻塞的。Linux下為
flag = fcntl (fd, F_GETFL);
fcntl (fd, F_SETFL, flag | O_NONBLOCK);
,Windows下為
flag = 1;
ioctlsocket (fd, FIONBIO, (unsigned long *) &flag);
。
當非阻塞socket的TCP連接正在進行時,Linux的錯誤號為EINPROGRESS,Windows的錯誤號為WSAEWOULDBLOCK。
file
Linux下面,文件換行是春攜仔”\n”,而windows下面是”\r\n”。
Linux下面,目錄分隔符是”/”,而windows下面是”\”。
Linux與Windows下面,均可以使用stat調(diào)用來查詢文件信息。但是,Linux只支持2G大小,而Windows只支持4G大小。為了支持更大的文件查詢,可以在隱猜Linux環(huán)境下加
_FILE_OFFSET_BITS=64定義,在Windows下面使用_stat64調(diào)用,入?yún)閟truct __stat64。
Linux中可根據(jù)stat的st_mode判斷文件類型,有S_ISREG、S_ISDIR等宏。Windows中沒有,需要自己定義相應的宏,如
#define S_ISREG(m) (((m) &) == ())
#define S_ISDIR(m) (((m) &) == ())
Linux中刪除文件是unlink,Windows中為DeleteFile。
time
Linux中,time_t結(jié)構(gòu)是長整形。而windows中,time_t結(jié)構(gòu)是64位的整形。如果要在windows始time_t為32位無符號整形,可以加宏定義,_USE_32BIT_TIME_T。
Linux中,sleep的單位為秒。Windows中,Sleep的單位為毫秒。即,Linux下sleep (1),在Windows環(huán)境下則需要Sleep (1000)。
Windows中的timecmp宏,不支持大于等于或者小于等于。
Windows中沒有struct timeval結(jié)構(gòu)的加減宏可以使用,需要手動定義:
#define MICROSECONDS (1000 * 1000)
#define timeradd(t1, t2, t3) do { \
(t3)->tv_sec = (t1)->tv_sec + (t2)->tv_sec; \
(t3)->tv_usec = (t1)->tv_usec + (t2)->tv_usec % MICROSECONDS;\
if ((t1)->tv_usec + (t2)->tv_usec > MICROSECONDS) (t3)->tv_sec ++;\
扒汪 } while (0)
#define timersub(t1, t2, t3) do { \
(t3)->tv_sec = (t1)->tv_sec – (t2)->tv_sec; \
(t3)->tv_usec = (t1)->tv_usec – (t2)->tv_usec; \
if ((t1)->tv_usec – (t2)->tv_usec tv_usec –, (t3)->tv_usec += MICROSECONDS; \
} while (0)
調(diào)用進程
Linux下可以直接使用system來調(diào)用外部程序。Windows更好使用WinExec,因為WinExec可以支持是打開還是隱藏程序窗口。用WinExec的第二個入?yún)⒅该鳎?/p>
SW_SHOW/SW_HIDE。
雜項
Linux為srandom和random函數(shù),Windows為srand和rand函數(shù)。
Linux為snprintf,Windows為_snprintf。
同理,Linux中的strcasecmp,Windows為_stricmp。
錯誤處理
Linux下面,通常使用全局變量errno來表示函數(shù)執(zhí)行的錯誤號。Windows下要使用GetLastError ()調(diào)用來取得。
Linux環(huán)境下僅有的
這些函數(shù)或者宏,Windows中完全沒有,需要用戶手動實現(xiàn)。
atoll
long long
atoll (const char *p)
{
int minus = 0;
long long value = 0;
if (*p == ‘-‘)
{
minus ++;
p ++;
}
while (*p >= ‘0’ && *p tv_sec = (long) (t /);
tv->tv_usec = (long) (t %);
}
if (tz)
{
if (!tzflag)
{
_tzset ();
tzflag++;
}
tz->tz_minuteswest = _timezone / 60;
tz->tz_dsttime = _daylight;
}
return 0;
}
編譯相關(guān)
當前函數(shù),Linux用__FUNCTION__表示,Windows用__func__表示。
Socket 編程 windows到Linux代碼移植遇到的問題
1)頭文件
windows下winsock.h/winsock2.h
linux下sys/socket.h
錯誤處理:errno.h
2)初始化
windows下需要用WSAStartup
linux下不需要
3)關(guān)閉socket
windows下closesocket(…)
linux下close(…)
4)類型
windows下SOCKET
linux下int
如我用到的一些宏:
#ifdef WIN32
typedef int socklen_t;
typedef int ssize_t;
#endif
#ifdef __LINUX__
typedef int SOCKET;
typedef unsigned char BYTE;
typedef unsigned long DWORD;
#define FALSE 0
#define SOCKET_ERROR (-1)
#endif
5)獲取錯誤碼
windows下getlasterror()/WSAGetLastError()
linux下errno變量
6)設置非阻塞
windows下ioctlsocket()
linux下fcntl()
7)send函數(shù)最后一個參數(shù)
windows下一般設置為0
linux下更好設置為MSG_NOSIGNAL,如果不設置,在發(fā)送出錯后有可 能會導致程序退出。
8)毫秒級時間獲取
windows下GetTickCount()
linux下gettimeofday()
3、多線程
多線程: (win)process.h –〉(linux)pthread.h
_beginthread –> pthread_create
_endthread –> pthread_exit
windows與linux平臺使用的socket均繼承自Berkeley socket(rfc3493),他們都支持select I/O模型,均支持使用getaddrinfo與getnameinfo實現(xiàn)協(xié)議無關(guān)編程。但存在細微差別,
主要有:
頭文件及類庫。windows使用winsock2.h(需要在windows.h前包含),并要鏈接庫ws2_32.lib;linux使用netinet/in.h, netdb.h等。
windows下在使用socket之前與之后要分別使用WSAStartup與WSAClean。
關(guān)閉socket,windows使用closesocket,linux使用close。
send*與recv*函數(shù)參數(shù)之socket長度的類型,windows為int,linux為socklen_t,可預編譯指令中處理這一差異,當平臺為windows時#define socklen_t unsigned int。
select函數(shù)之一個參數(shù),windows忽略該參數(shù),linux下該參數(shù)表示中socket的上限值,一般設為sockfd(需select的socket) + 1。
windows下socket函數(shù)返回值類型為SOCKET(unsigned int),其中發(fā)生錯誤時返回INVALID_SOCKET(0),linux下socket函數(shù)返回值類型int, 發(fā)生錯誤時返回-1。
另外,如果綁定本機回環(huán)地址,windows下sendto函數(shù)可以通過,linux下sendto回報錯:errno=22, Invalid arguement。一般情況下均綁定通配地址。
轉(zhuǎn)載jlins
下面大概分幾個方面進行羅列:
Linux要包含
#include
#include
#include
#include
等頭文件,而windows下則是包含
#include
。
Linux中socket為整形,Windows中為一個SOCKET。
Linux中關(guān)閉socket為close,Windows中為closesocket。
Linux中有變量socklen_t,Windows中直接為int。
因為linux中的socket與普通的fd一樣,所以可以在TCP的socket中,發(fā)送與接收數(shù)據(jù)時,直接使用read和write。而windows只能使用recv和send。
設置socet選項,比如設置socket為非阻塞的。Linux下為
flag = fcntl (fd, F_GETFL);
fcntl (fd, F_SETFL, flag | O_NONBLOCK);
,Windows下為
flag = 1;
ioctlsocket (fd, FIONBIO, (unsigned long *) &flag);
。
當非阻塞socket的TCP連接正在進行時,Linux的錯誤號豎笑為EINPROGRESS,Windows的錯誤號為WSAEWOULDBLOCK。
file
Linux下面,文件換行是”\n”,而windows下面是”\r\n”。
Linux下面,目錄分隔符是”/”,而windows下面是”\”。
Linux與Windows下面,均可以使用stat調(diào)用來查詢文件信息。但是,Linux只支持2G大小,而Windows只支持4G大小。為了支持更大的文件查詢,可以在Linux環(huán)境下加
_FILE_OFFSET_BITS=64定義,在Windows下面使用_stat64調(diào)用,入?yún)閟truct __stat64。
Linux中可根據(jù)stat的st_mode判斷文件類型,有S_ISREG、S_ISDIR等宏。罩圓Windows中沒有,需要自己定義相應的宏,如
#define S_ISREG(m) (((m) &) == ())
#define S_ISDIR(m) (((m) &) == ())
Linux中刪除文件是unlink,Windows中為DeleteFile。
time
Linux中,time_t結(jié)構(gòu)是長整形。而windows中,time_t結(jié)構(gòu)是64位的整形。如果要在windows始time_t為32位無符號整形,可以加宏定義,_USE_32BIT_TIME_T。
Linux中,sleep的單位為秒。Windows中,Sleep的單位為毫秒。即,Linux下sleep (1),在Windows環(huán)境下則需要Sleep (1000)。
Windows中的timecmp宏,不支持大于等于或者余悶含小于等于。
Windows中沒有struct timeval結(jié)構(gòu)的加減宏可以使用,需要手動定義:
#define MICROSECONDS (1000 * 1000)
#define timeradd(t1, t2, t3) do { \
(t3)->tv_sec = (t1)->tv_sec + (t2)->tv_sec; \
(t3)->tv_usec = (t1)->tv_usec + (t2)->tv_usec % MICROSECONDS;\
if ((t1)->tv_usec + (t2)->tv_usec > MICROSECONDS) (t3)->tv_sec ++;\
} while (0)
#define timersub(t1, t2, t3) do { \
(t3)->tv_sec = (t1)->tv_sec – (t2)->tv_sec; \
(t3)->tv_usec = (t1)->tv_usec – (t2)->tv_usec; \
if ((t1)->tv_usec – (t2)->tv_usec tv_usec –, (t3)->tv_usec += MICROSECONDS; \
} while (0)
調(diào)用進程
Linux下可以直接使用system來調(diào)用外部程序。Windows更好使用WinExec,因為WinExec可以支持是打開還是隱藏程序窗口。用WinExec的第二個入?yún)⒅该?,?/p>
SW_SHOW/SW_HIDE。
雜項
Linux為srandom和random函數(shù),Windows為srand和rand函數(shù)。
Linux為snprintf,Windows為_snprintf。
同理,Linux中的strcasecmp,Windows為_stricmp。
錯誤處理
Linux下面,通常使用全局變量errno來表示函數(shù)執(zhí)行的錯誤號。Windows下要使用GetLastError ()調(diào)用來取得。
Linux環(huán)境下僅有的
這些函數(shù)或者宏,Windows中完全沒有,需要用戶手動實現(xiàn)。
atoll
long long
atoll (const char *p)
{
int minus = 0;
long long value = 0;
if (*p == ‘-‘)
{
minus ++;
p ++;
}
while (*p >= ‘0’ && *p tv_sec = (long) (t /);
tv->tv_usec = (long) (t %);
}
if (tz)
{
if (,tzflag)
{
_tzset ();
tzflag++;
}
tz->tz_minuteswest = _timezone / 60;
tz->tz_dsttime = _daylight;
}
return 0;
}
編譯相關(guān)
當前函數(shù),Linux用__FUNCTION__表示,Windows用__func__表示。
Socket 編程 windows到Linux代碼移植遇到的問題
1)頭文件
windows下winsock.h/winsock2.h
linux下sys/socket.h
錯誤處理:errno.h
2)初始化
windows下需要用WSAStartup
linux下不需要
3)關(guān)閉socket
windows下closesocket(…)
linux下close(…)
4)類型
windows下SOCKET
linux下int
如我用到的一些宏:
#ifdef WIN32
typedef int socklen_t;
typedef int ssize_t;
#endif
#ifdef __LINUX__
typedef int SOCKET;
typedef unsigned char BYTE;
typedef unsigned long DWORD;
#define FALSE 0
#define SOCKET_ERROR (-1)
#endif
5)獲取錯誤碼
windows下getlasterror()/WSAGetLastError()
linux下errno變量
6)設置非阻塞
windows下ioctlsocket()
linux下fcntl()
7)send函數(shù)最后一個參數(shù)
windows下一般設置為0
linux下更好設置為MSG_NOSIGNAL,如果不設置,在發(fā)送出錯后有可 能會導致程序退出。
8)毫秒級時間獲取
windows下GetTickCount()
linux下gettimeofday()
3、多線程
多線程: (win)process.h –〉(linux)pthread.h
_beginthread –> pthread_create
_endthread –> pthread_exit
windows與linux平臺使用的socket均繼承自Berkeley socket(rfc3493),他們都支持select I/O模型,均支持使用getaddrinfo與getnameinfo實現(xiàn)協(xié)議無關(guān)編程。但存在細微差別,
主要有:
頭文件及類庫。windows使用winsock2.h(需要在windows.h前包含),并要鏈接庫ws2_32.lib;linux使用netinet/in.h, netdb.h等。
windows下在使用socket之前與之后要分別使用WSAStartup與WSAClean。
關(guān)閉socket,windows使用closesocket,linux使用close。
send*與recv*函數(shù)參數(shù)之socket長度的類型,windows為int,linux為socklen_t,可預編譯指令中處理這一差異,當平臺為windows時#define socklen_t unsigned int。
select函數(shù)之一個參數(shù),windows忽略該參數(shù),linux下該參數(shù)表示中socket的上限值,一般設為sockfd(需select的socket) + 1。
windows下socket函數(shù)返回值類型為SOCKET(unsigned int),其中發(fā)生錯誤時返回INVALID_SOCKET(0),linux下socket函數(shù)返回值類型int, 發(fā)生錯誤時返回-1。
另外,如果綁定本機回環(huán)地址,windows下sendto函數(shù)可以通過,linux下sendto回報錯:errno=22, Invalid arguement。一般情況下均綁定通配地址。
如何看懂《Linux多線程服務端編程
一:進程和線程
每個進程有自己獨立的地址空間?!霸谕粋€進程”還是“不在同一個進程”是系統(tǒng)功能劃分的重要決策點?!禘rlang程序設計》把進程比喻為人:
每個人有自己的記憶(內(nèi)存),人與人通過談話(消息傳遞)來交流,談話既可以是面談姿并野(同一臺服務器),也可以在里談(不同的服務器,有網(wǎng)絡通信)。面談和談的區(qū)別在于,面談可以立即知道對方是否死了(crash,SIGCHLD),而談只能通過周期性的心跳來判斷對方是否還活著。
有了這些比喻,設計分布式系統(tǒng)時可以采取“角色扮演”,團隊里的幾個人各自扮演一個進程,人的角色由進程的代碼決定(管登錄的、管消息分發(fā)的、管買賣的等等)。每個人有自己的記憶,但不知道別人的記憶,要想知道別人的看法,只能通過交談(暫不考慮共享內(nèi)存這種IPC)。然后就可以思考:跡喊
·容錯:萬一有人突然死了
·擴容:新人中途加進來
·負載均衡:把甲的活兒挪給乙做
·退休:甲要修復bug,先別派新任務,等他做完手上的事情就把他重啟
等等各種場景,十分便利。
線程的特點是共享地址空間,從而可以高效地共享數(shù)據(jù)。一臺機器上的多個進程能高效地共享代碼段(操作系統(tǒng)可以映射為同樣的物理內(nèi)存),但不能共享數(shù)據(jù)。如果多個進程大量共享內(nèi)存,等于是把多進程程序當成多線程來寫,掩耳盜鈴。
“多線程”的價值,我認為是為了更好地發(fā)揮多核處理器(multi-cores)的效能。在單核時代,多線程沒有多大價值(個人想法:如果要完成的任務是CPU密集型的,那多線程沒有優(yōu)勢,甚至因為線程切換的開銷,多線程反而更慢;如果要完成的任務既有CPU計算,又有磁盤或網(wǎng)絡IO,則使用多線程的好處是,當某個線程因為IO而阻塞時,OS可以調(diào)度其他線程執(zhí)行,雖然效率確實要比任務的順序執(zhí)行效率要高,然而,這種類型的任務,可以通過單線程的”non-blocking IO+IO multiplexing”的模型(事件驅(qū)動)來提高效率,采用多線程的方式,帶來的可能僅僅是編程上的簡單而已)。Alan Cox說過:”A computer is a state machine.Threads are for people who can’t program state machines.”(計算機是一臺狀態(tài)機。線程是給那些不能編寫狀態(tài)機程序的人準備的)如果只有一塊CPU、一個執(zhí)行單元,那么確實如Alan Cox所說,按狀態(tài)機的思路去寫程序是最蔽謹高效的。
二:單線程服務器的常用編程模型
據(jù)我了解,在高性能的網(wǎng)絡程序中,使用得最為廣泛的恐怕要數(shù)”non-blocking IO + IO multiplexing”這種模型,即Reactor模式。
在”non-blocking IO + IO multiplexing”這種模型中,程序的基本結(jié)構(gòu)是一個事件循環(huán)(event loop),以事件驅(qū)動(event-driven)和事件回調(diào)的方式實現(xiàn)業(yè)務邏輯:
view plain copy
//代碼僅為示意,沒有完整考慮各種情況
while(!done)
{
int timeout_ms = max(1000, getNextTimedCallback());
int retval = poll(fds, nfds, timeout_ms);
if (retval0){
處理IO事件,回調(diào)用戶的IO event handler
}
}
}
這里select(2)/poll(2)有伸縮性方面的不足(描述符過多時,效率較低),Linux下可替換為epoll(4),其他操作系統(tǒng)也有對應的高性能替代品。
Reactor模型的優(yōu)點很明顯,編程不難,效率也不錯。不僅可以用于讀寫socket,連接的建立(connect(2)/accept(2)),甚至DNS解析都可以用非阻塞方式進行,以提高并發(fā)度和吞吐量(throughput),對于IO密集的應用是個不錯的選擇。lighttpd就是這樣,它內(nèi)部的fdevent結(jié)構(gòu)十分精妙,值得學習。
基于事件驅(qū)動的編程模型也有其本質(zhì)的缺點,它要求事件回調(diào)函數(shù)必須是非阻塞的。對于涉及網(wǎng)絡IO的請求響應式協(xié)議,它容易割裂業(yè)務邏輯,使其散布于多個回調(diào)函數(shù)之中,相對不容易理解和維護。
三:多線程服務器的常用編程模型
大概有這么幾種:
a:每個請求創(chuàng)建一個線程,使用阻塞式IO操作。在Java 1.4引人NIO之前,這是Java網(wǎng)絡編程的推薦做法??上炜s性不佳(請求太多時,操作系統(tǒng)創(chuàng)建不了這許多線程)。
b:使用線程池,同樣使用阻塞式IO操作。與第1種相比,這是提高性能的措施。
c:使用non-blocking IO + IO multiplexing。即Java NIO的方式。
d:Leader/Follower等高級模式。
在默認情況下,我會使用第3種,即non-blocking IO + one loop per thread模式來編寫多線程C++網(wǎng)絡服務程序。
1:one loop per thread
此種模型下,程序里的每個IO線程有一個event loop,用于處理讀寫和定時事件(無論周期性的還是單次的)。代碼框架跟“單線程服務器的常用編程模型”一節(jié)中的一樣。
libev的作者說:
One loop per thread is usually a good model. Doing this is almost never wrong, some times a better-performance model exists, but it is always a good start.
這種方式的好處是:
a:線程數(shù)目基本固定,可以在程序啟動的時候設置,不會頻繁創(chuàng)建與銷毀。
b:可以很方便地在線程間調(diào)配負載。
c:IO事件發(fā)生的線程是固定的,同一個TCP連接不必考慮事件并發(fā)。
Event loop代表了線程的主循環(huán),需要讓哪個線程干活,就把timer或IO channel(如TCP連接)注冊到哪個線程的loop里即可:對實時性有要求的connection可以單獨用一個線程;數(shù)據(jù)量大的connection可以獨占一個線程,并把數(shù)據(jù)處理任務分攤到另幾個計算線程中(用線程池);其他次要的輔助性connections可以共享一個線程。
比如,在dbproxy中,一個線程用于專門處理客戶端發(fā)來的管理命令;一個線程用于處理客戶端發(fā)來的MySQL命令,而與后端數(shù)據(jù)庫通信執(zhí)行該命令時,是將該任務分配給所有事件線程處理的。
對于non-trivial(有一定規(guī)模)的服務端程序,一般會采用non-blocking IO + IO multiplexing,每個connection/acceptor都會注冊到某個event loop上,程序里有多個event loop,每個線程至多有一個event loop。
多線程程序?qū)vent loop提出了更高的要求,那就是“線程安全”。要允許一個線程往別的線程的loop里塞東西,這個loop必須得是線程安全的。
在dbproxy中,線程向其他線程分發(fā)任務,是通過管道和隊列實現(xiàn)的。比如主線程accept到連接后,將表示該連接的結(jié)構(gòu)放入隊列,并向管道中寫入一個字節(jié)。計算線程在自己的event loop中注冊管道的讀事件,一旦有數(shù)據(jù)可讀,就嘗試從隊列中取任務。
2:線程池
不過,對于沒有IO而光有計算任務的線程,使用event loop有點浪費。可以使用一種補充方案,即用blocking queue實現(xiàn)的任務隊列:
view plain copy
typedef boost::functionFunctor;
BlockingQueue taskQueue; //線程安全的全局阻塞隊列
//計算線程
void workerThread()
{
while (running) //running變量是個全局標志
{
Functor task = taskQueue.take(); //this blocks
task(); //在產(chǎn)品代碼中需要考慮異常處理
}
}
// 創(chuàng)建容量(并發(fā)數(shù))為N的線程池
int N = num_of_computing_threads;
for (int i = 0; i task = boost::bind(&Foo::calc,&foo);
taskQueue.post(task);
除了任務隊列,還可以用BlockingQueue實現(xiàn)數(shù)據(jù)的生產(chǎn)者消費者隊列,即T是數(shù)據(jù)類型而非函數(shù)對象,queue的消費者從中拿到數(shù)據(jù)進行處理。其實本質(zhì)上是一樣的。
3:總結(jié)
總結(jié)而言,我推薦的C++多線程服務端編程模式為:one (event) loop per thread + thread pool:
event loop用作IO multiplexing,配合non-blockingIO和定時器;
thread pool用來做計算,具體可以是任務隊列或生產(chǎn)者消費者隊列。
以這種方式寫服務器程序,需要一個優(yōu)質(zhì)的基于Reactor模式的網(wǎng)絡庫來支撐,muduo正是這樣的網(wǎng)絡庫。比如dbproxy使用的是libevent。
程序里具體用幾個loop、線程池的大小等參數(shù)需要根據(jù)應用來設定,基本的原則是“阻抗匹配”(解釋見下),使得CPU和IO都能高效地運作。所謂阻抗匹配原則:
如果池中線程在執(zhí)行任務時,密集計算所占的時間比重為 P (0 就能立刻列出用到某服務的客戶端地址(Foreign Address列),然后在客戶端的機器上用netstat或lsof命令找出是哪個進程發(fā)起的連接。TCP短連接和UDP則不具備這一特性。二是通過接收和發(fā)送隊列的長度也較容易定位網(wǎng)絡或程序故障。在正常運行的時候,netstat打印的Recv-Q和Send-Q都應該接近0,或者在0附近擺動。如果Recv-Q保持不變或持續(xù)增加,則通常意味著服務進程的處理速度變慢,可能發(fā)生了死鎖或阻塞。如果Send-Q保持不變或持續(xù)增加,有可能是對方服務器太忙、來不及處理,也有可能是網(wǎng)絡中間某個路由器或交換機故障造成丟包,甚至對方服務器掉線,這些因素都可能表現(xiàn)為數(shù)據(jù)發(fā)送不出去。通過持續(xù)監(jiān)控Recv-Q和Send-Q就能及早預警性能或可用性故障。以下是服務端線程阻塞造成Recv-Q和客戶端Send-Q激增的例子:
view plain copy
$netstat -tn
Proto Recv-Q Send-Q Local Address Foreign
tcp.0.0.10:.0.0.10:#服務端連接
tcp.0.0.10:.0.0.10:#客戶端連接
tcp.0.0.10:.0.0.4:
五:多線程服務器的適用場合
如果要在一臺多核機器上提供一種服務或執(zhí)行一個任務,可用的模式有:
a:運行一個單線程的進程;
b:運行一個多線程的進程;
c:運行多個單線程的進程;
d:運行多個多線程的進程;
考慮這樣的場景:如果使用速率為50MB/s的數(shù)據(jù)壓縮庫,進程創(chuàng)建銷毀的開銷是800微秒,線程創(chuàng)建銷毀的開銷是50微秒。如何執(zhí)行壓縮任務?
如果要偶爾壓縮1GB的文本文件,預計運行時間是20s,那么起一個進程去做是合理的,因為進程啟動和銷毀的開銷遠遠小于實際任務的耗時。
如果要經(jīng)常壓縮500kB的文本數(shù)據(jù),預計運行時間是10ms,那么每次都起進程 似乎有點浪費了,可以每次單獨起一個線程去做。
如果要頻繁壓縮10kB的文本數(shù)據(jù),預計運行時間是200微秒,那么每次起線程似 乎也很浪費,不如直接在當前線程搞定。也可以用一個線程池,每次把壓縮任務交給線程池,避免阻塞當前線程(特別要避免阻塞IO線程)。
由此可見,多線程并不是萬靈丹(silver bullet)。
1:必須使用單線程的場合
據(jù)我所知,有兩種場合必須使用單線程:
a:程序可能會fork(2);
實際編程中,應該保證只有單線程程序能進行fork(2)。多線程程序不是不能調(diào)用fork(2),而是這么做會遇到很多麻煩:
fork一般不能在多線程程序中調(diào)用,因為Linux的fork只克隆當前線程的thread of control,不可隆其他線程。fork之后,除了當前線程之外,其他線程都消失了。
這就造成一種危險的局面。其他線程可能正好處于臨界區(qū)之內(nèi),持有了某個鎖,而它突然死亡,再也沒有機會去解鎖了。此時如果子進程試圖再對同一個mutex加鎖,就會立即死鎖。因此,fork之后,子進程就相當于處于signal handler之中(因為不知道調(diào)用fork時,父進程中的線程此時正在調(diào)用什么函數(shù),這和信號發(fā)生時的場景一樣),你不能調(diào)用線程安全的函數(shù)(除非它是可重入的),而只能調(diào)用異步信號安全的函數(shù)。比如,fork之后,子進程不能調(diào)用:
malloc,因為malloc在訪問全局狀態(tài)時幾乎肯定會加鎖;
任何可能分配或釋放內(nèi)存的函數(shù),比如snprintf;
任何Pthreads函數(shù);
printf系列函數(shù),因為其他線程可能恰好持有stdout/stderr的鎖;
除了man 7 signal中明確列出的信號安全函數(shù)之外的任何函數(shù)。
因此,多線程中調(diào)用fork,唯一安全的做法是fork之后,立即調(diào)用exec執(zhí)行另一個程序,徹底隔斷子進程與父進程的聯(lián)系。
在多線程環(huán)境中調(diào)用fork,產(chǎn)生子進程后。子進程內(nèi)部只存在一個線程,也就是父進程中調(diào)用fork的線程的副本。
使用fork創(chuàng)建子進程時,子進程通過繼承整個地址空間的副本,也從父進程那里繼承了所有互斥量、讀寫鎖和條件變量的狀態(tài)。如果父進程中的某個線程占有鎖,則子進程同樣占有這些鎖。問題是子進程并不包含占有鎖的線程的副本,所以子進程沒有辦法知道它占有了哪些鎖,并且需要釋放哪些鎖。
盡管Pthread提供了pthread_atfork函數(shù)試圖繞過這樣的問題,但是這回使得代碼變得混亂。因此《Programming With Posix Threads》一書的作者說:”Avoid using fork in threaded code except where the child process will immediately exec a new program.”。
b:限制程序的CPU占用率;
這個很容易理解,比如在一個8核的服務器上,一個單線程程序即便發(fā)生busy-wait,占滿1個core,其CPU使用率也只有12.5%,在這種最壞的情況下,系統(tǒng)還是有87.5%的計算資源可供其他服務進程使用。
linux socket多線程的介紹就聊到這里吧,感謝你花時間閱讀本站內(nèi)容,更多關(guān)于linux socket多線程,「探索 Linux 網(wǎng)絡編程:socket 多線程」,socket編程在windows和linux下的區(qū)別,如何看懂《Linux多線程服務端編程的信息別忘了在本站進行查找喔。
創(chuàng)新互聯(lián)(cdcxhl.com)提供穩(wěn)定的云服務器,香港云服務器,BGP云服務器,雙線云服務器,高防云服務器,成都云服務器,服務器托管。精選鉅惠,歡迎咨詢:028-86922220。
網(wǎng)頁名稱:「探索Linux網(wǎng)絡編程:socket多線程」(linuxsocket多線程)
文章轉(zhuǎn)載:http://fisionsoft.com.cn/article/cccgdds.html


咨詢
建站咨詢
