十二:多线程服务端实现

1 理解线程

1.1 引入线程背景

多进程模型有如下缺陷:

  • 创建进程的过程会带来一定的开销。
  • 为了完成进程间的数据交换,需要特殊的IPC技术
  • 每秒少则数十次,多则数千次的‘上下文切换’是创建进程时最大的开销

运行程序前需要将相应进程信息读入内存,如果运行进程A后需要紧接着运行进程B,就应该将进程A相关信息移出内存,并读入进程B相关信息。这就是上下文切换。但此时进程A的数据也将被移动到硬盘,所以上下文切换需要很长时间。即使通过优化加快速度,也会存在一定的局限。

  为了保持多进程的优点,同时在一定程度上克服其缺点,引入了线程,线程具有以下优点。

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

1.2 线程和进程的差异

  每个进程的内存空间都由保存全局变量的“数据区”,向malloc等函数的动态分配提供空间的堆(Heap)、函数运行时使用的(Stack)构成,每个进程都拥有这种独立空间。
  但是如果只是想获得多个代码执行流,则不需要将内存完全分离(对系统负担很大),只需要分离栈区域。这样做有以下优势。

    • 上下文切换时不需要切换数据区和堆
    • 可以利用数据区和堆交换数据
这就是线程。线程为了保持多条代码执行流而隔离了栈区域,就像下面这样:

多个线程将共享数据区和堆。为了保持这种结构,线程将在进程内创建并运行,也就是说可以将进程和线程定义为如下形式:

  • 进程:在操作系统构成单独执行流的单位
  • 线程:在进程构成单独执行流的单位

操作系统、进程、线程的关系如下:

2 线程创建和运行

POSIX是Portable Operating System lnterface for Computer Environment (适用于计算机环境的
可移植操作系统接口)的简写,是为了提高UNIX系列操作系统间的移植性而制定的API规范。下
面要介绍的线程创建方法也是以POSIX标准为依据的。因此,它不仅适用于Linux ,也适用于大
部分UNIX系列的操作系统。

2.1 线程的创建和执行流程

#include <pthread.h>int pthread_create(pthread_t* restrict thread,  //保存新创建线程ID的变量地址值const pthread_attr_t* restrict attr, //传递线程属性的参数,传递NULL时,创建默认属性线程void* (*start_routine)(void*),  //在单独执行流中执行的函数地址值(函数指针)void* restrict arg);
注意编译时添加-lpthread
#include <stdio.h>
#include <pthread.h>void* thread_main(void* arg) {int i = 0;int cnt = *((int*)arg);for ( ; i < cnt; i++) {sleep(1);puts("running thread");}return NULL;
}int main(int argc, char* argv[]) {pthread_t t_id;int thread_param = 5;if (pthread_create(&t_id, NULL, thread_main, (void*)&thread_param) != 0) {puts("thread_create error");return -1;}sleep(10);//保证子线程先于主线程结束puts("end of main");return 0;
}

  如果将sleep(10)改成sleep(2),则不会输出5次running thread,因为main函数返回后整个进程将被销毁。但是我们通过sleep预测程序的执行流程的行为,有可能干扰程序的正常执行流。
为了更好地控制线程的执行流,需要了解下面的函数:

#include <pthread.h>
//status 保存线程的main函数返回值的指针变量地址值
int pthread_join(pthread_t thread, void** status);
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>void* thread_main(void* arg) {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;
}int main(int argc, char* argv[]) {pthread_t t_id;int thread_param = 5;void* thr_ret;if (pthread_create(&t_id, NULL, thread_main, (void*)&thread_param) != 0) {puts("thread_create error");return -1;}if(pthread_join(t_id, &thr_ret) != 0) {puts("pthread_join error");return 0;}printf("thread return message: %s \n", (char*)thr_ret);free(thr_ret);return 0;
}

