首页 技术 正文
技术 2022年11月16日
0 收藏 693 点赞 3,020 浏览 13511 个字

这是基础,理解不能有偏差

如果线程/进程的逻辑控制流在时间上重叠,那么就是并发的。
我们可以将并发看成是一种os内核用来运行多个应用程序的实例,但是并发不仅在内核,在应用程序中的角色也很重要。

在应用级程序中的并发举例:

访问慢速io设备,当一个应用等待慢速设备(磁盘)的数据到来时,内核会运行其他进程,使cpu保持繁忙。每个应用都可以按照类似的方式,通过交替执行io请求和其他有用的工作来使用并发。

与人交互,和计算机交互的人要求计算机由同时执行多个任务的能力,例如他们在打印一个文档时,可能要调整一个窗口的大小。需要用并发来实现。每次用户请求某种操作(点击鼠标),一个独立的并发逻辑刘创建来执行这个操作。

通过推迟工作来降低延迟,应用程序能够通过推迟其他操作和并发地执行他们,利用并发来降低某些操作的延迟。例如,一个动态存储分配器通过推迟合并,将它放在一个运行较低优先级的并发“合并”流中,在由空闲的cpu周期时充分利用这些空闲时间,减低某个free操作的延迟。

服务多个网络客户端,期望它能每秒为千百个client服务。需要创建一个并发服务器,它为每个客户端创建一个单独的逻辑流,运行服务器同时为多个客户端服务,避免了慢速客户端独占服务器。

多核机器上进行并行计算,多核处理器含有多个cpu,被划分为并发流的应用程序通常在多核机器上比在单个处理器机器上运行的更快。

——————-

现在有三种方法来构造 并发程序

进程,每个逻辑控制刘都是一个进程,由内核来调度和维护。因为进程由独立的逻辑地址空间,要和其他流进行通信,控制流必须使用某种显示的进程间通信(IPC)机制。

I/O多路复用,应用程序在一个程序的上下文中显示地调用它们自己的逻辑流。逻辑流被模型化为状态机,数据到达文件描述符fd后,主程序显示地从一个状态转化到另一个状态,因为程序是一个单独的进程,所有的流都共享同一个地址空间。

线程,线程是运行在一个单一上下文中的逻辑流,由内核调度。它既像进程流一样由内核进行调度,又像io多路复用一样共享同一个虚拟地址空间。

——————

基于进程的并发编程

构造一个并发服务器的自然方法就是,在父进程中接受客户端的连接请求,然后创建一个新的子进程来为每个新客户端提供服务。

假设我们有两个客户端和一个服务器。服务器正在监听一个监听描述符listenfd(3)上连接请求,现在假设服务器接受了client1的链接请求,并返回一个已连接描述符connfd(4)。

在接受连接请求后,服务器派生fork()一个子进程,这个子进程获得服务器描述符表的完整拷贝(包括listenfd3和connfd4)。子进程关闭它的拷贝中的监听描述符listenfd3,而父进程关闭它的已连接描述符connfd4。这是XX  子进程就忙于为client1提供服务,因为父、子进程中的已连接描述符connfd4都指向同一个文件表表项,所以父进程关闭它的已连接描述符connfd4,这一步骤是必不可少的(否则会引起存储器泄漏最终消耗掉可用的存储器,os崩溃)。

当client2申请服务时,步骤和上面一样的。

