线程

进程模型在处理用户请求的过程中,进程切换上下文的代价比较高,而,一种轻量级的模型可以处理多用户连接请求,那就是线程模型

线程(thread)是运行在进程中的一个“逻辑流”,现代操作系统都允许在单进程中运行多个线程。线程由操作系统内核管理。每个线程都有自己的上下文(context),包括一个可以唯一标识线程的ID(thread ID,或叫tid)、栈、程序计数器、寄存器等。在同一个进程中,所有的线程共享该进程的整个虚拟地址空间,包括代码、数据、堆、共享库等。

每个进程一开始就会产生一个线程,一般称为主线程,主线程可以再产生子线程,这样的主线程-子线程对可以叫做一个对等线程。

有多进程处理并发,为什么还需要多线程处理并发?

简单来说,就是在同一个进程下,线程上下文切换的开销要比进程小的多

如何理解上下文呢?

我们的代码被CPU执行的时候,是需要一些数据支持的,比如程序计数器告诉CPU代码执行到哪里了,寄存器里存了当前计算的一些中间值,内存里放置了一些当前用到的变量等,从一个计算场景,切换到另一个计算场景,程序计数器、寄存器等这些值重新载入新场景的值,就是线程的上下文切换。

主要线程函数

创建线程

pthread_create函数用来创建一个线程。

int pthread_create(pthread_t *tid, const pthread_attr_t *attr,
           void *(*func)(void *), void *arg); 返回:若成功则为0,若出错则为正的Exxx值

每个线程都有一个线程ID(tid)唯一标识,其数据类型为pthread_t,一般是unsigned int。pthread_create函数的第一个输出参数tid就代表了线程ID,如果创建成功,tid就返回正确的线程ID。

第二个参数:每个线程都会有很多属性,比如优先级,是否应该称为一个守护进程等,可通过pthread_attr_t来描述,一般不会特殊设置,可以指定这个参数为NULL。

第三个参数:为新线程的入口函数,该函数可以接收一个参数arg,类型为指针,如果想给线程入口函数传入多个值,那么需要把这些值包装成一个结构体,再把结构体的地址作为pthread_create的第四个参数,在线程入口函数内,再将该地址转为该结构体的指针对象。

在新线程的入口函数内,可以调用pthread_self函数返回线程的tid

pthread_t pthread_self(void)

终止线程

终止一个线程最直接的方法就是在父线程内调用pthread_exit函数

void pthread_exit(void *status)

当调用这个函数之后,父线程会等待其他所有的子线程终止,之后父线程自己终止。

也可以通过调用pthread_cancel来主动终止一个子线程,和pthread_exit不同的是,它可以指定某个子线程终止。

int pthread_cancel(pthread_t tid)

回收已终止线程的资源

通过调用pthread_join回收已终止线程的资源。

int pthread_join(pthread_t tid, void ** thread_return)

当调用pthread_join时,主线程会阻塞,直到对应tid的子线程自然终止。和pthread_cancel不同的是,它不会强迫子线程终止。

分离线程

一个线程的重要属性就是可结合的,或者是可分离的。一个可结合的线程是能够被其他线程杀死和回收资源的;而一个分离的线程不能被其他线程杀死或回收资源。一般来说,默认的属性是可结合的。

可通过调用pthread_detach函数来分离一个线程:

int pthread_detach(pthread_t tid)

在高并发的例子里,每个连接都由一个线程单独处理,在这种情况下,服务器程序并不需要对每个子线程进行终止,这样的话,每个子线程可以在入口函数开始的地方,把自己设置为分离的,这样就能在它终止后自动回收相关的线程资源了,就不需要调用 pthread_join 函数了。

每个连接一个线程处理

每次有新的连接到达后,就创建一个新线程