2.2 可在临界区内调用的函数

  多个函数同时执行临界区代码时可能会产生问题。根据临界区是否引起问题,函数分为2类。

  • 线程安全函数(被多个线程调用也不会引发问题)
  • 非线程安全函数

_REENTRANT宏的作用

大多数标准函数都是线程安全的函数。更幸运的是,平台在定义非线程安全函数的同时,也提供了线程安全的函数。比如,域名转IP地址的函数就不是线程安全的

//非线程安全
struct hostent* gethostbyname(const char* hostname);//线程安全
struct hostent* gethostbyname_r(const char* name,struct hostent* result,char* buffer,int buflen,int* h_errnop);

  线程安全版本有点麻烦,幸好可以通过声明头文件前定义_REENTRANT宏将gethostbyname函数调用改为gethostbyname_r。或者可以在编译时添加-D REENTRANT选项定义宏
root@my_linux:/tcpip# gcc -D_REENTRANT mythread.c -0 mthread -lpthread
(感觉很少人用,没找到很多帖子,而且相比线程非安全版本更容易遇到问题,不展开了)。

3 多线程存在的问题和临界区

3.1 多个线程访问同一变量的问题

当多个线程同时访问内存空间,都有可能发生问题
   不是说线程会分时使用CPU吗? 那应该不会出现同时访问变量的情况啊。
此处的“同时访问”与同一时刻访问有些区别:
  假设2个线程要执行变量值逐次加1的工作,如下图:

  图中描述的是2个线程准备将变量num值加1的情况。理想状态下,线程1将num加1变成100后,线程2再访问num时,变量num应该变成101。
  但是,整个过程不是num+=1就完事了,值的运算需要CPU。首先线程1读取num的值并将其传递给CPU,CPU加1得到100,最后把100写回num变量中,这样就完成了一次理想过程。但是如果再次过程中线程2通过切换得到CPU,那么就需要从99重来。这样两个线程都操作一次后num的是100,而不是101。
  因此线程1访问num时应该阻止其他线程访问。这就是同步。

3.2 临界区位置

我们可以对临界区定义为这种形式:
  函数内同时运行多个线程时引起问题的多条语句构成的代码块。

注意:全局变量num不是临界区,因为问题不是它引起的,而是对它进行操作的那些语句,通常位于函数内部。

4 线程同步

4.1 同步的两面性

线程同步用于解决线程访问顺序引发的问题。需要同步的情况可以从如下两个方面考虑。

  • 同时访问同一内存空间时发生的情况。
  • 需要指定访问同一内存控件的线程执行顺序的情况。

重点说明一下第二种情况。假设A B了两个线程,线程A负责向指定内存空间写入数据,线程B负责取走该数据。这种情况下,如果B先于A取走数据,将导致错误结果。接下来介绍互斥量和信号量同步技术。

4.2 互斥量

  互斥量(Mutual Exclusion)主要用于解决线程同步访问的问题,表示不允许多个线程同时访问。
  现实中,洗手间就是一个很好的例子,人们无法同时访问洗手间,需要排队,而且为了满足无法同时访问,进去的人还需要加锁,互斥量就是一把优秀的锁。

#include <pthread.h>//成功返回0,失败返回其他值
//mutex 互斥量地址 attr 互斥量属性,无特殊需要传NULL
int pthread_mutex_init(pthread_mutex_t* mutex, const pthread_mutexattr_t* attr);
int pthread_mutex_destroy(pthread_mutex_t* mutex);

  为了创建相当于锁系统的互斥量,需要声明如下pthread_mutex_t型变量:

pthread_mutex_t mutex;

注:不要使用PTHREAD-MUTEX-INITALIZER宏进行初始化,发生错误很难发现。(自行了解)

接下来介绍利用互斥量锁住或释放临界区时使用的函数。

#include <pthread.h>int pthread_mutex_lock(pthread_mutex_t* mutex);
//临界区开始
....
//临界区结束
int pthread_mutex_unlock(pthread_mutex_t* mutex);