需要注意的地方:1,通常会运行很长时间,我们必须包括一个SIGCHLD处理程序,来回收“僵死”子进程的资源。因为当SIGCHLD处理程序执行时,SIGCHLD信号是阻塞的,而unix信号是不排队的额,所以SIGCHLD处理程序必须准备好回收多个僵死子进程的资源(sigchld_handler()函数被调用后,一直在运行等待 回收 父进程的所有子进程资源)。2,父子进程必须关闭它们各自的connfd的拷贝,这对父进程来说是很重要的,以免存储器泄漏。3,最后因为套接字的文件表表项中的引用计数,直到父子 进程中的connfd都关闭了,客户端的连接才会终止。

 /*
* echoserverp.c - A concurrent echo server based on processes
*/
/* $begin echoserverpmain */
#include "csapp.h"
void echo(int connfd); void sigchld_handler(int sig) //line:conc:echoserverp:handlerstart
{
while (waitpid(-, , WNOHANG) > )
;
return;
} //line:conc:echoserverp:handlerend int main(int argc, char **argv)
{
int listenfd, connfd, port;
socklen_t clientlen=sizeof(struct sockaddr_in);
struct sockaddr_in clientaddr; if (argc != ) {
fprintf(stderr, "usage: %s <port>\n", argv[]);
exit();
}
port = atoi(argv[]); Signal(SIGCHLD, sigchld_handler);
listenfd = Open_listenfd(port);
while () {
connfd = Accept(listenfd, (SA *) &clientaddr, &clientlen);
if (Fork() == ) {
Close(listenfd); /* Child closes its listening socket */
echo(connfd); /* Child services client */ //line:conc:echoserverp:echofun
Close(connfd); /* Child closes connection with client */ //line:conc:echoserverp:childclose
exit(); /* Child exits */
}
Close(connfd); /* Parent closes connected socket (important!) */ //line:conc:echoserverp:parentclose
}
}
/* $end echoserverpmain */

进程的优劣:在父子进程间共享状态信息,进程由一个非常清晰的模型:共享文件表,但是不共享用户地址空间。
这既有优点也有缺点,优点是进程不可能不小心覆盖另一个进程的虚拟存储器,这就消除了许多令人迷惑的错误;缺点是独立的地址空间使得进程共享状态信息变得更加困难,为了共享信息,必须使用显示的进程通信(IPC)机制,这就导致基于进程设计程序,进程控制和IPC的开销较高;

常见的IPC 例子,waitpid()、unix信号是基本的IPC机制,它们允许进程发送小消息到同一个主机的其他进程;套接字接口是IPC的一种重要形式,允许不同主机上的进程交换任意的字节流。但是术语unix IPC是指运行进程和同一台主机上的其他进程进行通信的技术,包括管道,先进先出,系统V共享存储器以及系统信号量(semaphore)。可以看《unix环境编程》一书。

——————————————–

基于IO多路复用的并发编程

例子:echo(回显)服务器,能对用户从标准输入键入的命令作出响应;分析:服务器必须分别响应两种相互独立的I/O事件:1,网络客户端发起连接请求 2,用户在键盘上键入命令行;

问题:我们先等待哪个事件呢?如果在accept中等待一个连接请求,我们就不能响应输入的命令;如果在read中等待一个输入命令,我们就不能响应任何连接的请求。

解决方法:IO多路复用技术;

思路:利用select函数,要求内核挂起进程,只有在一个或多个IO事件发生后,才将控制返回给应用程序。例如,当{1,2,7}中任意描述符准备好写时返回。

select是一个复杂的函数,有许多不同的使用场景,现在先只考虑一组描述符准备好读,

 #include <unistd.h>
#include <sys/types.h>
int select(int n,fd_set *fdset, NULL, NULL, NULL);//返回值,表示已经准备好的描述符的非零的个数,若出错则为-1
FD_ZERO(fd_set *fdset)//在fdset清空所有的位
FD_CLR(int fd, fd_set *fdset)//清空fdset中的fd位
FD_SET(int fd, fd_set *fdset)//将fdset中的fd位置位
FD_ISSET(int fd,fd_set *fdset)//判断fdset中的fd位

select函数处理类型为fd_set的集合,就是描述符集合。逻辑上,我们将描述符集合看成是一个大小为n的位向量,b(n-1),….b(1),b(0)

每个b(k)对应与描述符k,当且仅当b(k)=1,才表明描述符k是描述符集合中的一个元素。只允许你对描述符集合做三件事情:

