C++网络编程(六):多线程并发服务器

引入线程

C++网络编程(四):多进程并发服务器一文中,使用fork函数实现了多进程并发服务器,但是也提到了一些问题:

  • fork是昂贵的。fork时需要复制父进程的所有资源,包括内存映象、描述字等;
  • 目前的实现使用了一种写时拷贝(copy-on-write)技术,可有效避免昂贵的复制问题,但fork仍然是昂贵的;
  • fork子进程后,父子进程间、兄弟进程间的通信需要进程间通信IPC机制,给通信带来了困难; 多进程在一定程度上仍然不能有效地利用系统资源
  • 系统中进程个数也有限制。

除此之外最重要的还有一点:每秒少则数十次、多则上千次的“上下文切换”是创建进程的最大开销。

为了保持多进程优点并克服缺点,我们需要引入线程,线程具有以下优点

  • 线程的创建和上下文切换比进程更快
  • 线程间交换数据时无需特殊技术

具体原因如下:多个线程之间会共享全局变量和堆等资源,具体可见多线程之间共享哪些资源?,这种方式可以带来上述的两种优点:

  • 上下文切换时不需要切换全局数据区和堆
  • 可以利用全局数据区和堆交换数据

线程创建及运行

线程具有单独的执行流,需要单独定义线程的main函数,还需要请求操作系统在单独的执行流中执行该函数,创建线程的函数如下:

#include<pthread.h>
int pthread_create(
	pthread_t * restrict thread,const pthread_attr_t * restrict attr,
	void * (*start_routine)(void *),void * restrict arg
);
返回值:成功时返回0,失败时返回其它值

参数:
thread:保存新创建线程ID的变量地址值。
attr:用于传递线程属性的参数,传递NULL时创建默认属性的线程
start_routine:相当于线程main函数的、在单独执行流中执行的函数地址值(函数指针)。
arg:通过第三个参数传递调用函数时包含传递参数信息的变量地址值。

代码示例:

thread1.c

#include<stdio.h>
#include<pthread.h>
void *thread_main(void *arg);
int main(){
   
	pthread_t t_id;
	int thread_param=5;
	if(pthread_create(&t_id,NULL,thread_main,(void*)&thread_param)!=0){
   
		puts("pthread_create() error");
		return -1;
	}
	sleep(10);
	puts("end of main");
	return 0;
}
void *thread_main(void *arg){
   
	int i;
	int cnt=*((int*)arg);
	for(i=0;i<cnt;i++){
   
		sleep(1);
		puts("running thread");
	}
	return NULL;
}

使用以下命令编译运行(-l用于指定动态链接库):

gcc thread1.c -o thread1 -lpthread
./thread1

运行结果:

(注:linux的sleep以秒为单位)

在这里插入图片描述

thread1.c的执行流程如下:

在这里插入图片描述
显然,如果我们把上面代码中的sleep(10)改成sleep(2),就不会输出5次“running thread”了,因为main函数返回后整个进程将被销毁,执行过程将变成下面这样:

在这里插入图片描述
由此可以看出,为线程提供足够的运行时间是很重要的一件事情,但如果我们使用sleep设定一个时间就相当于要预测程序的执行流程,这是不可能准确预测的事情。

因此,我们不用sleep函数,而是通常利用pthread_join函数来控制线程的执行流,如下:

#include<pthread.h>
int pthread_join(pthread_t thread,void ** status);
返回值:成功返回0,失败返回其他值
参数:
thread:该参数值ID的线程终止后才会从函数返回
status:保存线程的main函数返回值的指针变量地址值

调用该函数的进程或线程会进入等待状态,直到第一个参数为ID的线程终止为止,而且可以得到线程的main函数返回值。


代码示例:

thread2.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
void *thread_main(void *arg);

