网络编程实战
盛延敏
前大众点评云平台首席架构师
44207 人已学习
新⼈⾸单¥59
登录后,你可以任选4讲全文学习
课程目录
已完结/共 40 讲
网络编程实战
15
15
1.0x
00:00/00:00
登录|注册

26 | 使用阻塞I/O和线程模型:换一种轻量的方式

pthread_detach
pthread_join
pthread_cancel
pthread_exit
pthread_create
hello-world计数器应用的幸运结果
连接字队列优化
线程在高性能网络服务器中的重要性
线程的轻量性
工作线程主循环
主程序
队列设计
loop_echo程序
线程入口函数
服务器端程序改造
线程函数
主线程和子线程
线程的上下文
线程的定义
思考题
总结
构建线程池处理多个连接
每个连接一个线程处理
POSIX线程模型
线程概述
线程模型

该思维导图由 AI 生成,仅供参考

你好,我是盛延敏,这里是网络编程实战第 26 讲,欢迎回来。
在前面一讲中,我们使用了进程模型来处理用户连接请求,进程切换上下文的代价是比较高的,幸运的是,有一种轻量级的模型可以处理多用户连接请求,这就是线程模型。这一讲里,我们就来了解一下线程模型。
线程(thread)是运行在进程中的一个“逻辑流”,现代操作系统都允许在单进程中运行多个线程。线程由操作系统内核管理。每个线程都有自己的上下文(context),包括一个可以唯一标识线程的 ID(thread ID,或者叫 tid)、栈、程序计数器、寄存器等。在同一个进程中,所有的线程共享该进程的整个虚拟地址空间,包括代码、数据、堆、共享库等。
在前面的程序中,我们没有显式使用线程,但这不代表线程没有发挥作用。实际上,每个进程一开始都会产生一个线程,一般被称为主线程,主线程可以再产生子线程,这样的主线程 - 子线程对可以叫做一个对等线程。
你可能会问,既然可以使用多进程来处理并发,为什么还要使用多线程模型呢?
简单来说,在同一个进程下,线程上下文切换的开销要比进程小得多。怎么理解线程上下文呢?我们的代码被 CPU 执行的时候,是需要一些数据支撑的,比如程序计数器告诉 CPU 代码执行到哪里了,寄存器里存了当前计算的一些中间值,内存里放置了一些当前用到的变量等,从一个计算场景,切换到另外一个计算场景,程序计数器、寄存器等这些值重新载入新场景的值,就是线程的上下文切换。
确认放弃笔记?
放弃后所记笔记将不保留。
新功能上线,你的历史笔记已初始化为私密笔记,是否一键批量公开?
批量公开的笔记不会为你同步至部落
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
  • 深入了解
  • 翻译
    • 英语
    • 中文简体
    • 中文繁体
    • 法语
    • 德语
    • 日语
    • 韩语
    • 俄语
    • 西班牙语
    • 阿拉伯语
  • 解释
  • 总结

本文介绍了使用线程模型处理多用户连接请求的方法,并详细介绍了POSIX线程模型的相关函数。通过一个简单的例子程序展示了如何使用线程模型处理多用户连接请求,以及如何在服务器端程序中创建新线程来处理每个连接,实现了多个并发连接的处理。文章还介绍了如何使用预创建线程池的方式来优化服务器端程序,减少线程创建和销毁的开销。关键的连接字队列设计和实现也得到了详细讲解,包括锁和条件变量的使用。通过代码示例和解释,读者可以快速了解线程模型的使用方法和优势,以及在网络编程实践中的应用。总的来说,本文对于需要处理多用户连接请求的网络编程实践具有一定的参考价值。文章还提出了思考题,鼓励读者深入思考和交流。

仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《网络编程实战》
新⼈⾸单¥59
立即购买
登录 后留言