,分配它们
,将一个此种类型的变量赋值给另一个变量
,使用FD_ZERO,FD_SET,FD_CLR,FD_ISSET宏指令来修改和检查它们

针对问题,select函数有两个输入:

,一个称为读集合的描述符集合fdset
,该读集合的基数n(实际上就是任何描述符集合的最大基数)

select函数会一直阻塞,直到至少有一个描述符准备好可以读了。

那么问题是,什么时候代表一个描述符可以读了呢?当且仅当一个从该描述符读取一个字节的请求不会阻塞时,描述符k就表示可以读了。

问题是,select修改了参数fdset指向的fd_set,指明读集合中一个称为准备好(ready set)的子集,这个集合是由读集合中准备好可以读了的描述符组成的。函数返回的值指明了准备好集合的基数(个数);由于这个问题的存在,我们必须在每次调用select时都等新读集合。

 /*
* main.c
*
* Created on: Apr 21, 2016
* Author: lizhen
*/
#include "csapp.h" void echo(int connfd);
void command(void); int main(int argc,char* argv[]){
int listenfd, connfd, port;
socklen_t clientlen = sizeof(struct sockaddr_in);
struct sockaddr_in clientaddr;
fd_set read_set, ready_set; if(argc != ){
fprintf(stderr, "usage: %s <port>\n",argv[]);
exit();
} port = atoi(argv[]);
listenfd = Open_listenfd(port); FD_ZERO(&read_set);//清空read fd set
FD_SET(STDIN_FILENO,&read_set);//添加stdin 到 read fd set中
FD_SET(listenfd, &read_set);//添加listenfd 到 read fd set中 while(){
ready_set = read_set;//将描述符集合赋值给ready_set
Select(listenfd+,&ready_set,NULL,NULL,NULL);//因此select会修改参数
//ready_set指向的fd_set,所以我们必须将一个临时的fd_set传给select函数
//为什么会加1呢?
//因为select需要一个数值表示临时fd_set的基数(就是个数),listenfd的数值是3,
//前面的三个分别是stdin,stdout,stderr
if(FD_ISSET(STDIN_FILENO,&ready_set)){
command();//从stdin读取命令行
}
if(FD_ISSET(listenfd,&ready_set)){
//如果listenfd描述符准备好了,那么我们执行连接操作
connfd = Accept(listenfd,(SA*)&clientaddr, &clientlen);
echo(connfd);
Close(connfd);
}
}//while
} void command(void){
char buf[MAXLINE];
if(!Fgets(buf,MAXLINE,stdin)){
exit();
}
printf("%s",buf);//处理输入命令
}

分析代码:32行中不是调用accept函数来等待一个连接请求,而是调用select函数,这个函数会一直阻塞,直到listenfd或stdin_fileno准备好可以读了。
一旦select返回,我们就调用FD_ISSET宏指令来判断哪个描述符准备好可以读了:1,如果是标准输入读了,就调用command函数,该函数在返回主程序前,会读、解析和响应命令 2,如果是监听描述符准备好了,我们就调用accept来得到一个connfd,然后调用echo函数,它将来自client的每一行回送回去,直到这个client关闭这个连接中它的那一端。

代码问题:一旦它连接到某个客户端,就会连续回送输入行,直到客户端关闭这个连接中它的那一端。就是说,你在server端键入一个命令到标准输入,server端不会得到响应,直到server和client之间断开。

解决方法:可以采用更细粒度的多路复用,server每次循环(至多)回送一个文本行。

—————————–

基于IO多路复用的并发事件驱动服务器

IO多路复用可以用作并发时间驱动(event-driven)程序的基础。在事件驱动程序中,流是因为某种事件而前进的,一般概念就是将逻辑流模型化为状态机。状态机就是一组状态、输入事件、转移,其中转移就是将状态和输入事件映射到状态。每个状态都是将一个(输入状态、输入事件)映射到一个输出状态。自循环是同一个输入和输出状态之间的转移。

