很多人在刚学socket时,都是在线程中connect,然后while(flag) read();要停止这个线程时将falg置false,再wait,甚至直接termination(这种方式终止线程的安全隐患不在这里论述)。
一般情况下的确没什么问题,但拿到一个真正的项目中时,就不太好了,这样写就很可能出现这样的情况:网络情况不好时,connect和read可能需要很长时间才能返回,将flag置false时,可能还需要等待很久(最大等待时间和平台有关),也许长达几分钟,特别是在GUI线程中去停止这个线程时,很可能会造成界面卡死。
那么该如何解决这种问题呢?我的做法是,在connect之前,将socket置为非阻塞模式,这时再使用connect时,会立即返回,我们判断它的返回值,如果没有连接成功(内核还在继续为我们建立连接),即使用select去监测它,发现它连接成功时,再不愿成阻塞模式。
这样做还不够,因为select也是阻塞函数,但select是可以监测多个文件描述符的,我们可以创建一个pipe(还有eventfd之类),让select同时监测pipe和socket,在我们需要停止connect时,向pipe的写端写入数据,此时select就会被唤醒,当我们发现是由pipe唤醒select时,即直接释放相关资源后退出线程。
对于read部分,我们一样使用select同时监测pipe和socket,使用同样的方法退出。
这种方法相对可以更快更安全的终止线程,示例代码如下(注意,以下代码纯记事本写的,可能存在错误,仅供参考):
int MyThread::connect() { int fd; /// 此处略过部分代码 /// unsigned long ul = 1; ioctl(fd, FIONBIO, &ul); /// 设置为非阻塞模式 if (connect(fd, (struct sockaddr *)&raddr, sizeof(struct sockaddr_in)) == -1) { int error = 0; int max_fd = ( fd > _pipe_fd[0] ? fd : _pipe_fd[0] ) + 1; fd_set set; FD_ZERO(&set); FD_SET(fd, &set); fd_set rset; FD_ZERO(&rset); FD_SET(_pipe_fd[0], &rset); /// 将pipe读端加入select,便于线程外随时退出select if ( ! (error = (select(max_fd, &rset, &set, NULL, NULL) <= 0) ) ) /// error == 0 !error == 1表示没发生错误,进一步检查错误 { if (FD_ISSET(fd, &set)) { int len = 0; getsockopt(fd + 1, SOL_SOCKET, SO_ERROR, (char*)&error, &len); } else if (FD_ISSET(_pipe_fd[0], &rset)) { /// 接收到退出命令 /// 如果退出后还需要复用到pipe,此处建议将pipe里的东西read出来以清空 error = -2; } else { /// 如果发生错误,或者由pipe唤醒,则终止连接 error = -1; } } if (error) { close(fd); return -1; } } ul = 0; ioctl(sockfd, FIONBIO, &ul); /// 还原为阻塞模式 return fd; } void MyThread::run() { int fd = connect(); if (fd < 0) { return; } fd_set rset; int ret; int max_fd = ( fd > _pipe_fd[0] ? fd : _pipe_fd[0] ) + 1; while (true) { FD_ZERO(&rset); FD_SET(fd, &rset); FD_SET(_pipe_fd[0], &rset); ret = select(fd + 1, &rset, NULL, NULL, NULL); if (ret < 0) { break; } if (ret) { if (FD_ISSET(_pipe_fd[0], &rset)) { break; } if (FD_ISSET(fd, &rset)) { read(); } } } close(fd); }
以种方法只是我目前的处理方式,应该还有更好的方式(有很多优秀的库目前我还没有去深入学习)。
高手们可以飘过了,不过最好留下点指导吧。