注意:线程退出临界区时,如果忘了调用pthread_mutex_unlock那么为了进入临界区调用pthread_mutex_lock的线程就无法拜托阻塞状态,这种情况称为死锁

在这里插入图片描述

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>
#define NUM_THREAD 100
#define _REENTRANTlong long num = 0;
pthread_mutex_t mutex;void* thread_inc(void* arg) {pthread_mutex_lock(&mutex);for (int i = 0; i < 50000; i++) {num += 1;}pthread_mutex_unlock(&mutex);return NULL;
}void* thread_des(void* arg) {pthread_mutex_lock(&mutex);for (int i = 0; i < 50000; i++) {num -= 1;}pthread_mutex_unlock(&mutex);return NULL;
}int main(int argc, char* argv[]) {pthread_t thread_id[NUM_THREAD];pthread_mutex_init(&mutex, NULL);for (int i = 0; i < NUM_THREAD; i++) {if (i % 2) {pthread_create(&(thread_id[i]), NULL, thread_inc, NULL);} else {pthread_create(&(thread_id[i]), NULL, thread_des, NULL);}}for (int i = 0; i < NUM_THREAD; i++) {pthread_join(thread_id[i], NULL);}printf("       result : %lld\n", num);pthread_mutex_destroy(&mutex);return 0;
}

注意:尽可能减少lock和unlock的调用次数,因为调用过程很花时间。

4.3 信号量

  此处只涉及利用"二进制信号量"(只用0和1)完成"控制线程顺序"为中心的同步方法。下面是信号量创建和销毁方法。

#include <semaphore.h>//成功时返回0,失败时返回其他值
//sem 信号量的变量地址值
//pshared 传递其他值时,创建可以由多个进程共享的信号量;传递0时,创建只允许1个进程内部使用的信号量。
//value 指定新创建的信号量初始值
int sem_init(sem_t* sem, int pshared, unsigned int value);
int sem_destroy(sem_t* sem);

接下来介绍信号量中相当于互斥量lock,unlock的函数。

#include <semaphore.h>//传递保存信号量读取值的变量地址值,传递给sem_post时信号量增l ,传递给sem_wait时信号量减1
int sem_post(sem_t* sem);
int sem_wait(sem_t* sem);

  调用sem init函数时,操作系统将创建信号量对象, 此对象中记录着"信号量值" (Semaphore Value) 整数。该值在调用sem_post函数时增1 ,调用sem-mfait函数时减1 。但信号量的值不能小于0 ,因此, 在信号量为0的情况下调用sem-wait函数时, 调用函数的线程将进入阻塞状态(因为函数未返回)。当然, 此时如果有其他线程调用sem_post函数, 信号量的值将变为1 ,而原本阻塞的线程可以将该信号量重新减为0并跳出阻塞状态。实际上就是通过这种特性完成临界区的同步操作,可以通过如下形式同步临界区(假设信号量的初始值为1 )。

sem_wait(&sem); //信号量变为0
//临界区开始
//....
//临界区的结束
sem_post(&sem); //信号量变为1

  上述代码结构中,调用sem-wait函数进入临界区的线程在调用sem_post函数前不允许其他线程进入临界区。信号量的值在0和1 之间跳转,因此,具有这种特性的机制称为"二进制信号量"。
我们使用下面例子介绍关于控制访问顺序的同步:
  “线程A从用户输入得到值后存入全局变量num ,此时线程B将取走该值并累加。该过程共进行5 次, 完成后输出总和并退出程序。”
在这里插入图片描述