通常将状态机化为有向图,节点表示状态,有向弧表示转移,弧上的标号表示输入事件。

嘻嘻嘻嘻嘻嘻嘻嘻嘻嘻嘻嘻嘻嘻嘻嘻下

对于每一个客户端k,基于IO多路复用的并发服务器会创建一个新的状态机s(k),并将它和已连接描述符d(k)联系起来。即每个状态机s(k)都有一个状态(“等待描述符d(k)准备好读”),一个输入事件(“描述符dk准备好可以读了”)和一个转移(从描述符dk读一个文本行);

一定少不了select函数检测输入是事件的发生,当每个connfd准备好读时,server就为响应的状态机执行转移,就是从描述符读和写一个文本行;

 /*
* main.c
*
* Created on: Apr 21, 2016
* Author: lizhen
*/
#include "csapp.h" typedef struct{//表示connfd连接描述符的池子
int maxfd;//在read_set最大的描述符
fd_set read_set;//所有active 描述符的集合
fd_set ready_set;//准备好reading的描述符的子集合
int nready;//从select函数中返回的 所有准备好 的描述符数量
int maxi;//highwater index into client array,池子的水位高度,
//就是新加入到pool中的clientio下标
int clientfd[FD_SETSIZE];//set of active descriptors
//active 的client描述符 ?
rio_t clientrio[FD_SETSIZE];//set of active read buffers
//active的 读缓冲的集合 ?
}pool; int byte_cnt = ;//统计server收到的全部字节数量 void init_pool(int listenfd, pool *p);
void add_client(int connfd, pool *p);
void check_clients(pool *p); int main(int argc,char* argv[]){
int listenfd, connfd, port;
socklen_t clientlen = sizeof(struct sockaddr_in);
struct sockaddr_in clientaddr;
static pool pool; if(argc != ){
fprintf(stderr,"usage: %s <port>\n",argv[]);
exit();
} port = atoi(argv[]); printf("listending\n");
listenfd = Open_listenfd(port);
printf("listened\n");
init_pool(listenfd, &pool);
printf("inited_pooled\n"); while(){
//等待listening / connected描述符变为就绪
pool.ready_set = pool.read_set;
pool.nready = Select(pool.maxfd+,&pool.ready_set, NULL,NULL,NULL); //如果listenfd 就绪ready,添加新的client到pool中
if(FD_ISSET(listenfd,&pool.ready_set)){
printf("to be accepted\n");
connfd = Accept(listenfd, (SA*)&clientaddr,&clientlen);
add_client(connfd,&pool);
} //从一个就绪ready的连接描述符connfd,回送echo一个文本行
check_clients(&pool);
}//while
} void init_pool(int listenfd, pool *p){
//初始化,现在连接描述符connfd
int i;
p->maxi = -;
for(i = ;i<FD_SETSIZE;i++){//FD_SETSIZE=1024
p->clientfd[i] = -;
} //初始化,listenfd是select read set的唯一成员
p->maxfd = listenfd;
FD_ZERO(&p->read_set);
FD_SET(listenfd,&p->read_set);
} void add_client(int connfd, pool *p){
int i;
p->nready--; for(i = ;i<FD_SETSIZE;i++){//发现一个可用的插槽slot
if(p->clientfd[i] < ){
//添加一个连接描述符到池子pool
p->clientfd[i] = connfd;
Rio_readinitb(&p->clientrio[i],connfd); //添加描述符到描述符集合set
FD_SET(connfd,&p->read_set); //更新最大描述符,更新pool highwater mark,水位标志
if(connfd > p->maxfd){
p->maxfd = connfd;
} if(i > p->maxi){
p->maxi = i;
} break;
}//if
}//for if(i==FD_SETSIZE){//FD_SETSIZE=1024
app_error("add_client error: too many clients\n");
}
} void check_clients(pool *p){
int i,connfd,n;
char buf[MAXLINE]; rio_t rio; for(i = ;(i<= p->maxi)&&(p->nready > );i++){
connfd = p->clientfd[i];
rio = p->clientrio[i]; //如果描述符准备好了,从这个描述符回送echo一个文本行
if((connfd>) && (FD_ISSET(connfd,&p->ready_set))){
p->nready--;
if((n=Rio_readlineb(&rio,buf,MAXLINE))!=){
byte_cnt += n;
printf("sever received %d (%d total) bytes on fd %d\n",
n,byte_cnt,connfd);
Rio_writen(connfd,buf,n);
}//if //检测到EOF,从池子中移走描述符
else{
Close(connfd);
FD_CLR(connfd,&p->read_set);
p->clientfd[i] = -;
}
}
}
}

