秒杀多线程:多线程笔试面试题汇总(转)
多线程在笔试面试中经常出现,下面列出一些公司的多线程笔试面试题。首先是一些概念性的问答题,这些是多线程的基础知识,经常出现在面试中的第一轮面试(我参加2011年腾讯研究院实习生招聘时就被问到了几个概念性题目)。然后是一些选择题,这些一般在笔试时出现,虽然不是太难,但如果在选择题上花费大多时间无疑会对后面的编程题造成影响,因此必须迅速的解决掉。最后是综合题即难一些的问答题或是编程题。这种题目当然是最难解决了,要么会引来面试官的追问,要么就很容易考虑不周全,因此解决这类题目时一定要考虑全面和细致。
下面就来看看这三类题目吧。
一。概念性问答题
第一题:线程的基本概念、线程的基本状态及状态之间的关系?
第二题:线程与进程的区别?
这个题目问到的概率相当大,计算机专业考研中也常常考到。要想全部答出比较难。
第三题:多线程有几种实现方法,都是什么?
第四题:多线程同步和互斥有几种实现方法,都是什么?
我在参加2011年迅雷校园招聘时的一面和二面都被问到这个题目,回答的好将会给面试成绩加不少分。
第五题:多线程同步和互斥有何异同,在什么情况下分别使用他们?举例说明。
二。选择题
第一题(百度笔试题):
以下多线程对int型变量x的操作,哪几个不需要进行同步:
A. x=y; B. x++; C. ++x; D. x=1;
第二题(阿里巴巴笔试题)
多线程中栈与堆是公有的还是私有的
A:栈公有, 堆私有
B:栈公有,堆公有
C:栈私有, 堆公有
D:栈私有,堆私有
三。综合题
第一题(台湾某杀毒软件公司面试题):
在Windows编程中互斥量与临界区比较类似,请分析一下二者的主要区别。
第二题:
一个全局变量tally,两个线程并发执行(代码段都是ThreadProc),问两个线程都结束后,tally取值范围。
inttally = 0;//glable
voidThreadProc()
{
for(inti = 1; i <= 50; i++)
tally += 1;
}
第三题(某培训机构的练习题):
子线程循环 10 次,接着主线程循环 100 次,接着又回到子线程循环 10 次,接着再回到主线程又循环 100 次,如此循环50次,试写出代码。
第四题(迅雷笔试题):
编写一个程序,开启3个线程,这3个线程的ID分别为A、B、C,每个线程将自己的ID在屏幕上打印10遍,要求输出结果必须按ABC的顺序显示;如:ABCABC…依次递推。
第五题(Google面试题)
有四个线程1、2、3、4.线程1的功能就是输出1,线程2的功能就是输出2,以此类推……现在有四个文件ABCD.初始都为空。现要让四个文件呈如下格式:
A:1 2 3 4 1 2…
B:2 3 4 1 2 3…
C:3 4 1 2 3 4…
D:4 1 2 3 4 1…
请设计程序。
下面的第六题与第七题也是在考研中或是程序员和软件设计师认证考试中的热门试题。
第六题
生产者消费者问题
这是一个非常经典的多线程题目,题目大意如下:有一个生产者在生产产品,这些产品将提供给若干个消费者去消费,为了使生产者和消费者能并发执行,在两者之间设置一个有多个缓冲区的缓冲池,生产者将它生产的产品放入一个缓冲区中,消费者可以从缓冲区中取走产品进行消费,所有生产者和消费者都是异步方式运行的,但它们必须保持同步,即不允许消费者到一个空的缓冲区中取产品,也不允许生产者向一个已经装满产品且尚未被取走的缓冲区中投放产品。
第七题
读者写者问题
这也是一个非常经典的多线程题目,题目大意如下:有一个写者很多读者,多个读者可以同时读文件,但写者在写文件时不允许有读者在读文件,同样有读者读时写者也不能写。
多线程相关题目就列举到此,如果各位有多线程方面的笔试面试题,欢迎提供给我,我将及时补上。谢谢大家。
1:tcp和udp的区别
2:流量控制和拥塞控制的实现机制
3:滑动窗口的实现机制
4:多线程如何同步。
5:进程间通讯的方式有哪些,各有什么优缺点
6:tcp连接建立的时候3次握手的具体过程,以及其中的每一步是为什么
7:tcp断开连接的具体过程,其中每一步是为什么那么做
8:tcp建立连接和断开连接的各种过程中的状态转换细节
9:epool与select的区别
10:epool中et和lt的区别与实现原理
11:写一个server程序需要注意哪些问题
12:项目中遇到的难题,你是如何解决的
6:建立连接采用的3次握手协议,具体是指:
第一次握手是客户端connect连接到server,server accept client的请求之后,向client端发送一个消息,相当于说我都准备好了,你连接上我了,这是第二次握手,第3次握手就是client向server发送的,就是对第二次握手消息的确认。之后client和server就开始通讯了。
7:断开连接的4次握手,具体如下:
断开连接的一端发送close请求是第一次握手,另外一端接收到断开连接的请求之后需要对close进行确认,发送一个消息,这是第二次握手,发送了确认消息之后还要向对端发送close消息,要关闭对对端的连接,这是第3次握手,而在最初发送断开连接的一端接收到消息之后,进入到一个很重要的状态time_wait状态,这个状态也是面试官经常问道的问题,最后一次握手是最初发送断开连接的一端接收到消息之后。对消息的确认。
epool与select的区别:
select在一个进程中打开的最大fd是有限制的,由FD_SETSIZE设置,默认值是2048。不过 epoll则没有这个限制,它所支持的fd上限是最大可以打开文件的数目,这个数字一般远大于2048,一般来说内存越大,fd上限越大,1G内存都能达到大约10w左右。
select的轮询机制是系统会去查找每个fd是否数据已准备好,当fd很多的时候,效率当然就直线下降了,epoll采用基于事件的通知方式,一旦某个fd数据就绪时,内核会采用类似callback的回调机制,迅速激活这个文件描述符,而不需要不断的去轮询查找就绪的描述符,这就是epool高效最本质的原因。
无论是select还是epoll都需要内核把FD消息通知给用户空间,如何避免不必要的内存拷贝就很重要,在这点上,epoll是通过内核于用户空间mmap同一块内存实现的,而select则做了不必要的拷贝
10:epool中et和lt的区别与实现原理
LT:水平触发,效率会低于ET触发,尤其在大并发,大流量的情况下。但是LT对代码编写要求比较低,不容易出现问题。LT模式服务编写上的表现是:只要有数据没有被获取,内核就不断通知你,因此不用担心事件丢失的情况。
ET:边缘触发,效率非常高,在并发,大流量的情况下,会比LT少很多epoll的系统调用,因此效率高。但是对编程要求高,需要细致的处理每个请求,否则容易发生丢失事件的情况。
另一点区别就是设为ET模式的文件句柄必须是非阻塞的
附一个例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
|
#include <stdio.h> #include <stdlib.h> #include <errno.h> #include <string.h> #include <sys/types.h> #include <netinet/in.h> #include <sys/socket.h> #include <sys/wait.h> #include <unistd.h> #include <arpa/inet.h> #include <openssl/ssl.h> #include <openssl/err.h> #include <fcntl.h> #include <sys/epoll.h> #include <sys/time.h> #include <sys/resource.h> #define MAXBUF 1024 #define MAXEPOLLSIZE 10000 /* setnonblocking - 设置句柄为非阻塞方式 */ int setnonblocking( int sockfd) { if (fcntl(sockfd, F_SETFL, fcntl(sockfd, F_GETFD, 0)|O_NONBLOCK) == -1) { return -1; } return 0; } /* handle_message - 处理每个 socket 上的消息收发 */ int handle_message( int new_fd) { char buf[MAXBUF + 1]; int len; /* 开始处理每个新连接上的数据收发 */ bzero(buf, MAXBUF + 1); /* 接收客户端的消息 */ len = recv(new_fd, buf, MAXBUF, 0); if (len > 0) { printf ( "%d接收消息成功:'%s',共%d个字节的数据\n" , new_fd, buf, len); } else { if (len < 0) printf ( "消息接收失败!错误代码是%d,错误信息是'%s'\n" , errno , strerror ( errno )); close(new_fd); return -1; } /* 处理每个新连接上的数据收发结束 */ return len; } int main( int argc, char **argv) { int listener, new_fd, kdpfd, nfds, n, ret, curfds; socklen_t len; struct sockaddr_in my_addr, their_addr; unsigned int myport, lisnum; struct epoll_event ev; struct epoll_event events[MAXEPOLLSIZE]; struct rlimit rt; myport = 5000; lisnum = 2; /* 设置每个进程允许打开的最大文件数 */ rt.rlim_max = rt.rlim_cur = MAXEPOLLSIZE; if (setrlimit(RLIMIT_NOFILE, &rt) == -1) { perror ( "setrlimit" ); exit (1); } else { printf ( "设置系统资源参数成功!\n" ); } /* 开启 socket 监听 */ if ((listener = socket(PF_INET, SOCK_STREAM, 0)) == -1) { perror ( "socket" ); exit (1); } else { printf ( "socket 创建成功!\n" ); } setnonblocking(listener); bzero(&my_addr, sizeof (my_addr)); my_addr.sin_family = PF_INET; my_addr.sin_port = htons(myport); my_addr.sin_addr.s_addr = INADDR_ANY; if (bind(listener, ( struct sockaddr *) &my_addr, sizeof ( struct sockaddr)) == -1) { perror ( "bind" ); exit (1); } else { printf ( "IP 地址和端口绑定成功\n" ); } if (listen(listener, lisnum) == -1) { perror ( "listen" ); exit (1); } else { printf ( "开启服务成功!\n" ); } /* 创建 epoll 句柄,把监听 socket 加入到 epoll 集合里 */ kdpfd = epoll_create(MAXEPOLLSIZE); len = sizeof ( struct sockaddr_in); ev.events = EPOLLIN | EPOLLET; ev.data.fd = listener; if (epoll_ctl(kdpfd, EPOLL_CTL_ADD, listener, &ev) < 0) { fprintf (stderr, "epoll set insertion error: fd=%d\n" , listener); return -1; } else { printf ( "监听 socket 加入 epoll 成功!\n" ); } curfds = 1; while (1) { /* 等待有事件发生 */ nfds = epoll_wait(kdpfd, events, curfds, -1); if (nfds == -1) { perror ( "epoll_wait" ); break ; } /* 处理所有事件 */ for (n = 0; n < nfds; ++n) { if (events[n].data.fd == listener) { new_fd = accept(listener, ( struct sockaddr *) &their_addr,&len); if (new_fd < 0) { perror ( "accept" ); continue ; } else { printf ( "有连接来自于: %d:%d, 分配的 socket 为:%d\n" , inet_ntoa(their_addr.sin_addr), ntohs(their_addr.sin_port), new_fd); } setnonblocking(new_fd); ev.events = EPOLLIN | EPOLLET; ev.data.fd = new_fd; if (epoll_ctl(kdpfd, EPOLL_CTL_ADD, new_fd, &ev) < 0) { fprintf (stderr, "把 socket '%d' 加入 epoll 失败!%s\n" , new_fd, strerror ( errno )); return -1; } curfds++; } else { ret = handle_message(events[n].data.fd); if (ret < 1 && errno != 11) { epoll_ctl(kdpfd, EPOLL_CTL_DEL, events[n].data.fd,&ev); curfds--; } } } } close(listener); return 0; } |
在这里简单说一下linux多线程同步的方法吧(win上有一定的差别,也有一定的累似)
1:线程数据,每个线程数据创建一个键,它和这个键相关联,在各个线程里,都使用这个键来指代线程数据,但在不同的线程里,这个键代表的数据是不同的,在同一个线程里,它代表同样的数据内容。以此来达到线程安全的目的。
2:互斥锁,就是在各个线程要使用的一些公共数据之前加锁,使用之后释放锁,这个是非常常用的线程安全控制的方法,而频繁的加解锁也对效率有一定的影响。
3:条件变量,而条件变量通过允许线程阻塞和等待另一个线程发送信号的方法弥补了互斥锁的不足,它常和互斥锁一起使用。使用时,条件变量被用来阻塞一个线程,当条件不满足时,线程往往解开相应的互斥锁并等待条件发生变化。一旦其它的某个线程改变了条件变量,它将通知相应的条件变量唤醒一个或多个正被此条件变量阻塞的线程。这些线程将重新锁定互斥锁并重新测试条件是否满足。一般说来,条件变量被用来进行线程间的同步。
4:信号量,信号量本质上是一个非负的整数计数器,它被用来控制对公共资源的访问。当公共资源增加时,调用函数sem_post()增加信号量。只有当信号量值大于0时,才能使用公共资源,使用后,函数sem_wait()减少信号量。函数sem_trywait()和函数pthread_ mutex_trylock()起同样的作用,它是函数sem_wait()的非阻塞版本
另外pthread_join也可以等待一个线程的终止。可能还有一些其他我暂时没有想到的方法,欢迎补充