#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
#define _REENTRANTvoid* read(void* arg);
void* accu(void* arg);
static sem_t sem_one;
static sem_t sem_two;
static int num;int main(int argc, char* argv[]) {pthread_t t1, t2;sem_init(&sem_one, 0, 0);sem_init(&sem_two, 0, 1);pthread_create(&t1, NULL, read, NULL);pthread_create(&t2, NULL, accu, NULL);pthread_join(t1, NULL);pthread_join(t2, NULL);sem_destroy(&sem_one);sem_destroy(&sem_two);return 0;
}void* read(void* arg) {for (int i = 0; i < 5; i++) {fputs("Input num: ", stdout);sem_wait(&sem_two);scanf("%d", &num);sem_post(&sem_one);}return NULL;
}void* accu(void* arg) {int sum = 0;for (int i = 0; i < 5; i++) {sem_wait(&sem_one);sum += num;sem_post(&sem_two);}printf("Result: %d\n", sum);return NULL;
}

5 线程的销毁和多线程并发服务端的实现

5.1 销毁线程的3种方法

  Linux线程并不是在首次调用的线程main函数返回时自动销毁,所以必须用下面两个方法之一加以明确,否则由线程创建的空间将一直存在。

  • 调用pthread_join函数
  • 调用pthread_detach函数

  pthread_join函数调用时,不仅会等待线程终止,还会引导线程销毁。但该函数的问题是,线程终止前,调用该函数的线程会进入阻塞状态。因此,通常调用如下函数引导线程销毁。

#include <pthread.h>
//分离线程,相当于“两者没关系了”,两者真正的“并行”执行,子线程执行完自行回收
int pthread_detach(pthread_t thread);

  调用上述函数不会引起线程中止或者进入阻塞状态。注意:两者不能一起使用。(不过我遇到的C++业务中一般会使用pthread_join(C++是join),因为需要使用多线程的操作往往成对出现(开始和结束),所以线程回收写进结束函数(或者析构函数),释放时机由自己把控)。

5.2 多线程并发服务端的实现

在这里插入图片描述

  • chat_server.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <pthread.h>#define BUF_SIZE 100