如果我们开启一个server端,将两个client端来连接,可以发现,connfd的大小是有规律的,server为第一个client分配的connfd是4,server为第二个client分配的connfd是5;因为之前的描述符分别是stdin(0),stdout(1),stderr(2),listenfd(3)

[lizhen@dhcp-- Debug]$ ./Server
listending
listened
inited_pooled
to be accepted
sever received ( total) bytes on fd
to be accepted
sever received ( total) bytes on fd
###########################################第一个client
[lizhen@dhcp-- Debug]$ ./Client localhost ###########################################第二个client
[lizhen@dhcp-- Debug]$ ./Client localhost
kkkkkkkkk
kkkkkkkkk

代码解读:

main函数中通过调用init_pool初始化池子,

服务器进入无限循环,在循环的每次迭代中,服务器调用select函数来检测两种不同类型的输入事件:1,来自一个新客户端的连接请求到达 2,一个已存在
的客户端的connfd准备好可以读了;

当一个连接请求到达时,服务器打开连接,接着调用add_client函数,将该客户端添加到池子中

最后服务器调用check_clients函数,把来自每个准备好的connfd的一个文本回送出去。

 int main(int argc,char* argv[]){
int listenfd, connfd, port;
socklen_t clientlen = sizeof(struct sockaddr_in);
struct sockaddr_in clientaddr;
static pool pool; if(argc != ){
fprintf(stderr,"usage: %s <port>\n",argv[]);
exit();
} port = atoi(argv[]); printf("listending\n");
listenfd = Open_listenfd(port);
printf("listened\n");
init_pool(listenfd, &pool);
printf("inited_pooled\n"); while(){
//等待listening / connected描述符变为就绪
pool.ready_set = pool.read_set;
pool.nready = Select(pool.maxfd+,&pool.ready_set, NULL,NULL,NULL); //如果listenfd 就绪ready,添加新的client到pool中
if(FD_ISSET(listenfd,&pool.ready_set)){
printf("to be accepted\n");
connfd = Accept(listenfd, (SA*)&clientaddr,&clientlen);
add_client(connfd,&pool);
} //从一个就绪ready的连接描述符connfd,回送echo一个文本行
check_clients(&pool);
}//while
}

init_pool函数初始client池子,clientfd数组表示connfd的集合,其中整数-1表示一个可用的槽位;

初始化时,connfd集合是空的,而且listendf是select读集合read_set中唯一的描述符;

void init_pool(int listenfd, pool *p){
//初始化,现在连接描述符connfd
int i;
p->maxi = -;
for(i = ;i<FD_SETSIZE;i++){//FD_SETSIZE=1024
p->clientfd[i] = -;
} //初始化,listenfd是select read set的唯一成员
p->maxfd = listenfd;
FD_ZERO(&p->read_set);
FD_SET(listenfd,&p->read_set);
}

add_client函数添加一个新的客户端到活动客户端池子中,

在clientfd数组中找到一个空槽位后,服务器将这个connfd添加到数组中,并初始化相应的RIO读缓冲区,这样就可以对这个描述符调用rio_readlineb

然后,将这个connfd添加到select读集合read_set,并更新该池的一些全局属性

