目录
什么是线程:
进程是正在运行的程序及其占用的资源,而线程是进程的一条执行路径。所有的线程都是在同一进程空间运行,这也意味着多条线程将共享该进程中的全部系统资源,如虚拟地址空间,文件描述符和信号处理等等。但同一进程中的多个线程有各自的调用栈(call stack),自己的寄存器环境(register context),自己的线程本地存储(thread-local storage)。 一个进程可以有很多线程,每条线程并行执行不同的任务。进程可以比作父母给儿女买了一个房子,并对着自己现住的房子一比一的装修好,而线程则是在原有的房子中在开辟出一个新的房间。
多进程与多线程的区别、优缺点及其应用场景:
区别:
- 线程是进程划分成的更⼩的运⾏单位,⼀个进程在其执⾏的过程中可以产⽣多个线程。
- 线程和进程最⼤的不同在于基本上各进程是独⽴的,⽽各线程则不⼀定,因为同⼀进程中的线程极有可能会相互影响。
- 线程执⾏开销⼩,但不利于资源的管理和保护;⽽进程正相反。
- 进程是对运行时程序的封装,是系统进行资源调度和分配的的基本单位,实现了操作系统的并发;
- 线程是进程的子任务,是CPU调度和分派的基本单位,用于保证程序的实时性,实现进程内部的并发;
- 一个程序至少有一个进程,一个进程至少有一个线程,线程依赖于进程而存在;
- 进程在执行过程中拥有独立的内存单元,而多个线程共享进程的内存。
消耗资源:
从内核的观点看,进程的目的就是担当分配系统资源(CPU时间、内存等)的基本单位。线程是进程的一个执行流,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。线程,它们彼此之间使用相同的地址空间,共享大部分数据,启动一个线程所花费的空间远远小于启动一个进程所花费的空间,而且,线程间彼此切换所需的时间也远远小于进程间切换所需要的时间。据统计,总的说来,一个进程的开销大约是一个线程开销的30倍左右,当然,在具体的系统上,这个数据可能会有较大的区别。通讯方式:
进程之间传递数据只能是通过通讯的方式,即费时又不方便。线程时间数据大部分共享(线程函数内部不共享),快捷方便。但是数据同步需要锁对于static变量尤其注意
优缺点:
多进程优点:
1、每个进程相互独立,不影响主程序的稳定性,子程序崩溃没关系;
2、通过增加CPU,就可以扩充性能;
3、可以尽量减少线程加锁/解锁的影响,极大提高性能,就算是线程运行的模块算法效率低也没关系;
4、每个子进程都有2GB地址空间和相关资源,总体能够达到的性能上限非常大。
多进程缺点:
1、逻辑控制复杂,需要和主程序交互;
2、需要跨进程边界,如果有大数据量传送,就不太好,适合小数据量传送、密集运算。
3、多进程调度开销比较大;
多线程优点:
1、无需跨进程边界;
2、程序逻辑和控制方式简答;
3、所有线程可以直接共享内存和变量等;
4、线程方式消耗的总资源比进程方式好。
多线程缺点:
1、每个线程与主程序公用地址空间,受限于2GB地址空间;
2、线程之间的同步和加锁控制比较麻烦;
3、一个线程的崩溃可能影响到整个程序的稳定性;
4、到达一定的线程数程度后,即使再增加CPU也无法提高性能,例如Windows Server 2003,大约是1500个左右的线程数就快到极限了(线程堆栈设定为1M),如果设定线程堆栈为2M,还达不到1500个线程总数;
5、线程能够提高的总性能有限,而且流程多了之后,线程本身的调度也是一个麻烦事儿,需要消耗较多的CPU。
最好是多进程和多线程结合,即根据实际的需要,每个CPU开启一个子进程,这个子进程开启多线程可以为若干同类型的数据进行处理。当然你也可以利用多线程+多CPU+轮询方式来解决问题;方法和手段的多样的,关键是自己看起来实现方便又能够满足要求,代价也合适。
应用场景:
多进程的应用场景:
nginx主流的工作模式是多进程(也支持多线程)
几乎所有的web server服务器都有多进程,至少有一个守护进程配合一个worker进程,例如apached,httpd等以d结尾的进程,包括init.d本身就是0级进程,所有你认知的进程都是他的进程
chrome浏览器也是多进程方式(原因:1、可能存在一些网页不符合编程规范,容易崩溃,采用多进程,一个网页崩溃也不会影响其他网页,而多线程会。2、网页之间互相隔离,保证安全,不必担心某个网页的恶意代码获取存放在网页中的敏感信息。)
redis也可以归类到“多进程单线程”模型(平时工作是单个进程,涉及耗时操作如持久化或aof重写是会用到多个线程)
多线程应用场景
线程间有数据共享,并且数据需要修改时(不同任务间需要大量共享数据或频繁通信时)。
提供非均质的服务(有优先级任务处理)事件响应应有优先级
单任务并行计算,在非CPU Bound的场景下提高响应优先级,降低延时
与人有IO交互的应用,良好的用户体验(键盘鼠标的输入,立即响应)
案例:桌面软件,响应用户输入的是一个线程,后台程序处理是另一个线程
怎么选:
1)需要频繁创建销毁的优先用线程
这种原则最常见的应用就是Web服务器了,来一个连接建立一个线程,断了就销毁线程,要是用进程,创建和销毁的代价是很难承受的2)需要进行大量计算的优先使用线程
所谓大量计算,当然就是要耗费很多CPU,切换频繁了,这种情况下线程是最合适的。
这种原则最常见的是图像处理、算法处理。3)强相关的处理用线程,弱相关的处理用进程
什么叫强相关、弱相关?理论上很难定义,给个简单的例子就明白了。
一般的Server需要完成如下任务:消息收发、消息处理。“消息收发”和“消息处理”就是弱相关的任务,而“消息处理”里面可能又分为“消息解码”、“业务处理”,这两个任务相对来说相关性就要强多了。因此“消息收发”和“消息处理”可以分进程设计,“消息解码”、“业务处理”可以分线程设计。当然这种划分方式不是一成不变的,也可以根据实际情况进行调整。4)可能要扩展到多机分布的用进程,多核分布的用线程
5)都满足需求的情况下,用你最熟悉、最拿手的方式
至于“数据共享、同步”、“编程、调试”、“可靠性”这几个维度的所谓的“复杂、简单”应该怎么取舍,我只能说:没有明确的选择方法。但我可以告诉你一个选择原则:如果多进程和多线程都能够满足要求,那么选择你最熟悉、最拿手的那个。
需要提醒的是:虽然我给了这么多的选择原则,但实际应用中基本上都是“进程+线程”的结合方式,千万不要真的陷入一种非此即彼的误区。
线程的相关函数和知识:
创建线程:
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
说明: pthreand_create()用来创建一个线程,并执行第三个参数start_routine所指向的函数。第三个参数start_routine是一个函数指针,它指向的函数原型是 void *func(void *),这是所创建的子线程要执行的任务(函数);第四个参数arg就是传给了所调用的函数的参数,如果有多个参数需要传递给子线程则需要封装到一个结构体里传进去;第一个参数thread是一个pthread_t类型的指针,他用来返回该线程的线程ID。每个线程都能够通过pthread_self()来获取自己的线程ID(pthread_t类型)。第二个参数是线程的属性,其类型是pthread_attr_t类型,其定义如下:typedef struct { int detachstate; 线程的分离状态 int schedpolicy; 线程调度策略 struct sched_param schedparam; 线程的调度参数 int inheritsched; 线程的继承性 int scope; 线程的作用域 size_t guardsize; 线程栈末尾的警戒缓冲区大小 int stackaddr_set; void * stackaddr; 线程栈的位置 size_t stacksize; 线程栈的大小 }pthread_attr_t;
对于这些属性,我们需要设定的是线程的分离状态,如果有必要也需要修改每个线程的栈大小。每个线程创建后默认是 joinable 状态,该状态需要主线程调用 pthread_join 等待它退出,否则子线程在结束时,内存资源不能得到释放造成内存泄漏。所以我 们创建线程时一般会将线程设置为分离状态,具体有两种方法:1. 线程里面调用 pthread_detach(pthread_self()) 这个方法最简单2. 在创建线程的属性设置里设置PTHREAD_CREATE_DETACHED属性
1. 可会合(joinable):这种关系下,主线程需要明确执行等待操作,在子线程结束后,主线程的等待操作执行完毕,子线程和主线程会合,这时主线程继续执行等待操作之后的下一步操作。主线程必须会合可会合的子线程。在主线程的线程函数内部调用子线程对象的wait函数实现,即使子线程能够在主线程之前执行完毕,进入终止态,也必须执行会合操作,否则,系统永远不会主动销毁线程,分配给该线程的系统资源也永远不会释放。2. 相分离(detached):表示子线程无需和主线程会合,也就是相分离的,这种情况下,子线程一旦进入终止状态,这种方式常用在线程数较多的情况下,有时让主线程逐个等待子线程结束,或者让主线程安排每个子线程结束的等待顺序,是很困难或不可能的,所以在并发子线程较多的情况下,这种方式也会经常使用。线程的分离状态决定一个线程以什么样的方式来终止自己,在默认的情况下,线程是非分离状态的,这种情况下,原有的线程等待创建的线程结束,只有pthread_join函数返回时,创建的线程才算终止,释放自己占用的系统资源,而分离线程没有被其 他的线程所等待,自己运行结束了,线程也就终止了,马上释放系统资源。
下面是线程的一个例子:
1 #include <stdio.h>
2 #include <string.h>
3 #include <errno.h>
4 #include <unistd.h>
5 #include <stdlib.h>
6 #include <pthread.h>
7
8 void *thread_worker1(void *args);
9 void *thread_worker2(void *args);
10
11 int main (int argc ,char **argv)
12 {
13 int shared_var = 1000;
14 pthread_t tid;
15 pthread_attr_t thread_attr;
16
17 if( pthread_attr_init(&thread_attr)) // 初始化线程属性
18 {
19 printf("pthread_attr_init() failure:%s\n",strerror(errno));
20 return -1;
21 }
22
23 if(pthread_attr_setstacksize(&thread_attr,120*1024)) //可以设置栈大小
24 {
25 printf("pthread_attr_setstacksize() failure:%s\n",strerror(errno));
26 return -2;
27 }
28
29 if(pthread_attr_setdetachstate(&thread_attr,PTHREAD_CREATE_DETACHED)) // 可以设置线程的状态,可会合或相分离,这里设置为分离状态
30 {
31 printf("pthread_attr_setdetachstate() failure:%s\n",strerror(errno));
32 return -3;
33 }
34
35 pthread_create(&tid,&thread_attr,thread_worker1,&shared_var);//创建线程,并使用之前定义好的线程属性
36 printf("Thread worker1 tid[%ld] create ok\n",tid);
37
38 pthread_create(&tid, NULL, thread_worker2,&shared_var);
/*创建线程,没用定义线程属同时在thread_worker2() 里并没有调用pthread_detach()设置线程为分离状
态。这时就需要主线程在45行处调用pthread_join() 等待第二个子线程退出。当然主线程也就阻塞在这里不会往下继续执行了。*/
39 printf("thread worker2 tif[%ld] create ok\n",tid);
40
41 pthread_attr_destroy(&thread_attr);
42 pthread_join(tid,NULL);
43
44 while(1)
45 {
46 printf("Main/Control thread share_var:%d\n",shared_var);
47 sleep(10);
48 }
49 }
50 void *thread_worker1(void *args)/*在执行thread_worker1、*thread_worker2两个线程时,我通过第四个参数将主线程栈中的 shared_var 变量地址传给了子线程,因为所有线程都是在同一进程空间中运行,而只是子线程有自己独立的栈空间,所以这时所有子线程都可以访问主线程空间的shared_var变量。*/
51 {
52 int *ptr = (int *)args;
53
54 if( !args )
55 {
56 printf("%s() get invalid arguments\n",__FUNCTION__);
57 pthread_exit(NULL);
58 }
59 printf("Thread worker1 [%ld] star running...\n",pthread_self());
60
61 while(1)
62 {
63 printf("+++:%s before shared_var++:%d\n",__FUNCTION__,*ptr);
64 *ptr +=1;
65 sleep(2);
66 printf("+++:%s after sleep shared_var :%d\n",__FUNCTION__,*ptr);
67 }
68 printf("thread worker1 exit...\n");
69 return NULL;
70 }
71 void *thread_worker2(void *args)
72 {
73 int *ptr = (int *)args;
74 if( !args )
75 {
76 printf("%s() get invalid arguments\n",__FUNCTION__);
77 pthread_exit(NULL);
78 }
79 printf("Thread worker2 [%ld] star running...\n",pthread_self());
80
81 while(1)
82 {
83 printf("---:%s before shared_var++:%d\n",__FUNCTION__,*ptr);
84 *ptr +=1;
85 sleep(2);
86 printf("---:%s after sleep shared_var :%d\n",__FUNCTION__,*ptr);
87 }
88 printf("thread worker2 exit...\n");
89 return NULL;
90 }
主线程创建子线程后究竟是子线程还是主线程先执行,或究竟哪个子线程先运行系统并没有规定,这个依赖操作系统的进程调度策略。当然因为代码45行处主线程调用了pthread_join会导致主线程阻塞,所以主线程不会往下继续执行while(1)循环。我们再来深入分析各个线程的代码:我们在创建子线程之后,在子线程的执行函数里一般都会用while(1)的死循环来让子线程一直运行,否则子线程将按代码顺序执行,执行完毕就线程退出了。同样的,我们主线程也应该要用一个while(1)循环一直运行,否则主线程退出会导致进程退出,而进程退出会导致所有子线程退出了。从上面的运行结果我们可以看到,thread_worker1 在创建后首先开始运行,在开始自加之前值为初始值1000,然后让该值自加后休眠2秒后再打印该值发现不是1001而是1002了。这是由于shared_var 这个变量会被两个子线程同时访问修改导致。如果一个资源会被不同的线程访问修改,那么我们把这个资源叫做临界资源,那么对于该资源访问修改相关的代码就叫做临界区。那么怎么解决多个线程之间共享同一个共享资源,是多线程编程需要考虑的一个问题。
多线程改写服务器程序
流程图:
![](https://img-blog.csdnimg.cn/0f26428521364096bfe57f65916d702f.png)
程序代码:
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <string.h>
4 #include <errno.h>
5 #include <sys/types.h>
6 #include <sys/socket.h>
7 #include <stdlib.h>
8 #include <netinet/in.h>
9 #include <arpa/inet.h>
10 #include <getopt.h>
11 #include <ctype.h>
12 #include <pthread.h>
13
14 typedef void *(THREAD_BODY) (void *thread_arg);
15
16 void *thread_worker(void *ctx);
17 int thread_start(pthread_t * thread_id,THREAD_BODY *thread_workbody,void *thread_arg);
18
19 void print_usage(char *progname)
20 {
21 printf("%s usage : \n ",progname);
22 printf("-p(--port):sepcify server listen port.\n");
23 printf("-h(--help):print this help information.\n");
24 return ;
25 }
26 int main (int argc ,char **argv)
27 {
28 int ch = -1;
29 int sockfd = -1;
30 int clifd;
31 int port = 0 ;
32 int rv = -1;
33 int on = 1;
34 struct sockaddr_in seraddr;
35 struct sockaddr_in cliaddr;
36 socklen_t clilen = sizeof(cliaddr);
37 struct option opts[] = {
38 {"port",required_argument,NULL,'p'},
39 {"help",no_argument,NULL,'h'},
40 {NULL,0,NULL,0}
41 };
42 pthread_t tid;
43
44
45 while((ch = getopt_long(argc,argv,"p:h",opts,NULL)) != -1)
46
47 {
48 switch(ch)
49 {
50 case 'p':
51 port=atoi(optarg);
52 break;
53 case 'h':
54 print_usage(argv[0]);
55 return 0;
56 }
57 }
58 if( !port )
59 {
60 print_usage(argv[0]);
61 return 0;
62 }
63
64
65 sockfd = socket(AF_INET,SOCK_STREAM,0);
66 if(sockfd<0)
67 {
68 printf("create socket failure:%s \n",strerror(errno));
69 return -1;
70 }
71
72 printf("create socket[%d] secessful!",sockfd);
73 setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on));
74
75 memset(&seraddr,0,sizeof(seraddr));
76 seraddr.sin_family = AF_INET;
77 seraddr.sin_port = htons(port);
78 seraddr.sin_addr.s_addr = htonl(INADDR_ANY);
79 rv = bind(sockfd,(struct sockaddr *)&seraddr,sizeof(seraddr));
80 if( rv<0 )
81 {
82 printf("Sockect[%d] bind on port[%d]failure: %s\n",sockfd,port,strerror(errno));
83 return -2;
84 }
85
86 listen(sockfd,13);
87 printf("Start to listen on port[%d]",port);
88
89 while(1)
90 {
91 printf("start accept new incoming ...\n");
92 clifd = accept(sockfd,(struct sockaddr *)&cliaddr,&clilen);
93 if(clifd<0)
94 {
95 printf("Accept new client failure;%s\n",strerror(errno));
96 continue;
97 }
98 printf("Accept new clien[%s:%d] sucessfully\n",inet_ntoa(cliaddr.sin_addr),ntohs(cliaddr.sin_port));
99 thread_start(&tid,thread_worker,(void *)(&clifd));
100 }
101 close(sockfd);
102 return 0;
103
104 }
105
106
107 int thread_start(pthread_t * thread_id, THREAD_BODY * thread_workbody,void *thread_arg)
108 {
109 int rv = -1;
110 pthread_attr_t thread_attr;
111
112 if( pthread_attr_init(&thread_attr) )
113 {
114 printf("pthread_attr_init() failure:%s\n",strerror(errno));
115 goto cleanup;
116 }
117
118 if( pthread_attr_setstacksize(&thread_attr,120*1024) )
119 {
120 printf("pthread_attr_setstacksize() failure:%s\n",strerror(errno));
121 goto cleanup;
122 }
123 if( pthread_attr_setdetachstate(&thread_attr,PTHREAD_CREATE_DETACHED) )
124 {
125 printf("pthread_attr_setsdetachstate() failure:%s\n",strerror(errno));
126 goto cleanup;
127 }
128 if(pthread_create(thread_id, &thread_attr,thread_workbody,thread_arg))
129 {
130 printf("Create thread failure:%s\n",strerror(errno));
131 goto cleanup;
132 }
133 rv = 0;
134 cleanup:
135 pthread_attr_destroy(&thread_attr);
136
137 }
138 void *thread_worker(void *ctx)
139 {
140 int clifd;
141 int rv;
142 char buf[1024];
143 int i;
144
145 if(!ctx)
146 {
147 printf("Invalid input argument in %s()\n",__FUNCTION__);
148 pthread_exit(NULL);
149 }
150 clifd = *(int *)ctx;
151 printf("Child thread start to communicate with socket client...\n");
152
153 while(1)
154 {
155 memset(buf, 0, sizeof(buf));
156 rv = read(clifd, buf, sizeof(buf));
157 if(rv < 0)
158 {
159 printf("Read data from client socket[%d] failure:%s and thread will exit\n",clifd,strerror(errno));
160 close(clifd);
161 pthread_exit(NULL);
162 }
163 else if( rv >0 )
164 {
165 printf("Read %d bytes data form server:%s\n",rv,buf);
166 }
167
168 for(i=0;i<rv;i++)
169 {
170 buf[i] = toupper(buf[i]);
171 }
172
173
174 rv = write(clifd, buf, rv);
175 if( rv < 0)
176 {
177 printf("Write to client by socket[%d] failure:%s and thread will exit\n",clifd,strerror(errno));
178 close(clifd);
179 pthread_exit(NULL);
180 }
181
182 }
183 }
184 return rv;