int main(int argc, char *argv[])
{
   
    pthread_t t_id;
    int thread_param = 5;
    void *thr_ret;
    // 请求创建一个线程,从 thread_main 调用开始,在单独的执行流中运行。同时传递参数
    if (pthread_create(&t_id, NULL, thread_main, (void *)&thread_param) != 0)
    {
   
        puts("pthread_create() error");
        return -1;
    }
    //main函数将等待 ID 保存在 t_id 变量中的线程终止
    if (pthread_join(t_id, &thr_ret) != 0)
    {
   
        puts("pthread_join() error");
        return -1;
    }
    printf("Thread return message : %s \n", (char *)thr_ret);
    free(thr_ret);
    return 0;
}
void *thread_main(void *arg) //传入的参数是 pthread_create 的第四个
{
   
    int i;
    int cnt = *((int *)arg);
    char *msg = (char *)malloc(sizeof(char) * 50);
    strcpy(msg, "Hello,I'am thread~ \n");
    for (int i = 0; i < cnt; i++)
    {
   
        sleep(1);
        puts("running thread");
    }
    return (void *)msg; //返回值是 thread_main 函数中内部动态分配的内存空间地址值
}

使用以下命令编译运行(-l用于指定动态链接库):

gcc thread2.c -o thread2 -lpthread
./thread1

运行结果:
在这里插入图片描述
执行流程图如下:

在这里插入图片描述

线程安全

我们需要考虑多个线程同时执行临界区代码带来的问题,我们可将函数分为:

  • 线程安全函数
  • 非线程安全函数

线程安全函数被多个线程同时调用也不会引发问题,在linux中线程安全函数的名称后缀通常为_r,通过声明头文件前定义_REENTRANT宏可以自动将函数名gethostbyname变成gethostbyname_r,具体可见-D_REENTRANT 宏作用

同时我们可以不必为了上述宏定义特意添加#define语句,可以在编译时通过添加-D_REENTRANT选项定义宏。

如:

gcc -D_REENTRANT mythread.c -o mthread -lpthread

工作线程模型

接下来介绍创建多个线程的情况。

比如我们要计算1到10的和,创建两个线程,一个线程计算1到5的和,另一个线程计算6-10的和,main函数只负责输出运算结果。这种编程模型叫做“工作线程模型”,计算1到5之和与计算6到10之和的线程将成为main线程管理的工作。

执行流程如下:

在这里插入图片描述

代码示例:

注意以下代码有临界区相关问题