maxfd变量记录了select的最大描述符

maxi变量记录的是clientfd数组的最大索引,这样check_clients函数可以无需搜索整个数组(这个数组1024元素)了;

 void add_client(int connfd, pool *p){
int i;
p->nready--; for(i = ;i<FD_SETSIZE;i++){//发现一个可用的插槽slot
if(p->clientfd[i] < ){
//添加一个连接描述符到池子pool
p->clientfd[i] = connfd;
Rio_readinitb(&p->clientrio[i],connfd); //添加描述符到描述符集合set
FD_SET(connfd,&p->read_set); //更新最大描述符,更新pool highwater mark,水位标志
if(connfd > p->maxfd){
p->maxfd = connfd;
} if(i > p->maxi){
p->maxi = i;
} break;
}//if
}//for if(i==FD_SETSIZE){//FD_SETSIZE=1024
app_error("add_client error: too many clients\n");
}
}

check_clients函数 回送echo来自每个准备好的connfd的一个文本行

如果成功从connfd读取了一个文本行,那么我们就该将该文本行回送到client

如果因为客户端关闭了这个连接中它的那一端(client自身),检测到EOF,那么我们将关闭这边的连接端,并从池子中清除掉这个描述符;

 void check_clients(pool *p){
int i,connfd,n;
char buf[MAXLINE]; rio_t rio; for(i = ;(i<= p->maxi)&&(p->nready > );i++){
connfd = p->clientfd[i];
rio = p->clientrio[i]; //如果描述符准备好了,从这个描述符回送echo一个文本行
if((connfd>) && (FD_ISSET(connfd,&p->ready_set))){
p->nready--;
if((n=Rio_readlineb(&rio,buf,MAXLINE))!=){
byte_cnt += n;
printf("sever received %d (%d total) bytes on fd %d\n",
n,byte_cnt,connfd);
Rio_writen(connfd,buf,n);
}//if //检测到EOF,从池子中移走描述符
else{
Close(connfd);
FD_CLR(connfd,&p->read_set);
p->clientfd[i] = -;
}//if-else
}//if
}//for
}

select函数检测输入事件,add_client函数创建一个新的逻辑流(状态机),check_clients函数通过回送输入行来执行状态转移,而且当客户端完成文本发送时,还要删除这个状态机。

IO多路复用技术的优劣:

好处:

1,它比基于进程的设计给了程序员更多的对程序行为的控制,例如我们可以写一个事件驱动的并发服务器,为某些客户端提供它们需要的服务;这对进程并发服务器来说很难。

2,一个基于IO多路复用的事件驱动服务器是运行在单一进程上下文中的,因此每个逻辑流都能访问该进程的全部地址空间,所以共享数据很容易。

3,与单进程运行相关的,可以使用GDB来调试并发服务器,就像顺序程序那样

4,事件驱动程序比基于进程的设计要高效的过,不需要切换进程上下文来调度新的流

缺点:

1,编码复杂,随着并发粒度的减小,复杂性还会上升。粒度指的是,每个逻辑流每个时间片执行的指令数量,在这个例子中,粒度就是杜伊个完整的文本行所需要的指令数量,
因为只要有一个逻辑流忙于读一个文本行,其他逻辑流就不可能有进展。但是这会容易因此 client发送部分文本然后快开的,恶意攻击。

2,不能充分利用多核处理器

—————————————————————————–

基于线程的并发编程

线程就是运行在进程上下文中的逻辑流。

只要不显示开辟新线程,那么我们的进程就只有一个线程;

每个进程都有自己的进程上下文,包括一个唯一的整数线程ID(tid),栈,栈指针,程序计数器,通用目的计数器和条件码,所有的运行都在一个进程里的线程共享该进程的整个虚拟地址空间。

同进程一样,线程由内核自动调度,并且内核通过一个整数id来表示线程