#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
#include <sys/poll.h>
#include <sys/socket.h>
#include <netinet/in.h> #define SERV_PORT 43211
#define LISTENQ 1024
#define INIT_SIZE 128
#define MAXLINE 1024
#define MAX_LINE 16384 extern void loop_echo(int); int tcp_server_listen(int port) {
int listenfd;
listenfd = socket(AF_INET, SOCK_STREAM, 0); struct sockaddr_in server_addr;
bzero(&server_addr, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(port); int on = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)); int rt1 = bind(listenfd, (struct sockaddr *) &server_addr, sizeof(server_addr));
if (rt1 < 0) {
perror( "bind failed ");
return -1;
} int rt2 = listen(listenfd, LISTENQ);
if (rt2 < 0) {
perror("listen failed ");
return -1;
} signal(SIGPIPE, SIG_IGN); return listenfd;
} void pthread_run(void *arg)
{
pthread_detach(pthread_self());
int fd = (int)arg;
loop_echo(fd);
} int main(int argc, char* argv[])
{
int listen_fd = tcp_server_listen(SERV_PORT);
pthread_t tid; while(1)
{
struct sockaddr_storage ss;
socklen_t slen = sizeof(ss);
int fd = accept(listen_fd, (struct sockaddr *)&ss, &slen);
if(fd < 0)
{
perror("accept failed");
return -1;
}
else
{
pthread_create(&tid, NULL, &pthread_run, (void *)fd);
}
}
}

在新线程入口函数thread_run里,使用了pthread_detach方法,将子线程转变为分离的,意味着子线程独自负责线程资源的回收。

loop_ehco程序如下,在接收客户端的数据后,再编码回送回去

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
#include <sys/poll.h>
#include <sys/socket.h>
#include <netinet/in.h> #define SERV_PORT 43211
#define LISTENQ 1024
#define INIT_SIZE 128
#define MAXLINE 1024 #define MAX_LINE 16384 char rot13_char(char c) {
if ((c >= 'a' && c <= 'm') || (c >= 'A' && c <= 'M'))
return c + 13;
else if ((c >= 'n' && c <= 'z') || (c >= 'N' && c <= 'Z'))
return c - 13;
else
return c;
} void loop_echo(int fd) {
char outbuf[MAX_LINE + 1];
size_t outbuf_used = 0;
ssize_t result;
while (1) {
char ch;
result = recv(fd, &ch, 1, 0); //断开连接或者出错
if (result == 0) {
break;
} else if (result == -1) {
perror("read error");
break;
} if (outbuf_used < sizeof(outbuf)) {
outbuf[outbuf_used++] = rot13_char(ch);
} if (ch == '\n') {
send(fd, outbuf, outbuf_used, 0);
outbuf_used = 0;
continue;
}
}
}

构建线程池处理多个连接

上述程序虽可以正常工作,但如果并发连接过多,就会引起线程的频繁创建和销毁,虽然说线程切换上下文开销不大,但这般频繁的创建销毁也还是会带来不小的开销。

可以使用预创建线程池的方式进行优化。在服务器启动时,可以先按照固定大小预创建多个线程,当有新连接建立时,往连接字队列里放置这个新连接描述字,线程池里的线程负责从连接字队列中取出连接描述字进行处理。



程序的关键在于连接字队列的设计,因为既有往队列中放置描述符的操作,也有从队列中取出描述符的操作。

需要引入两个重要概念,一个是锁mutex,一个是条件变量condition。加锁就是其他线程不能进入;条件变量则是在多个线程需要交互的情况下,用来线程间同步的原语。

