1、linux多線程為什麼單線程執行
主要是兩個問題,
任務調度
和oversubscription。
openmp默認使用的schele是取決於
編譯器
實現的。gcc默認使用schele(dynamic,1),也就是動態調度並且塊大小是1。在你的程序裡面,這種調度是及其低效的,看代碼都能預期到,不太可能比
單線程
快。
動態調度的一種簡單理解方式是,計算任務存在一個任務隊列裡面,你的
for循環
每一個i值對應一個計算任務。每個線程每次提取一批任務,然後計算。「一批」是多少呢?就是前面說的塊大小,在你的程序裡面是1。提取任務需要什麼操作呢?因為這個任務隊列是多線程共享的,提取任務前必須加鎖,讀取一批,從隊列中移除,然後解鎖。說到這里,你應該已經知道原因了。
你的線程一次只提取一次計算任務,這個任務還完成得很快。然後所有的16個線程排著隊,逐個去加鎖,搶任務,然後解鎖讓其它線程繼續搶。然後馬上發現這個任務很快,又要重新去排隊等任務,始終處於飢餓狀態。注意排隊的時候可能也是要佔cpu的,因為使用了busy
wait,所以可能你看來十六核滿負荷,但是其實啥也沒干。
我的建議就是,使用static
schele,或者增加dynamic
schele的塊大小,比如1024,取決於你循環多少次。一般
如果你知道
每次循環的執行時間基本都是一樣,並且是專用伺服器設置好affinity,無其它負荷無oversubscription無numa問題的話,static
schele會是個比較好的選擇。這樣每個線程做哪些任務只需要進行一次分配,最小化了openmp本身的消耗。
還有一個非常重要的問題!
數值計算
不要使用
cpu超線程
!cpu的超線程對於數值計算基本是有害無益的,線程數不要大於實際核數,否則就是oversubscription。你這已經是非常嚴重的oversubscription了。數值計算專用的話,建議直接關閉伺服器bios裡面的超線程選項。
2、linux單進程如何實現多核cpu多線程分配?
linux下的單進程多線程的程序,要實現每個線程平均分配到多核cpu,主要有2個方法
1:利用linux系統自己的線程切換機制,linux有一個服務叫做irqbalance,這個服務是linux系統自帶的,默認會啟動,這個服務的作用就是把多線程平均分配到CPU的每個核上面,只要這個服務不停止,多線程分配就可以自己實現。但是要注意,如果線程函數內部的有某個循環,且該循環內沒有任何系統調用的話,可能會導致這個線程的CPU時間無法被切換出去。也就是占滿CPU現象,此時加個系統調用,例如sleep,線程所佔的CPU時間就可以切換出去了。
2:利用pthread庫自帶的線程親和性設置函數,來設置線程在某個CPU核心上跑,這個需要在程序內部實現。同時注意不要和進程親和性設置搞混淆了
int pthread_setaffinity_np(pthread_t thread, size_t cpusetsize,3、linux多線程服務端編程 看什麼書
這本書主要分享了作者在實現公司內部的分布式服務系統中積累的多線程和專網路編程方屬面的經驗,並介紹了C++ 在編寫這種分布式系統的服務端程序時的功能取捨與注意事項,書中的很多決策(design decision)是在這一應用場景下做出的。
這本書沒有細談分布式系統的設計,只在第9章列舉了分布式系統的挑戰及其對程序設計(服務端編程)的影響,例如可靠性、可維護性等。
4、陳碩 linux 多線程伺服器編程 上的例子怎樣編譯
Linux多線程程序編譯時記得加上一個-pthread的編譯參數就可以了,不加這個參數就通不過。
5、Linux多線程編程問題?
等一下吧,等我大便完了給你寫,奶奶的,誰讓我心腸好呢!
伺服器端的代碼
編譯是gcc加上-lpthread
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
static void *
listener (void *ip_srv, unsigned short port_srv);
static void *
server_thread (void *fd);
char *
attime ()
{
time_t t;
time (&t);
return (ctime (&t));
}
int
main (int argc, char **argv)
{
char *ip_srv = "0.0.0.0";
int port_srv = 43567;
int i = 1;
for (i = 1; i < argc; i += 2)
{
if (argv[i][0] != '-')
{
fprintf (stderr, "Usage:%s -S ip_srv "
"-P port_srv " "-p port_clt " "-T sleep_time\n", argv[0]);
fprintf (stderr,
"'-S ip_srv'------Select ip for this server, default ip is 0.0.0.0.\n"
"'-P port_srv'----Select listening port for this server, default port_srv is 43567.\n");
exit (1);
}
else
{
switch (argv[i][1])
{
case ('S'):
ip_srv = argv[i + 1];
break;
case ('P'):
port_srv = atoi (argv[i + 1]);
if (port_srv >= 0x0000ffff)
{
fprintf (stderr,
"'-P': Too big number for server's port.\nTry number in [2000 ~ 60000]\n");
exit (1);
}
break;
default:
fprintf (stderr, "Usage:%s -S ip_srv "
"-P port_srv " "-p port_clt " "-T sleep_time\n", argv[0]);
fprintf (stderr,
"'-S ip_srv'------Select ip for this server, default ip is 0.0.0.0.\n"
"'-P port_srv'----Select listening port for this server, default port_srv is 43567.\n");
exit (1);
}
}
}
printf ("server ip : %s\n", ip_srv);
printf ("server port : %d\n", port_srv);
listener(ip_srv, port_srv);
return 0;
}
void *
listener (void *ip_srv, unsigned short port_srv)
{
int err;
int listenfd, connectfd;
struct sockaddr_in adr_srv, adr_clt;
int len_adr;
pthread_t tid;
len_adr = sizeof adr_srv;
memset (&adr_srv, 0, len_adr);
adr_srv.sin_family = AF_INET;
adr_srv.sin_port = htons (port_srv);
adr_srv.sin_addr.s_addr = inet_addr ((char *) ip_srv);
if (INADDR_NONE == adr_srv.sin_addr.s_addr)
{
perror ("bad address in listener thread");
exit (1);
}
if ((listenfd = socket (AF_INET, SOCK_STREAM, 0)) == -1)
{
perror ("in listener thread::socket()");
exit(1);
}
if ((err = bind (listenfd, (struct sockaddr *) &adr_srv, len_adr)) == -1)
{
printf ("Address is still in use, try later!\n");
perror ("in listener thread::bind()");
exit (1);
}
if ((err = listen (listenfd, 10)) == -1)
{
perror ("in listener thread::listen()");
exit (1);
}
while (1)
{
len_adr = sizeof adr_clt;
if ((connectfd =
accept (listenfd, (struct sockaddr *) &adr_clt, &len_adr)) != -1)
{
printf ("\nat %sAcceppt client from %s!\n", attime (),
inet_ntoa (adr_clt.sin_addr));
pthread_create (&tid, NULL, server_thread, (void *) connectfd);
}
}
close (listenfd);
fprintf (stderr, "Damn it! I am down!!\n");
exit (-1);
}
void *
server_thread (void *fd)
{
char msg[128];
struct sockaddr_in adr_clt;
int len_adr;
int sockfd = (int) fd;
int err = 0;
if ((err = pthread_detach (pthread_self ())) != 0)
{
close (sockfd);
perror ("Can't detach server thread.");
pthread_exit ((void*)1);
}
getsockname (sockfd, (struct sockaddr *) &adr_clt, &len_adr);
while(1)
{
memset (msg, 0, sizeof(msg));
if ((err = recv (sockfd, msg, sizeof(msg), MSG_NOSIGNAL)) <= 0)
{
close (sockfd);
if (err == 0)
fprintf (stderr, "client %s close the socket.\n", inet_ntoa (adr_clt.sin_addr));
else
perror ("in server_thread::recv()");
pthread_exit ((void*)1);
}
if (0 == strncmp(msg, "\n", sizeof(msg)))
strncpy (msg, "[null message]", sizeof(msg)-1);
fprintf (stdout, "\n--at %ssmessage from %s:\n%s\n",
attime (), inet_ntoa (adr_clt.sin_addr), msg);
if (0 == strncmp(msg, "exit\n", sizeof(msg)))
break;
}
close (sockfd);
return ((void *) 0);
}
客戶端的
#include <stdio.h>
#include <time.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>
#include <assert.h>
static void
print_usage (void)
{
fprintf (stderr,
"'-s srv_ip'------Select ip for this server, default ip is 0.0.0.0.\n"
"'-p srv_port'----Select server port, default port_clt is 43567.\n");
}
static void
l (const char *on_what)
{
fputs (strerror (errno), stderr);
fputs (": ", stderr);
fputs (on_what, stderr);
fputc ('\n', stderr);
exit (-1);
}
int
main (int argc, char **argv)
{
int err;
int i;
int port = 43567;
char *srv_ip = "0.0.0.0";
struct sockaddr_in adr_srvr; /* AF_INET */
int len_inet; /* length */
int sockfd; /* Socket */
char msg[128];
for (i = 1; i < argc;)
{
if (argv[i][0] != '-')
{
fprintf (stderr, "Usage:%s -s srv_ip "
"-p srv_port " "-l layer" "-S" "-d\n", argv[0]);
print_usage ();
exit (1);
}
else
{
switch (argv[i][1])
{
case ('s'):
srv_ip = argv[i + 1];
i += 2;
break;
case ('p'):
port = atoi (argv[i + 1]);
i += 2;
break;
default:
fprintf (stderr, "Usage:%s -s srv_ip -p srv_port\n", argv[0]);
print_usage ();
exit (1);
}
}
}
/*
* Create a server socket address:
*/
len_inet = sizeof adr_srvr;
memset (&adr_srvr, 0, len_inet);
adr_srvr.sin_family = AF_INET;
adr_srvr.sin_port = htons (port);
adr_srvr.sin_addr.s_addr = inet_addr (srv_ip);
if (adr_srvr.sin_addr.s_addr == INADDR_NONE)
l ("bad address.");
sockfd = socket (AF_INET, SOCK_STREAM, 0);
if (sockfd == -1)
l ("socket()");
/*
* Connect to the server:
*/
printf ("server ip : %s\n", srv_ip);
printf ("server port: %d\n", port);
err = connect (sockfd, (struct sockaddr *) &adr_srvr, len_inet);
if (err == -1)
l ("client: connect(2)");
printf ("CONNECT SERVER SECCESSFULLY!\n");
while (1)
{
printf ("INPUT YOUR MESSAGE: ");
fflush(stdout);
memset(msg, 0, sizeof(msg));
fgets (msg, sizeof(msg), stdin);
if ((err = send (sockfd, msg, 1+strlen(msg), MSG_NOSIGNAL)) < 0)
l ("client: write()");
}
}
6、Linux多線程實現線程間不停的切換
你這個問題很有意思。第一次執行的時候,可以看出,能執行0~6共7次;第二次的時候,從6開始,到5,只有2次了,並且以後都是只有2次。
基於你的描述,我想可以用互斥信號量來做。
1、初始化2個信號量pmutex1(有資源), pmutex2(無資源),初始化gnum=0
2、啟動兩個線程
2.1 線程1
lock_the_mutex_signal(pmutex1); // 上鎖自身線程,首次可執行
while (gnum < 5) {
do_sth(); // 做你的業務邏輯
gnum++; // 增加執行次數
} // end while()
unlock_the_mutex_signal(pmutex2); // 解鎖另一線程
2.2 線程2
lock_the_mutex_signal(pmutex2); // 上鎖自己,首次執行將阻塞,並交出CPU
while (gnum > 5) {
do_sth(); // ...
gnum--; // ...
} // end while()
unlock_the_mutex_signal(pmutex1); // release the lock
PS:如果你不是非常嚴格地(從系統級杜絕不該被執行的線程被調用)要求線程切換的話,這個邏輯應該可以工作。自己沒有試,希望你明白我的思想,如有錯誤,自己再修改一下。
7、如何看懂《Linux多線程服務端編程
一:進程和線程
每個進程有自己獨立的地址空間。「在同一個進程」還是「不在同一個進程」是系統功能劃分的重要決策點。《Erlang程序設計》[ERL]把進程比喻為人:
每個人有自己的記憶(內存),人與人通過談話(消息傳遞)來交流,談話既可以是面談(同一台伺服器),也可以在電話里談(不同的伺服器,有網路通信)。面談和電話談的區別在於,面談可以立即知道對方是否死了(crash,SIGCHLD),而電話談只能通過周期性的心跳來判斷對方是否還活著。
有了這些比喻,設計分布式系統時可以採取「角色扮演」,團隊里的幾個人各自扮演一個進程,人的角色由進程的代碼決定(管登錄的、管消息分發的、管買賣的等等)。每個人有自己的記憶,但不知道別人的記憶,要想知道別人的看法,只能通過交談(暫不考慮共享內存這種IPC)。然後就可以思考:
·容錯:萬一有人突然死了
·擴容:新人中途加進來
·負載均衡:把甲的活兒挪給乙做
·退休:甲要修復bug,先別派新任務,等他做完手上的事情就把他重啟
等等各種場景,十分便利。
線程的特點是共享地址空間,從而可以高效地共享數據。一台機器上的多個進程能高效地共享代碼段(操作系統可以映射為同樣的物理內存),但不能共享數據。如果多個進程大量共享內存,等於是把多進程程序當成多線程來寫,掩耳盜鈴。
「多線程」的價值,我認為是為了更好地發揮多核處理器(multi-cores)的效能。在單核時代,多線程沒有多大價值(個人想法:如果要完成的任務是CPU密集型的,那多線程沒有優勢,甚至因為線程切換的開銷,多線程反而更慢;如果要完成的任務既有CPU計算,又有磁碟或網路IO,則使用多線程的好處是,當某個線程因為IO而阻塞時,OS可以調度其他線程執行,雖然效率確實要比任務的順序執行效率要高,然而,這種類型的任務,可以通過單線程的」non-blocking IO+IO multiplexing」的模型(事件驅動)來提高效率,採用多線程的方式,帶來的可能僅僅是編程上的簡單而已)。Alan Cox說過:」A computer is a state machine.Threads are for people who can』t program state machines.」(計算機是一台狀態機。線程是給那些不能編寫狀態機程序的人准備的)如果只有一塊CPU、一個執行單元,那麼確實如Alan Cox所說,按狀態機的思路去寫程序是最高效的。
二:單線程伺服器的常用編程模型
據我了解,在高性能的網路程序中,使用得最為廣泛的恐怕要數」non-blocking IO + IO multiplexing」這種模型,即Reactor模式。
在」non-blocking IO + IO multiplexing」這種模型中,程序的基本結構是一個事件循環(event loop),以事件驅動(event-driven)和事件回調的方式實現業務邏輯:
[cpp] view plain copy
//代碼僅為示意,沒有完整考慮各種情況
while(!done)
{
int timeout_ms = max(1000, getNextTimedCallback());
int retval = poll(fds, nfds, timeout_ms);
if (retval<0){
處理錯誤,回調用戶的error handler
}else{
處理到期的timers,回調用戶的timer handler
if(retval>0){
處理IO事件,回調用戶的IO event handler
}
}
}
這里select(2)/poll(2)有伸縮性方面的不足(描述符過多時,效率較低),Linux下可替換為epoll(4),其他操作系統也有對應的高性能替代品。
Reactor模型的優點很明顯,編程不難,效率也不錯。不僅可以用於讀寫socket,連接的建立(connect(2)/accept(2)),甚至DNS解析都可以用非阻塞方式進行,以提高並發度和吞吐量(throughput),對於IO密集的應用是個不錯的選擇。lighttpd就是這樣,它內部的fdevent結構十分精妙,值得學習。
基於事件驅動的編程模型也有其本質的缺點,它要求事件回調函數必須是非阻塞的。對於涉及網路IO的請求響應式協議,它容易割裂業務邏輯,使其散布於多個回調函數之中,相對不容易理解和維護。
三:多線程伺服器的常用編程模型
大概有這么幾種:
a:每個請求創建一個線程,使用阻塞式IO操作。在Java 1.4引人NIO之前,這是Java網路編程的推薦做法。可惜伸縮性不佳(請求太多時,操作系統創建不了這許多線程)。
b:使用線程池,同樣使用阻塞式IO操作。與第1種相比,這是提高性能的措施。
c:使用non-blocking IO + IO multiplexing。即Java NIO的方式。
d:Leader/Follower等高級模式。
在默認情況下,我會使用第3種,即non-blocking IO + one loop per thread模式來編寫多線程C++網路服務程序。
1:one loop per thread
此種模型下,程序里的每個IO線程有一個event loop,用於處理讀寫和定時事件(無論周期性的還是單次的)。代碼框架跟「單線程伺服器的常用編程模型」一節中的一樣。
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:線程數目基本固定,可以在程序啟動的時候設置,不會頻繁創建與銷毀。
b:可以很方便地在線程間調配負載。
c:IO事件發生的線程是固定的,同一個TCP連接不必考慮事件並發。
Event loop代表了線程的主循環,需要讓哪個線程幹活,就把timer或IO channel(如TCP連接)注冊到哪個線程的loop里即可:對實時性有要求的connection可以單獨用一個線程;數據量大的connection可以獨佔一個線程,並把數據處理任務分攤到另幾個計算線程中(用線程池);其他次要的輔助性connections可以共享一個線程。
比如,在dbproxy中,一個線程用於專門處理客戶端發來的管理命令;一個線程用於處理客戶端發來的MySQL命令,而與後端資料庫通信執行該命令時,是將該任務分配給所有事件線程處理的。
對於non-trivial(有一定規模)的服務端程序,一般會採用non-blocking IO + IO multiplexing,每個connection/acceptor都會注冊到某個event loop上,程序里有多個event loop,每個線程至多有一個event loop。
多線程程序對event loop提出了更高的要求,那就是「線程安全」。要允許一個線程往別的線程的loop里塞東西,這個loop必須得是線程安全的。
在dbproxy中,線程向其他線程分發任務,是通過管道和隊列實現的。比如主線程accept到連接後,將表示該連接的結構放入隊列,並向管道中寫入一個位元組。計算線程在自己的event loop中注冊管道的讀事件,一旦有數據可讀,就嘗試從隊列中取任務。
2:線程池
不過,對於沒有IO而光有計算任務的線程,使用event loop有點浪費。可以使用一種補充方案,即用blocking queue實現的任務隊列:
[cpp] view plain copy
typedef boost::function<void()>Functor;
BlockingQueue<Functor> taskQueue; //線程安全的全局阻塞隊列
//計算線程
void workerThread()
{
while (running) //running變數是個全局標志
{
Functor task = taskQueue.take(); //this blocks
task(); //在產品代碼中需要考慮異常處理
}
}
// 創建容量(並發數)為N的線程池
int N = num_of_computing_threads;
for (int i = 0; i < N; ++i)
{
create_thread(&workerThread); //啟動線程
}
//向任務隊列中追加任務
Foo foo; //Foo有calc()成員函數
boost::function<void()> task = boost::bind(&Foo::calc,&foo);
taskQueue.post(task);
除了任務隊列,還可以用BlockingQueue<T>實現數據的生產者消費者隊列,即T是數據類型而非函數對象,queue的消費者從中拿到數據進行處理。其實本質上是一樣的。
3:總結
總結而言,我推薦的C++多線程服務端編程模式為:one (event) loop per thread + thread pool:
event loop用作IO multiplexing,配合non-blockingIO和定時器;
thread pool用來做計算,具體可以是任務隊列或生產者消費者隊列。
以這種方式寫伺服器程序,需要一個優質的基於Reactor模式的網路庫來支撐,muo正是這樣的網路庫。比如dbproxy使用的是libevent。
程序里具體用幾個loop、線程池的大小等參數需要根據應用來設定,基本的原則是「阻抗匹配」(解釋見下),使得CPU和IO都能高效地運作。所謂阻抗匹配原則:
如果池中線程在執行任務時,密集計算所佔的時間比重為 P (0 < P <= 1),而系統一共有 C 個 CPU,為了讓這 C 個 CPU 跑滿而又不過載,線程池大小的經驗公式 T = C/P。(T 是個 hint,考慮到 P 值的估計不是很准確,T 的最佳值可以上下浮動 50%)
以後我再講這個經驗公式是怎麼來的,先驗證邊界條件的正確性。
假設 C = 8,P = 1.0,線程池的任務完全是密集計算,那麼T = 8。只要 8 個活動線程就能讓 8 個 CPU 飽和,再多也沒用,因為 CPU 資源已經耗光了。
假設 C = 8,P = 0.5,線程池的任務有一半是計算,有一半等在 IO 上,那麼T = 16。考慮操作系統能靈活合理地調度 sleeping/writing/running 線程,那麼大概 16 個「50%繁忙的線程」能讓 8 個 CPU 忙個不停。啟動更多的線程並不能提高吞吐量,反而因為增加上下文切換的開銷而降低性能。
如果 P < 0.2,這個公式就不適用了,T 可以取一個固定值,比如 5*C。
另外,公式里的 C 不一定是 CPU 總數,可以是「分配給這項任務的 CPU 數目」,比如在 8 核機器上分出 4 個核來做一項任務,那麼 C=4。
四:進程間通信只用TCP
Linux下進程間通信的方式有:匿名管道(pipe)、具名管道(FIFO)、POSIX消息隊列、共享內存、信號(signals),以及Socket。同步原語有互斥器(mutex)、條件變數(condition variable)、讀寫鎖(reader-writer lock)、文件鎖(record locking)、信號量(semaphore)等等。
進程間通信我首選Sockets(主要指TCP,我沒有用過UDP,也不考慮Unix domain協議)。其好處在於:
可以跨主機,具有伸縮性。反正都是多進程了,如果一台機器的處理能力不夠,很自然地就能用多台機器來處理。把進程分散到同一區域網的多台機器上,程序改改host:port配置就能繼續用;
TCP sockets和pipe都是操作文件描述符,用來收發位元組流,都可以read/write/fcntl/select/poll等。不同的是,TCP是雙向的,Linux的pipe是單向的,進程間雙向通信還得開兩個文件描述符,不方便;而且進程要有父子關系才能用pipe,這些都限制了pipe的使用;
TCP port由一個進程獨占,且進程退出時操作系統會自動回收文件描述符。因此即使程序意外退出,也不會給系統留下垃圾,程序重啟之後能比較容易地恢復,而不需要重啟操作系統(用跨進程的mutex就有這個風險);而且,port是獨占的,可以防止程序重復啟動,後面那個進程搶不到port,自然就沒法初始化了,避免造成意料之外的結果;
與其他IPC相比,TCP協議的一個天生的好處是「可記錄、可重現」。tcpmp和Wireshark是解決兩個進程間協議和狀態爭端的好幫手,也是性能(吞吐量、延遲)分析的利器。我們可以藉此編寫分布式程序的自動化回歸測試。也可以用tcpcopy之類的工具進行壓力測試。TCP還能跨語言,服務端和客戶端不必使用同一種語言。
分布式系統的軟體設計和功能劃分一般應該以「進程」為單位。從宏觀上看,一個分布式系統是由運行在多台機器上的多個進程組成的,進程之間採用TCP長連接通信。
使用TCP長連接的好處有兩點:一是容易定位分布式系統中的服務之間的依賴關系。只要在機器上運行netstat -tpna|grep <port>就能立刻列出用到某服務的客戶端地址(Foreign Address列),然後在客戶端的機器上用netstat或lsof命令找出是哪個進程發起的連接。TCP短連接和UDP則不具備這一特性。二是通過接收和發送隊列的長度也較容易定位網路或程序故障。在正常運行的時候,netstat列印的Recv-Q和Send-Q都應該接近0,或者在0附近擺動。如果Recv-Q保持不變或持續增加,則通常意味著服務進程的處理速度變慢,可能發生了死鎖或阻塞。如果Send-Q保持不變或持續增加,有可能是對方伺服器太忙、來不及處理,也有可能是網路中間某個路由器或交換機故障造成丟包,甚至對方伺服器掉線,這些因素都可能表現為數據發送不出去。通過持續監控Recv-Q和Send-Q就能及早預警性能或可用性故障。以下是服務端線程阻塞造成Recv-Q和客戶端Send-Q激增的例子:
[cpp] view plain copy
$netstat -tn
Proto Recv-Q Send-Q Local Address Foreign
tcp 78393 0 10.0.0.10:2000 10.0.0.10:39748 #服務端連接
tcp 0 132608 10.0.0.10:39748 10.0.0.10:2000 #客戶端連接
tcp 0 52 10.0.0.10:22 10.0.0.4:55572
五:多線程伺服器的適用場合
如果要在一台多核機器上提供一種服務或執行一個任務,可用的模式有:
a:運行一個單線程的進程;
b:運行一個多線程的進程;
c:運行多個單線程的進程;
d:運行多個多線程的進程;
考慮這樣的場景:如果使用速率為50MB/s的數據壓縮庫,進程創建銷毀的開銷是800微秒,線程創建銷毀的開銷是50微秒。如何執行壓縮任務?
如果要偶爾壓縮1GB的文本文件,預計運行時間是20s,那麼起一個進程去做是合理的,因為進程啟動和銷毀的開銷遠遠小於實際任務的耗時。
如果要經常壓縮500kB的文本數據,預計運行時間是10ms,那麼每次都起進程 似乎有點浪費了,可以每次單獨起一個線程去做。
如果要頻繁壓縮10kB的文本數據,預計運行時間是200微秒,那麼每次起線程似 乎也很浪費,不如直接在當前線程搞定。也可以用一個線程池,每次把壓縮任務交給線程池,避免阻塞當前線程(特別要避免阻塞IO線程)。
由此可見,多線程並不是萬靈丹(silver bullet)。
1:必須使用單線程的場合
據我所知,有兩種場合必須使用單線程:
a:程序可能會fork(2);
實際編程中,應該保證只有單線程程序能進行fork(2)。多線程程序不是不能調用fork(2),而是這么做會遇到很多麻煩:
fork一般不能在多線程程序中調用,因為Linux的fork只克隆當前線程的thread of control,不可隆其他線程。fork之後,除了當前線程之外,其他線程都消失了。
這就造成一種危險的局面。其他線程可能正好處於臨界區之內,持有了某個鎖,而它突然死亡,再也沒有機會去解鎖了。此時如果子進程試圖再對同一個mutex加鎖,就會立即死鎖。因此,fork之後,子進程就相當於處於signal handler之中(因為不知道調用fork時,父進程中的線程此時正在調用什麼函數,這和信號發生時的場景一樣),你不能調用線程安全的函數(除非它是可重入的),而只能調用非同步信號安全的函數。比如,fork之後,子進程不能調用:
malloc,因為malloc在訪問全局狀態時幾乎肯定會加鎖;
任何可能分配或釋放內存的函數,比如snprintf;
任何Pthreads函數;
printf系列函數,因為其他線程可能恰好持有stdout/stderr的鎖;
除了man 7 signal中明確列出的信號安全函數之外的任何函數。
因此,多線程中調用fork,唯一安全的做法是fork之後,立即調用exec執行另一個程序,徹底隔斷子進程與父進程的聯系。
在多線程環境中調用fork,產生子進程後。子進程內部只存在一個線程,也就是父進程中調用fork的線程的副本。
使用fork創建子進程時,子進程通過繼承整個地址空間的副本,也從父進程那裡繼承了所有互斥量、讀寫鎖和條件變數的狀態。如果父進程中的某個線程佔有鎖,則子進程同樣佔有這些鎖。問題是子進程並不包含佔有鎖的線程的副本,所以子進程沒有辦法知道它佔有了哪些鎖,並且需要釋放哪些鎖。
盡管Pthread提供了pthread_atfork函數試圖繞過這樣的問題,但是這回使得代碼變得混亂。因此《Programming With Posix Threads》一書的作者說:」Avoid using fork in threaded code except where the child process will immediately exec a new program.」。
b:限製程序的CPU佔用率;
這個很容易理解,比如在一個8核的伺服器上,一個單線程程序即便發生busy-wait,占滿1個core,其CPU使用率也只有12.5%,在這種最壞的情況下,系統還是有87.5%的計算資源可供其他服務進程使用。
因此對於一些輔助性的程序,如果它必須和主要服務進程運行在同一台機器的話,那麼做成單線程的能避免過分搶奪系統的計算資源。
8、linux並發伺服器中epoll+多線程分別怎麼理
某個線程處理某個特定事件吧
通過epoll檢測一些事件,事件觸發時,創建一個線程來專職處理這個事件
9、Linux下多線程和多進程程序的優缺點,各個適合什麼樣的業務場景
IBM有個傢伙做了個測試,發現切換線程context的時候,windows比linux快一倍多。進出最快的鎖(windows2k的 critical section和linux的pthread_mutex),windows比linux的要快五倍左右。當然這並不是說linux不好,而且在經過實際編程之後,綜合來看我覺得linux更適合做high performance server,不過在多線程這個具體的領域內,linux還是稍遜windows一點。這應該是情有可原的,畢竟unix家族都是從多進程過來的,而 windows從頭就是多線程的。
如果是UNIX/linux環境,採用多線程沒必要。
多線程比多進程性能高?誤導!
應該說,多線程比多進程成本低,但性能更低。
在UNIX環境,多進程調度開銷比多線程調度開銷,沒有顯著區別,就是說,UNIX進程調度效率是很高的。內存消耗方面,二者只差全局數據區,現在內存都很便宜,伺服器內存動輒若干G,根本不是問題。
多進程是立體交通系統,雖然造價高,上坡下坡多耗點油,但是不堵車。
多線程是平面交通系統,造價低,但紅綠燈太多,老堵車。
我們現在都開跑車,油(主頻)有的是,不怕上坡下坡,就怕堵車。
高性能交易伺服器中間件,如TUXEDO,都是主張多進程的。實際測試表明,TUXEDO性能和並發效率是非常高的。TUXEDO是貝爾實驗室的,與UNIX同宗,應該是對UNIX理解最為深刻的,他們的意見應該具有很大的參考意義。
多線程的優點:
無需跨進程邊界;
程序邏輯和控制方式簡單;
所有線程可以直接共享內存和變數等;
線程方式消耗的總資源比進程方式好;
多線程缺點:
每個線程與主程序共用地址空間,受限於2GB地址空間;
線程之間的同步和加鎖控制比較麻煩;
一個線程的崩潰可能影響到整個程序的穩定性;
到達一定的線程數程度後,即使再增加CPU也無法提高性能,例如Windows Server 2003,大約是1500個左右的線程數就快到極限了(線程堆棧設定為1M),如果設定線程堆棧為2M,還達不到1500個線程總數;
線程能夠提高的總性能有限,而且線程多了之後,線程本身的調度也是一個麻煩事兒,需要消耗較多的CPU
多進程優點:
每個進程互相獨立,不影響主程序的穩定性,子進程崩潰沒關系;
通過增加CPU,就可以容易擴充性能;
可以盡量減少線程加鎖/解鎖的影響,極大提高性能,就算是線程運行的模塊演算法效率低也沒關系;
每個子進程都有2GB地址空間和相關資源,總體能夠達到的性能上限非常大
多線程缺點:
邏輯控制復雜,需要和主程序交互;
需要跨進程邊界,如果有大數據量傳送,就不太好,適合小數據量傳送、密集運算
多進程調度開銷比較大;
最好是多進程和多線程結合,即根據實際的需要,每個CPU開啟一個子進程,這個子進程開啟多線程可以為若干同類型的數據進行處理。當然你也可以利用多線程+多CPU+輪詢方式來解決問題……
方法和手段是多樣的,關鍵是自己看起來實現方便有能夠滿足要求,代價也合適。