同基于IO多路复用的流一样,多个线程运行在单一进程的上下文中,因此共享进程虚拟地址空间的所有内容,包括它的code,data,heap,shared library和打开的文件。

每个进程开始时都是单一线程,称为主线程;某一时刻,主线程创建了一个对等线程,从这个时间开始,两个线程开始并发执行,最后因为主线程执行一个慢速系统调用,例如read/sleep,或者因为它被系统的时间间隔计时器中断,控制就会通过上下文切换传递到对等线程,对等线程执行一段时间,然后控制传递给主线程,go on。

—-线程与进程的不同点:

1,因为一个线程的上下文要比一个进程的上下文要小得多,线程的上下文切换要比进程的上下文切换快的多

2,线程不像进程那样,不是按照严格的父子层次来组织的。和一个进程相关的线程 组成一个对等线程池,独立于其他线程创建的线程。(对等意味着,任一线程都可以杀死其他对等的thread)

3,主线程和其他线程的区别,仅在于它总是进程中的第一个要运行的线程;对等线程池的概念的意思:一个线程可以杀死它的任何对等线程,或者等待它的任一对等线程终止。

4,每个对等线程共享数据

———-Posix线程,主要学习它

Pthreads定一个大约60个函数,允许程序创建、杀死、回收线程,与对等线程安全共享数据,还可以通知对等线程系统状态的变化。

/*
* hello.c - Pthreads "hello, world" program
*/
/* $begin hello */
#include "csapp.h"
void *thread(void *vargp); //line:conc:hello:prototypeint main() //line:conc:hello:main
{
pthread_t tid; //line:conc:hello:tid
Pthread_create(&tid, NULL, thread, NULL); //line:conc:hello:create
Pthread_join(tid, NULL); //line:conc:hello:join
exit(); //line:conc:hello:exit
}void *thread(void *vargp) /* thread routine */ //line:conc:hello:beginthread
{
printf("Hello, world!\n");
return NULL; //line:conc:hello:return
} //line:conc:hello:endthread
/* $end hello */

线程的代码和本地数据呗封装在一个线程例程中,每个例程接受一个通用指针作为输入,并返回一个通用指针。如果传递更多个参数给线程例程,我们可以将参数放在一个结构中,并传递一个指向该结构的指针。如果想要例程返回多个参数,可以返回指向结构的指针。

当对pthread_create的调用返回时,主线程和新创建的对等线程同时运行,并且tid包含新线程的id

通过pthread_join,主线程等待对等线程(只有tid指定的线程)终止。

[libevent ]   http://www.ibm.com/developerworks/cn/aix/library/au-libev/

相关推荐
python开发_常用的python模块及安装方法
adodb:我们领导推荐的数据库连接组件bsddb3:BerkeleyDB的连接组件Cheetah-1.0:我比较喜欢这个版本的cheeta…
日期:2022-11-24 点赞:878 阅读:8,983
Educational Codeforces Round 11 C. Hard Process 二分
C. Hard Process题目连接:http://www.codeforces.com/contest/660/problem/CDes…
日期:2022-11-24 点赞:807 阅读:5,500
下载Ubuntn 17.04 内核源代码
zengkefu@server1:/usr/src$ uname -aLinux server1 4.10.0-19-generic #21…
日期:2022-11-24 点赞:569 阅读:6,344
可用Active Desktop Calendar V7.86 注册码序列号
可用Active Desktop Calendar V7.86 注册码序列号Name: www.greendown.cn Code: &nb…
日期:2022-11-24 点赞:733 阅读:6,127
Android调用系统相机、自定义相机、处理大图片
Android调用系统相机和自定义相机实例本博文主要是介绍了android上使用相机进行拍照并显示的两种方式,并且由于涉及到要把拍到的照片显…
日期:2022-11-24 点赞:512 阅读:7,761
Struts的使用
一、Struts2的获取  Struts的官方网站为:http://struts.apache.org/  下载完Struts2的jar包,…
日期:2022-11-24 点赞:671 阅读:4,838