全部留言(41)

  • 最新
  • 精选
  • 丷王传奇丷
    第二题:可能会得到比正确的值小的值 i++大致分为三个步骤: 1、从内存读出i的值到寄存器 2、操作寄存器加1 3、将寄存器值写到i内存 多个线程去操作同一个全局变量的时候,可能某个线程在第二步的时候切换到另一个线程,这样就导致少加了。比如线程A 在i=1 的时候在第二步,这个时候寄存器加1值为2,在这个时候切换到线程B ,由于线程A还没有把2写到i里面,使用B读出来还是1,自增写到i里面,i为2,在切换到线程A,线程A将寄存器里面的2写到i,这样就少加了一次。

    作者回复: 多线程程序设计的常见坑。解释到位,赞。

    2020-02-13
    4
    20
  • 忆水寒
    第一道,其实这里使用的生产者-消费者模型,可以使用扩容策略解决fd存放的问题。 第二题,在并发场景下很容易造成计算结果的不准确。因为这里面是两个线程各执行1000次。实际上很大结果是少于2000的结果。解决方法可以加上锁或volitate关键字(解决可见行问题)。

    作者回复: 👍

    2020-03-15
    4
    16
  • 沉淀的梦想
    关于队列满的情况,额外加一个cond,表示队列未满条件就可以了。 typedef struct { int number; int *fd; int front; int rear; // 队列中当前元素数目 int count; pthread_mutex_t mutex; pthread_cond_t not_empty; // 队列未满条件 pthread_cond_t not_full; } block_queue; void block_queue_init(block_queue *blockQueue, int number) { blockQueue->number = number; blockQueue->fd = calloc(number, sizeof(int)); blockQueue->count = blockQueue->front = blockQueue->rear = 0; pthread_mutex_init(&blockQueue->mutex, NULL); pthread_cond_init(&blockQueue->not_empty, NULL); pthread_cond_init(&blockQueue->not_full, NULL); } void block_queue_push(block_queue *blockQueue, int fd) { pthread_mutex_lock(&blockQueue->mutex); while (blockQueue->count == blockQueue->number){ //队列满 pthread_cond_wait(&blockQueue->not_full, &blockQueue->mutex); } blockQueue->fd[blockQueue->rear] = fd; if (++blockQueue->rear == blockQueue->number) { blockQueue->rear = 0; } blockQueue->count++; printf("push fd %d", fd); pthread_cond_signal(&blockQueue->not_empty); 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->not_empty, &blockQueue->mutex); int fd = blockQueue->fd[blockQueue->front]; if (++blockQueue->front == blockQueue->number) { blockQueue->front = 0; } blockQueue->count--; printf("pop fd %d", fd); pthread_cond_signal(&blockQueue->not_full); // 解锁 pthread_mutex_unlock(&blockQueue->mutex); return fd; }

    作者回复: 不错的想法。

    2019-10-07
    5
    16
  • Steiner
    请问block_queue_pop的pthread_cond_wait为什么要放在while而不是if中

    作者回复: 好问题。 这是为了确保被pthread_cond_wait唤醒之后的线程,确实可以满足继续往下执行的条件。如果没有while循环的再次确认,可能直接就往下执行了。

    2019-10-14
    2
    12
  • 刘丹
    block_queue_push未考虑队列满的情况,可以在本函数里先判断是否队列满了,如果满就按某个策略扩容,例如扩大1.5或2倍。扩容失败或者容量超过最大值,就返回失败。

    作者回复: 很好的方法。

    2019-10-07
    5
  • 林燕
    老师,请问一下,我跑了thread02这个程序。在没有客户端连上来的情况下,我发现四个子线程都执行到了block_queqe_pop函数里面,并都成功执行完pthread_mutex_lock,阻塞在后面的while循环里面。这里我想问的是四个子线程,不是最多只应该有一个能拿到互斥量,其他三个都应该阻塞在pthread_mutex_lock函数中么?

    作者回复: 问题的关键是pthread_cond_wait这个函数,事实上,这个函数使得当前线程进入休眠,并且释放当前线程持有的互斥锁。而当调用线程后来从pthread_cond_wait返回时,该线程再次持有该互斥锁。这就是为什么四个子线程都可以跑完pthread_mutex_lock,然后都在pthread_cond_wait这个函数中休眠等待的原因。

    2021-04-25
    3
  • 愚笨的老鼠
    这个线程函数里面,没有调用close关闭connfd,导致,很多很多socket状态不对,没有正常进入time_wait状态 tcp4 0 0 127.0.0.1.43211 127.0.0.1.53703 CLOSE_WAIT tcp4 0 0 127.0.0.1.53703 127.0.0.1.43211 FIN_WAIT_2 tcp4 0 0 127.0.0.1.43211 127.0.0.1.53695 CLOSE_WAIT tcp4 0 0 127.0.0.1.53695 127.0.0.1.43211 FIN_WAIT_2

    作者回复: 确实如此,需要在循环函数里处理完加上close(fd)的操作。

    2020-03-20
    3
  • 愚笨的老鼠
    有个地方不太理解,一开始初始状态下,线程pop加锁了,为什么,有accept时候,执行push的时候获取锁怎么能成功呢,不是锁已经被pop用了,还没释放呢

    作者回复: 问题的关键在于pthread_cond_wait的语义,在消费者线程中,通过wait等待连接处理,这个时候accept之后执行 push获取锁是可以的,并且通过signal的方式让消费者线程苏醒过来处理。 在绝大部分时刻,消费者线程都是在wait状态的,push一开始的lock锁只是一个非常短暂的check动作。

    2020-03-21
    2
    2
  • supermouse
    老师我想问一下,线程的创建销毁和进程的创建销毁哪个开销比较大?

    作者回复: 我觉得是线程。

    2020-02-24
    4
    2
  • 阿尔卑斯
    我自定义阻塞队列(多了个cnt计数元素个数,和条件变量分开来,实现起来清晰明了简单多了) typedef struct { int size; //队列容量 int *pFd; //存储队列元素,动态分配 int cnt; //队列当前元素个数 int pushIdx; //入队元素索引 int popIdx; //出对元素索引 pthread_mutex_t mutex; //锁 pthread_cond_t noFull; //非满,即队列元素个数cnt从size值,变成size - 1时的触发条件 pthread_cond_t noEmpty;//非空,即队列元素个数cnt从0值,变成1时的触发条件 }BlockQueue;

    作者回复: 可以的。

    2019-11-06
    2
收起评论
显示
设置
留言
41
收藏
沉浸
阅读
分享
手机端
快捷键
回顶部