#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
#include <sys/poll.h>
#include <sys/socket.h>
#include <netinet/in.h> #define SERV_PORT 43211
#define THREAD_NUMBER 4
#define BLOCK_QUEUE_SIZE 100
#define LISTENQ 1024 extern void loop_echo(int); typedef struct
{
/* data */
pthread_t thread_tid; //thread ID
long thread_count; // connections handled
}Thread; Thread *thread_array; typedef struct {
int number; //队列里的描述字最大个数
int *fd; //数组指针
int front; //当前队列的头位置
int rear; //当前队列的尾位置
pthread_mutex_t mutex; //锁
pthread_cond_t cond; //条件变量
}block_queue; int tcp_server_listen(int port) {
int listenfd;
listenfd = socket(AF_INET, SOCK_STREAM, 0); struct sockaddr_in server_addr;
bzero(&server_addr, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(port); int on = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)); int rt1 = bind(listenfd, (struct sockaddr *) &server_addr, sizeof(server_addr));
if (rt1 < 0) {
perror( "bind failed ");
return -1;
} int rt2 = listen(listenfd, LISTENQ);
if (rt2 < 0) {
perror("listen failed ");
return -1;
} signal(SIGPIPE, SIG_IGN); return listenfd;
} //初始化队列
void block_queue_init(block_queue *blockQueue, int number)
{
blockQueue->number = number;
blockQueue->fd = calloc(number,sizeof(int));
blockQueue->front = blockQueue->rear = 0;
pthread_mutex_init(&blockQueue->mutex, NULL);
pthread_cond_init(&blockQueue->cond, NULL);
} //往队列里放置一个描述字fd
void block_queue_push(block_queue *blockQueue, int fd)
{
//一定要先加锁,因为有多个线程需要读写队列
pthread_mutex_lock(&blockQueue->mutex);
//将描述字放到队列尾的位置
blockQueue->fd[blockQueue->rear] = fd;
//如果已经到最后,重置尾的位置
if(++blockQueue->rear == blockQueue->number)
{
blockQueue->rear = 0;
}
printf("push fd %d\n",fd);
//通知其他等待度的线程,有新的连接字符等待处理
pthread_cond_signal(&blockQueue->cond);
//解锁
pthread_mutex_unlock(&blockQueue->mutex); } //从队列里独处描述字进行处理
int block_queue_pop(block_queue *blockQueue)
{
//加锁
pthread_mutex_lock(&blockQueue->mutex);
//判断队列里没有新的连接字可以处理,就一直条件等待,直到有新的连接字入队列
while(blockQueue->front == blockQueue->rear)
{
pthread_cond_wait(&blockQueue->cond, &blockQueue->mutex);
}
//取出队列头的连接字
int fd = blockQueue->fd[blockQueue->front];
//如果已经到最后,重置头的位置
if(++blockQueue->front == blockQueue->number)
{
blockQueue->front = 0;
}
printf("pop fd %d",fd);
//解锁
pthread_mutex_unlock(&blockQueue->mutex);
//返回连接字
return fd;
}
void thread_run(void *arg)
{
pthread_t tid = pthread_self();
pthread_detach(tid); block_queue *blockQueue = (block_queue*)arg;
while(1)
{
int fd = block_queue_pop(blockQueue);
printf("get fd in thread, fd = %ld, tid = %ld\n",fd, tid);
loop_echo(fd);
}
} int main(int argc, char *argv[])
{
int listen_fd = tcp_server_listen(SERV_PORT); block_queue blockQueue;
block_queue_init(&blockQueue, BLOCK_QUEUE_SIZE); thread_array = calloc(THREAD_NUMBER, sizeof(Thread));
int i;
for(i = 0; i < THREAD_NUMBER; i++)
{
pthread_create(&(thread_array[i].thread_tid), NULL, &thread_run, (void *)&blockQueue);
} while(1)
{
struct sockaddr_storage ss;
socklen_t slen = sizeof(ss);
int fd = accept(listen_fd, (struct sockaddr *)&ss, &slen);
if(fd < 0)
{
perror("accept failed");
return -1;
}
else
{
block_queue_push(&blockQueue, fd);
}
}
return 0;
}

PS:

记得对操作进行加锁和解锁,通过pthread_mutex_lock和pthread_mutex_unlock来完成。

当工作线程没有描述字可用时,需要等待,通过调用pthread_cond_wait,所有的工作线程等待有新的描述字可达。