#include <stdio.h>
#include <pthread.h>
void *thread_summation(void 
  • 1
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
《Linux多线程服务端编程:使用muduo C++网络库》主要讲述采用现代C++在x86-64 Linux上编写多线程TCP网络服务程序的主流常规技术,重点讲解一种适应性较强的多线服务器编程模型,即one loop per thread。 目 录 第1部分C++ 多线程系统编程 第1章线程安全的对象生命期管理3 1.1当析构函数遇到多线程. . . . . . . . . . . . . . . . .. . . . . . . . . . . 3 1.1.1线程安全的定义. . . . . . . . . . . . . . . . .. . . . . . . . . . . 4 1.1.2MutexLock 与MutexLockGuard. . . . . . . . . . . . . . . . . . . . 4 1.1.3一个线程安全的Counter 示例.. . . . . . . . . . . . . . . . . . . 4 1.2对象的创建很简单. . . . . . . . . . . . . . . . .. . . . . . . . . . . . . . 5 1.3销毁太难. . . . . . . . . . . . . . . . . . . .. . . . . . . . . . . . . . . . 7 1.3.1mutex 不是办法. . . . . . . . . . . . . . . . . . . .. . . . . . . . 7 1.3.2作为数据成员的mutex 不能保护析构.. . . . . . . . . . . . . . 8 1.4线程安全的Observer 有多难.. . . . . . . . . . . . . . . . . . . . . . . . 8 1.5原始指针有何不妥. . . . . . . . . . . . . . . . .. . . . . . . . . . . . . . 11 1.6神器shared_ptr/weak_ptr . . . . . . . . . .. . . . . . . . . . . . . . . . 13 1.7插曲:系统地避免各种指针错误. . . . . . . . . . . . . . . . .. . . . . . 14 1.8应用到Observer 上.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16 1.9再论shared_ptr 的线程安全.. . . . . . . . . . . . . . . . . . . . . . . . 17 1.10shared_ptr 技术与陷阱. . . .. . . . . . . . . . . . . . . . . . . . . . . . 19 1.11对象池. . . . . . . . . . . . . . . . . . . .. . . . . . . . . . . . . . . . . 21 1.11.1enable_shared_from_this . . . . . . . . . . . . . . . . . . . . . . 23 1.11.2弱回调. . . . . . . . . . . . . . . . . . . .. . . . . . . . . . . . . 24 1.12替代方案. . . . . . . . . . . . . . . . . . . .. . . . . . . . . . . . . . . . 26 1.13心得与小结. . . . . . . . . . . . . . . . . . . .. . . . . . . . . . . . . . . 26 1.14Observer 之谬. . . .. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28 第2章线程同步精要 2.1互斥器(mutex). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32 2.1.1只使用非递归的mutex . . . . . . . . . . . . . .. . . . . . . . . . 33 2.1.2死锁. . . . . . . . . . . . . . . . . . . .. . . . . . . . . . . . . . 35 2.2条件变量(condition variable). . . . . . . . . .
1.网络编程概要.mkv 2.一个TCP的简单实验.mkv 3.课程内容大纲.mkv 4.回顾基础的Sockets API.mkv 5.TTCP代码概览.mkv 6.使用TTCP进行网络传输性能测试.mkv 7.阻塞IO下的TTCP实验.mkv 8.TCP自连接.mkv 9.扩展练习.mkv 10.时钟概述.mkv 11.时钟精确度和校准.mkv 12.网络时间同步.mkv 13.Roundtrip代码分析.mkv 14.其他测试方案.mkv 15.UDP vs TCP.mkv 16.扩展知识.mkv 17.如何正确使用TCP.mkv 18.TCP使用的注意事项.mkv 19.多个版本的Netcat概览.mkv 20.第一个Netcat的实现.mkv 21.IO-multiplexing方式实现Netcat.mkv 22.使用非阻塞IO 1.mkv 23.使用非阻塞IO 2.mkv 24.进程监控概述.mkv 25.实现前要考虑的问题.mkv 26.procmon代码解析.mkv 27.dummyload实现原理和代码解析.mkv 28.procmon性能测试.mkv 29.知识扩展和总结.mkv 30.功能描述.mkv 31.数据结构设计与分析.mkv 32.数据结构代码解读.mkv 33.网络IO模型与代码解读.mkv 34.性能测试 1.mkv 35.性能测试 2.mkv 36.性能分析.mkv 37.定制数据结构以减小内存使用.mkv 38.数独求解服务简介.mkv 39.并发模型和测试工具.mkv 40.批处理模型及疑似内存泄露.mkv 41.内置性能监控.mkv 42.延迟测量与禁用Nagle_s算法.mkv 43.最大容量及伸缩性测试.mkv 44.延迟分布于请求数及并发模型的关系.mkv 45.过载保护.mkv 46.负载均衡.mkv 47.负载均衡实例.mkv 48.如何进一步适应生产环境.mkv 49.代码阅读1:客户端.mkv 50.代码阅读2:服务端.mkv 51.苏迪曼杯羽毛球比赛.mkv 52.记分系统设计.mkv 53.聊天服务器.mkv 54.聊天服务器代码改进.mkv 55.hub服务器[new!].mkv 56.设计难点[new!].mkv 57. TCP relay功能描述及Python实现.mkv 58. TCP半关连接.mkv 59. 非阻塞TCP relay实现.mkv 60. 源码及运行.mkv 61. 竞态条件及修复.mkv 62. SOCKS4a服务器实现.mkv 63. 非阻塞IO之外的选择.mkv 64. 用 GO 语言实现 TCP relay.mkv 65. 事件驱动与多线程的取舍.mkv 66. 第七层以外的实现方式.mkv 67. 正确理解TCP的可靠性.mkv 68. Muduo与C++11.mkv 69. N皇后问题及单机求解方法.mkv 70. 并行算法与MapReduce.mkv 71. RPC简介与接口定义.mkv 72. 代码实现与运行实例.mkv 73. Go语言客户端.mkv 74. RPC 负载均衡.mkv 75. 多机求平均数和中位数的算法.mkv 76. 代码实现及运行实例.mkv 77. 实现RCP框架:服务端.mkv 78. 实现RPC框架:客户端.mkv 79. 单词计数及按频度排序,单机算法.mkv 80. 单机版代码阅读.mkv 81. 多机单词计数算法与代码.mkv 82. 多机找出最常见的K个单词.mkv 83. 复活《TCPIP 详解第2卷》讲的4.4BSD协议栈.mkv 84. 课程总结.mkv muduo-examples-in-go-master.zip muduo-master.zip muduo-protorpc-cpp11.zip recipes-master.zip 官网地址-课程大纲 .txt

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值