#define MAX_CLNT 256void * handle_clnt(void * arg);
void send_msg(char * msg, int len);
void error_handling(char * msg);int clnt_cnt=0;
int clnt_socks[MAX_CLNT];
pthread_mutex_t mutx;int main(int argc, char *argv[])
{int serv_sock, clnt_sock;struct sockaddr_in serv_adr, clnt_adr;int clnt_adr_sz;pthread_t t_id;if(argc!=2) {printf("Usage : %s <port>\n", argv[0]);exit(1);}pthread_mutex_init(&mutx, NULL);serv_sock=socket(PF_INET, SOCK_STREAM, 0);memset(&serv_adr, 0, sizeof(serv_adr));serv_adr.sin_family=AF_INET; serv_adr.sin_addr.s_addr=htonl(INADDR_ANY);serv_adr.sin_port=htons(atoi(argv[1]));if(bind(serv_sock, (struct sockaddr*) &serv_adr, sizeof(serv_adr))==-1)error_handling("bind() error");if(listen(serv_sock, 5)==-1)error_handling("listen() error");while(1){clnt_adr_sz=sizeof(clnt_adr);clnt_sock=accept(serv_sock, (struct sockaddr*)&clnt_adr,&clnt_adr_sz);pthread_mutex_lock(&mutx);//添加套介子到数组clnt_socks[clnt_cnt++]=clnt_sock;pthread_mutex_unlock(&mutx);pthread_create(&t_id, NULL, handle_clnt, (void*)&clnt_sock);pthread_detach(t_id);printf("Connected client IP: %s \n", inet_ntoa(clnt_adr.sin_addr));}close(serv_sock);return 0;
}void * handle_clnt(void * arg)
{int clnt_sock=*((int*)arg);int str_len=0, i;char msg[BUF_SIZE];while((str_len=read(clnt_sock, msg, sizeof(msg)))!=0)send_msg(msg, str_len);pthread_mutex_lock(&mutx);for(i=0; i<clnt_cnt; i++)   // remove disconnected client{if(clnt_sock==clnt_socks[i]) //删除当前的连接,后边的统一往前移动{while(i++<clnt_cnt-1)clnt_socks[i]=clnt_socks[i+1]; break;}}clnt_cnt--;pthread_mutex_unlock(&mutx);close(clnt_sock);return NULL;
}
void send_msg(char * msg, int len)   // send to all
{int i;pthread_mutex_lock(&mutx);for(i=0; i<clnt_cnt; i++)write(clnt_socks[i], msg, len);pthread_mutex_unlock(&mutx);
}
void error_handling(char * msg)
{fputs(msg, stderr);fputc('\n', stderr);exit(1);
}
  • chat_client.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h> 
#include <string.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <pthread.h>#define BUF_SIZE 100
#define NAME_SIZE 20void * send_msg(void * arg);
void * recv_msg(void * arg);
void error_handling(char * msg);char name[NAME_SIZE]="[DEFAULT]";
char msg[BUF_SIZE];int main(int argc, char *argv[])
{int sock;struct sockaddr_in serv_addr;pthread_t snd_thread, rcv_thread;void * thread_return;if(argc!=4) {printf("Usage : %s <IP> <port> <name>\n", argv[0]);exit(1);}sprintf(name, "[%s]", argv[3]);sock=socket(PF_INET, SOCK_STREAM, 0);memset(&serv_addr, 0, sizeof(serv_addr));serv_addr.sin_family=AF_INET;serv_addr.sin_addr.s_addr=inet_addr(argv[1]);serv_addr.sin_port=htons(atoi(argv[2]));if(connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr))==-1)error_handling("connect() error");pthread_create(&snd_thread, NULL, send_msg, (void*)&sock);pthread_create(&rcv_thread, NULL, recv_msg, (void*)&sock);pthread_join(snd_thread, &thread_return);pthread_join(rcv_thread, &thread_return);close(sock);  return 0;
}void * send_msg(void * arg)   // send thread main
{int sock=*((int*)arg);char name_msg[NAME_SIZE+BUF_SIZE];while(1) {fgets(msg, BUF_SIZE, stdin);if(!strcmp(msg,"q\n")||!strcmp(msg,"Q\n")) {close(sock);exit(0);}sprintf(name_msg,"%s %s", name, msg);write(sock, name_msg, strlen(name_msg));}return NULL;
}void * recv_msg(void * arg)   // read thread main
{int sock=*((int*)arg);char name_msg[NAME_SIZE+BUF_SIZE];int str_len;while(1){str_len=read(sock, name_msg, NAME_SIZE+BUF_SIZE-1);if(str_len==-1) return (void*)-1;name_msg[str_len]=0;fputs(name_msg, stdout);}return NULL;
}void error_handling(char *msg)
{fputs(msg, stderr);fputc('\n', stderr);exit(1);
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/535710.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Python实战:采集全国5A景点名单

本文将以采集全国 5A 景点名单为例&#xff0c;详细介绍如何使用 Python 进行数据采集。 本文采集到全国340家5A景区的名单&#xff0c;包括景区名称、地区、 A级、评定年份这些字段。 一、分析数据源 为了获取权威数据&#xff0c;我们来到主管部门的官方网站&#xff0c;在右…

学生打架校园防霸凌系统可以监测到吗

随着社会的进步和教育的发展&#xff0c;校园安全问题日益受到社会各界的关注。其中&#xff0c;学生打架和校园霸凌问题尤为突出&#xff0c;不仅影响了学生的身心健康&#xff0c;也破坏了校园的和谐氛围。为了有效预防和应对这些问题&#xff0c;许多学校开始引入校园防霸凌…

origin修改图例为显示”长名称/单位/注释/自定义“等

背景 由于在origin作图时希望修改自动显示的图例&#xff0c;但每次手动更新又比较繁琐&#xff08;特别是在数据量较多的情况下&#xff09;&#xff0c;为了一劳永逸 步骤 1. 在数据工作表中设置好需要修改后的名称&#xff08;我写到长名称里了&#xff09; 2. 修改图例的…

【原创】[新增]ARCGIS之土地报备Txt、征地Xls格式批量导出Por旗舰版

一、软件简介 2024年新增旗舰版软件&#xff0c;本软件全新界面开发&#xff0c;保留原有软件功能及一些使用习惯&#xff0c;并集成了现已有的所有定制格式的支持&#xff0c;并增加自定义格式的导出&#xff1b;做到1N2&#xff08;即为1种通用版本N种定制格式导出txt、Xls&a…

C++ 作业 24/3/13

1、设计一个Per类&#xff0c;类中包含私有成员:姓名、年龄、指针成员身高、体重&#xff0c;再设计一个Stu类&#xff0c;类中包含私有成员:成绩、Per类对象p1&#xff0c;设计这两个类的构造函数、析构函数和拷贝构造函数。 #include <iostream>using namespace std;c…

IU5070E线性单节锂电池充电管理IC

IU5070E是一款具有太阳能板最大功率点跟踪MPPT功能&#xff0c;单节锂离子电池线性充电器&#xff0c;最高支持1.5A的充电电流&#xff0c;支持非稳压适配器。同时输入电流限制精度和启动序列使得这款芯片能够符合USB-IF涌入电流规范。 IU5070E具有动态电源路径管理(DPPM)功能&…

数据库管理-第160期 Oracle Vector DB AI-11(20240312)

数据库管理160期 2024-03-12 数据库管理-第160期 Oracle Vector DB & AI-11&#xff08;20240312&#xff09;1 向量的函数操作to_vector()将vector转换为标准值vector_norm()vector_dimension_count()vector_dimension_format() 2 将向量转换为字符串或CLOBvector_seriali…

用友U8 Cloud base64 SQL注入漏洞复现

0x01 产品简介 用友U8 Cloud是用友推出的新一代云ERP&#xff0c;主要聚焦成长型、创新型企业&#xff0c;提供企业级云ERP整体解决方案。 0x02 漏洞概述 用友U8 Cloud base64接口处存在SQL注入漏洞&#xff0c;未授权的攻击者可通过此漏洞获取数据库权限&#xff0c;从而盗…

Ubuntu 系统的基础操作

一. VMware虚拟机安装Ubuntu20.04 安装好就可以进系统了 二. Xshell连接Ubuntu 1.配置网络 2.去连接Xshell 然后输入用户名 xyl 和密码 123 就可以登录上去 三. Ubuntu的使用 1.简介和下载地址 简介&#xff1a; Ubuntu&#xff08;乌班图&#xff09;是一个基于Debian的以…

企智汇数字化项目管理平台,助力企业高效项目管理!数字化转型必备!

数字化项目管理平台是一种集成了先进项目信息技术的管理工具&#xff0c;旨在帮助组织更有效地管理项目&#xff0c;实现项目目标的顺利完成。以下是企智汇数字化项目管理平台的一些核心特点和功能&#xff1a; 1. 统一的信息管理&#xff1a;企智汇数字化项目管理平台能够将项…

助贷系统crm:帮助助贷机构实现高效的客户关系管理

助贷系统CRM&#xff08;客户关系管理系统&#xff09;是一种能够帮助助贷企业实现高效客户关系管理的工具&#xff0c;通过助贷系统CRM&#xff0c;助贷企业可以更好地管理企业客户信息&#xff0c;跟踪客户互动、提高客户满意度&#xff0c;从而促进业务增长。 1. 客户信息集…

【竞技宝】CS2:VP2-0轻取Heroic晋级IEM达拉斯正赛

北京时间2024年3月13日,IEM达拉斯欧洲区封闭预选赛正在如火如荼,今天迎来了胜者组决赛VP对阵Heroic。本场比赛,VP在图一死亡游乐园打出招牌式的慢节奏进攻,Heroic始终无法抵抗住VP压时间的进攻,最终VP赢下图一。来到图二荒漠迷城之后,Heroic一度取得比分的领先,但VP在最后阶段连…