网络编程:阻塞I/O和线程模型的更多相关文章

  1. UNIX网络编程读书笔记:I/O模型(阻塞、非阻塞、I/O复用、信号驱动、异步)

    I/O模型 UNIX下可用的5种I/O模型: (1)阻塞I/O (2)非阻塞I/O (3)I/O复用(select和poll) (4)信号驱动I/O(SIGIO) (5)异步I/O 对于一个套接口上的 ...

  2. c++ 网络编程(九)LINUX/windows-IOCP模型 多线程超详细教程及多线程实现服务端

    原文作者:aircraft 原文链接:https://www.cnblogs.com/DOMLX/p/9661012.html 先讲Linux下(windows下在后面可以直接跳到后面看): 一.线程 ...

  3. 网络编程中select模型和poll模型学习(linux)

    一.概述 并发的网络编程中不管是阻塞式IO还是非阻塞式IO,都不能很好的解决同时处理多个socket的问题.操作系统提供了复用IO模型:select和poll,帮助我们解决了这个问题.这两个函数都能够 ...

  4. python之网络编程--锁、信号量、线程、队列

    一.线程,可以发现顺序执行比开线程执行时间要短.原因是,一个进程中的多线程处理,由于存在GIL,并且GIL中只能存在一个线程,加上线程又存在切换的问题,所以时间耗得多.想要解决这个问题,是开几个进程, ...

  5. Windows网络编程系列教程之四:Select模型

    讲一下套接字模式和套接字I/O模型的区别.先说明一下,只针对Winsock,如果你要骨头里挑鸡蛋把UNIX下的套接字概念来往这里套,那就不关我的事. 套接字模式:阻塞套接字和非阻塞套接字.或者叫同步套 ...

  6. [转载]Windows网络编程系列教程之四:Select模型

    原文:http://www.51see.com/asp/bbs/public/bp_show.asp?t_id=200308131152297103 讲一下套接字模式和套接字I/O模型的区别.先说明一 ...

  7. Linux网络编程学习(一) ----- 概论和Linux模型(第一章第二章)

    1.什么是计算机网络,通信方式是什么? 计算机网络就是通过通信线路相互连接的计算机的集合,主要通过双绞线.同轴电缆.电话线或者光缆等有形传输介质通信,还有就是通过激光.微波.卫星等实现无线通信 2.W ...

  8. 基于ASIO的协程与网络编程

    协程 协程,即协作式程序,其思想是,一系列互相依赖的协程间依次使用CPU,每次只有一个协程工作,而其他协程处于休眠状态.协程可以在运行期间的某个点上暂停执行,并在恢复运行时从暂停的点上继续执行. 协程 ...

  9. Java网络编程和NIO详解开篇:Java网络编程基础

    Java网络编程和NIO详解开篇:Java网络编程基础 计算机网络编程基础 转自:https://mp.weixin.qq.com/s/XXMz5uAFSsPdg38bth2jAA 我们是幸运的,因为 ...

  10. Python3 网络编程和并发编程总结

    目录 网络编程 开发架构 OSI七层模型 socket subprocess 粘包问题 socketserver TCP UDP 并发编程 多道技术 并发和并行 进程 僵尸进程和孤儿进程 守护进程 互 ...

随机推荐

  1. vue路由$router.push()的三种传参方式

  2. hbase - [02] 分布式安装部署

    一.角色规划 主机名 node01 node02 node03 node04 Zookeeper ○ ○ ○   NameNode ○ ○     JournalNode ○ ○ ○   DataNo ...

  3. 面试题30. 包含min函数的栈

    地址:https://leetcode-cn.com/problems/bao-han-minhan-shu-de-zhan-lcof/ <?php /** 定义栈的数据结构,请在该类型中实现一 ...

  4. 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!

    3月6日最新消息,阿里云通义千问官方宣布推出最新推理模型 QwQ-32B,这一模型仅有 32B 参数,但在效果上与拥有 671B 参数的 DeepSeek-R1 相媲美.如果你自己部署 DeepSee ...

  5. xss学习及xss-lab解题记录

    什么是XSS(跨站脚本攻击) SQL注入是服务端将用户输入的数据当成SQL代码去执行 XSS可以理解为服务端把用户输入的数据当成前端代码去执行 前端代码->主要是js代码 两个关键条件: 第一个 ...

  6. 谜一般的js,迷一般的console

    问题的来源,是关于事件对象的currentTarget的讨论,currentTarget是什么,嗯,很简单就是绑定了监听函数,并且当前监听函数正在执行的那个dom元素.本着踏实,实事求是,严以律己的态 ...

  7. halcon 入门教程(六) 图像匹配(基于形状的模板匹配)与缺陷检测区域定位

    原文作者:aircraft 原文链接:https://www.cnblogs.com/DOMLX/p/18783476 有兴趣可以多看其他的halcon教程 halcon 学习教程目录 本篇主要讲一下 ...

  8. SpringSecurity5(10-动态权限管理)

    授权流程 SpringSecurity 的授权流程如下: 拦截请求,已认证用户访问受保护的 web 资源将被 SecurityFilterChain 中的 FilterSecurityIntercep ...

  9. storm部署文档

    背景 这篇笔记原来是记录在印象笔记中的,没有发布到博客中,这次我重新整理一下发布上来,希望给读者以参考. Storm的部署手册 Zookeepr的部署 首先下载安装包:apache-zookeeper ...

  10. 【C语言】转义字符及其对应英文

    对于很多人来说,用转义字符都是熟能生巧,而不清楚为什么是那样的转义字符,所以我在这列了一个表,翻译了其对应的英文. 转义字符分为一般转义字符.八进制转义字符.十六进制转义字符. 一般转义字符:\0. ...