c++进阶篇——初窥多线程(二) 基于C语言实现的多线程编写

前言

在上一篇文章中我们介绍了在计算机底层视角下的虚拟内存和操作系统在用户层所进行的各个分层,在这篇文章我们就要开始尝试书写多线程代码了,其实在c++11后c++就提供供了线程类给我们使用,c++线程类其实主要是对c操作多线程的函数进行了封装,本质上其实是一致的,所以在讲解我们cpp的多线程编写之前,我觉得先来了解一下C语言是如何实现多线程的编写的,这样可以让我们更好的去理解cpp线程类的工作原理,话不多说,发车发车!

线程的创建

在之前我们讲解Linux下的进程控制时说过我们在创建进程时进程都会有自己的进程编号pid,而线程和它们一样,每一个线程都有唯一的线程编号,它的类型为pthread_t,它是一个无符号长整形数,我们可以调用下面这个函数来获取当前线程的线程编号:

pthread_t ptread_self(void);  //返回当前线程的线程编号

如果我们希望在一个进程中创建子线程,就要调用线程创建函数,但是和进程不同,我们必须要给每一个线程指定一个线程处理函数,否则线程将无法正常工作,处理函数的定义如下:

int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg);
  • 参数:
    • thread: 传出参数,是无符号长整形数,线程创建成功, 会将线程ID写入到这个指针指向的内存中
    • attr: 线程的属性, 一般情况下使用默认属性即可, 写NULL
    • start_routine: 函数指针,创建出的子线程的处理动作,也就是该函数在子线程中执行。
    • arg: 作为实参传递到 start_routine 指针指向的函数内部
  • 返回值:线程创建成功返回0,创建失败返回对应的错误号

下面我们来看一个线程创建的实例:

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include <pthread.h>// 线程的处理函数
void* work(void* arg)
{printf("子线程id=%ld\n",pthread_self());for(int i=0;i<9;i++){printf("child id=%d\n",i);}return NULL;
}int main(int argc,char* argv[])
{pthread_t tid;pthread_create(&tid,NULL,work,NULL);printf("主线程id=%ld\n",pthread_self());for(int i=0;i<3;i++){printf("main id=%d\n",i);}sleep(10);return 0;
}

注意:
我们在Linux下编译该代码要导入线程库。编译命令如下:

all: demo1demo1: Create_Thread.cgcc -pthread -o demo1 Create_Thread.c clean:rm -f demo1

运行结果如下:

root@iZuf6ckztbjhtavfplgp0dZ:~/mylib/cppdemo/Linux系统编程/多线程/threads(c)# ./demo1
主线程id=139930484275008
main id=0
main id=1
main id=2
子线程id=139930484270848
child id=0
child id=1
child id=2
child id=3
child id=4
child id=5
child id=6
child id=7
child id=8

如果我们去除sleep函数的使用我们会发现结果是这样的:

主线程id=140150867830592
main id=0
main id=1
main id=2

这是因为虚拟地址生存周期是和主线程保持一致的,与子线程无关,主线程提前结束,导致哪怕子线程还没有开始运行,程序也自动停止运行了,这里的sleep函数所起到的作用主要就是线程同步。

线程的退出

在我们编写多线程代码时,如果我们希望让线程退出,但是不希望因此导致虚拟地址空间的释放(主线程突出时会释放),这时候我们可以调用线程退出函数,这样线程的退出就不会影响其他线程的正常使用了,函数定义如下:

void pthread_exit(void* retval)

- retval:线程退出的时候携带的数据,当前子线程的主线程会得到该数据。如果不需要使用,指定为NULL

下面我们来看一个简单的示例:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>void* work(void* arg)
{sleep(5);printf("子线程运行中,子线程id:%ld\n",pthread_self());\for(int i=1;i<9;i++){printf("id=:%d\n",i);if (i==6){pthread_exit(NULL);}}return NULL;
}int main(int argc,int *argv[])
{pthread_t tid;pthread_create(&tid,NULL,work,NULL);printf("主线程运行中,主线程id:%ld\n",pthread_self());for(int i=0;i<3;i++){printf("id=:%d\n",i);}pthread_exit(NULL);//主线程退出return 0;
}

输出结果为:

主线程运行中,主线程id:140161600366400
id=:0
id=:1
id=:2
子线程运行中,子线程id:140161600362240
id=:1
id=:2
id=:3
id=:4
id=:5
id=:6

我们可以看到虽然主线程提前推出了,但是却并没有影响到子线程的运行。

线程回收

线程回收函数

进程和线程一样,子线程退出的时候它的内核资源是由主线程来回收,线程回收的函数是pthread_join(),该函数是阻塞函数,当有子线程正在运行,调用该函数会阻塞,直到所有子线程都退出后才能进行子线程资源的回收,一次只能回收一个子线程的资源,如果我们有多个子线程资源需要回收,需要借助循环来完成,函数原型如下:

int pthread_join(pthread_t pid,void** retval);

参数说明:

  • pid:要被回收的线程编号
  • retval:二级指针,指向一级指针,是一个传出参数 ,一级指针里面储存的是pthread_exit()函数传出的数据,如果不需要该数据可以设为NULL
  • 返回值:线程回收成功返回0,回收失败返回错误号。

回收子线程数据的实现方式

在子线程退出的时候可以通过pthread_join函数来将数据传出,我们在回收子线程的同时也可以接收子线程的数据这样的实现方法有很多种,下面我们来看下面几种:
备注
导致实现方法多样性的原因: 通过上面的介绍,我们知道子线程在被回收的时候会将数据写入到一块内存中,然后采纳数传出该内存的地址而非是存储数据本身,而传出的参数类型是void*,这个万能指针可以指向任意一块内存,也导致了我们可以通过不同的形式来接收子线程数据。

  1. 使用子线程栈
    在我们接收子线程数据的时候可以通过子线程栈来回收子线程数据,示例代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>typedef struct Person
{int id;char* name;int age;
}Person;void* work(void* arg)
{printf("子线程id:%ld",pthread_self());for(int i=0;i<9;i++){printf("id=%d\n",i);if (i==6){Person p;p.id=1;p.name="张三";p.age=20;pthread_exit((void*)&p);}}return NULL;
}int main(int argc,char* argv[])
{pthread_t tid;if (pthread_create(&tid,NULL,work,NULL)!=0){printf("线程创建失败");return -1;}printf("主线程id:%ld\n",pthread_self());void *ptr;pthread_join(tid,&ptr);struct Person* p=(struct Person*)ptr;printf("id=%d\n",p->id);printf("name=%s\n",p->name);printf("age=%d\n",p->age);printf("子线程数据成功接收\n");return 0;
}

我们编译运行后结果如下:

主线程id:139934351951680
子线程id:139934351947520id=0
id=1
id=2
id=3
id=4
id=5
id=6
id=0
name=
age=22476544
子线程数据成功接收

我们可以发现在主线程中并没有子线程的数据,这是因为当子线程退出时,子线程所占据的栈区就会被回收,进而导致了子线程想要传递的数据被释放掉了,所以说我们一般不会采取子线程栈来接收数据,而是使用其他方式来接收数据。
2. 使用全局变量

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>typedef struct Person
{int id;char* name;int age;
}Person;Person p;void* work(void* arg)
{printf("子线程id:%ld",pthread_self());for(int i=0;i<9;i++){printf("id=%d\n",i);if (i==6){p.id=1;p.name="张三";p.age=20;pthread_exit((void*)&p);}}return NULL;
}int main(int argc,char* argv[])
{pthread_t tid;if (pthread_create(&tid,NULL,work,NULL)!=0){printf("线程创建失败");return -1;}printf("主线程id:%ld\n",pthread_self());void *ptr;pthread_join(tid,&ptr);struct Person* p=(struct Person*)ptr;printf("id=%d\n",p->id);printf("name=%s\n",p->name);printf("age=%d\n",p->age);printf("子线程数据成功接收\n");return 0;
}

输出结果为:

主线程id:140702000142144
子线程id:140702000137984id=0
id=1
id=2
id=3
id=4
id=5
id=6
id=1
name=张三
age=20
子线程数据成功接收
  1. 使用主线程栈
    虽然线程之间有自己的栈空间,但是它们彼此之间也可以互相访问,而一般主线程都是最后退出的,所以我们可以尝试把子线程返回的数据保存到了主线程的栈区内存中。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>typedef struct Person
{int id;char* name;int age;
}Person;void* work(void* arg)
{Person *p=(Person*)arg;printf("子线程id:%ld",pthread_self());for(int i=0;i<9;i++){printf("id=%d\n",i);if (i==6){p->id=1;p->name="张三";p->age=20;pthread_exit((void*)&p);}}return NULL;
}int main(int argc,char* argv[])
{Person p;pthread_t tid;if (pthread_create(&tid,NULL,work,&p)!=0){printf("线程创建失败");return -1;}printf("主线程id:%ld\n",pthread_self());void *ptr;pthread_join(tid,&ptr);printf("id=%d\n",p.id);printf("name=%s\n",p.name);printf("age=%d\n",p.age);printf("子线程数据成功接收\n");return 0;
}

线程分离

在一些情况下,程序中的主线程会拥有自己的业务处理流程,如果让主线程负责子线程的资源回收,那么调用pthread_join函数在子线程全部结束前主线程会一直阻塞,这时候我们可使用线程分离函数来将该线程剥离出来,调用该函数后子线程会与主线程分离,但是这样pthread_join就接收不到子线程资源了,线程分离函数定义如下:

int pthread_detach(pthread_t id);

示例如下:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>// 子线程的处理代码
void* working(void* arg)
{printf("我是子线程, 线程ID: %ld\n", pthread_self());for(int i=0; i<9; ++i){printf("child == i: = %d\n", i);}return NULL;
}int main()
{//创建一个子线程pthread_t tid;pthread_create(&tid, NULL, working, NULL);printf("子线程创建成功, 线程ID: %ld\n", tid);// 2. 子线程不会执行下边的代码, 主线程执行printf("我是主线程, 线程ID: %ld\n", pthread_self());for(int i=0; i<3; ++i){printf("i = %d\n", i);}// 设置子线程和主线程分离pthread_detach(tid);// 让主线程自己退出即可pthread_exit(NULL);return 0;
}

一些其他的线程函数

线程取消

线程取消指的是我们可以在一个线程中调用它来取消另一个线程,函数定义如下:

int pthread_cancel(pthread_t pid);

代码示例如下:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>// 子线程的处理代码
void* work(void* arg)
{printf("我是子线程, 线程ID: %ld\n", pthread_self());for(int i=0; i<9; ++i){sleep(1);printf("child == i: = %d\n", i);}return NULL;
}int main()
{//创建一个子线程pthread_t tid;pthread_create(&tid, NULL, work, NULL);printf("子线程创建成功, 线程ID: %ld\n", tid);// 2. 子线程不会执行下边的代码, 主线程执行printf("我是主线程, 线程ID: %ld\n", pthread_self());for(int i=0; i<3; ++i){sleep(1);printf("i = %d\n", i);}// 设置子线程和主线程分离pthread_cancel(tid);// 让主线程自己退出即可pthread_exit(NULL);return 0;
}

输出如下:

我是主线程, 线程ID: 139763539765056
我是子线程, 线程ID: 139763539760896
i = 0
child == i: = 0
i = 1
child == i: = 1
i = 2
child == i: = 2

注意:线程的取消分两步:

  • 主线程基于线程取消函数发送请求
  • 当子线程再次进行系统调用时,线程会被取消(没有这一步,线程就还存在)

结语

关于C语言库中关于线程的函数介绍到此就告一段落了,下一篇文章我们就要开始介绍cpp中一些关于线程的知识了,下篇见!

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

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

相关文章

VB.net实战(VSTO):VSTOwpf体验框架打包教程

如果是考虑到Wps用户较多&#xff0c;就不建议采用侧边栏的形式 只是个体验框架&#xff0c;界面未作美化&#xff0c;office的用户可以用任意一种窗体&#xff0c;喜欢那个界面就写那个界面&#xff0c;wps的侧边栏只能弹出一部分&#xff0c;每次需要的手动拖动。 打包了案例…

Java——IO流(一)-(6/8):字节流-FileInputStream 每次读取多个字节(示例演示)、一次读取完全部字节(方式一、方式二,注意事项)

目录 文件字节输入流&#xff1a;每次读取多个字节 实例演示 注意事项 文件字节输入流&#xff1a;一次读取完全部字节 方式一 方式二 注意事项 文件字节输入流&#xff1a;每次读取多个字节 用到之前介绍过的常用方法&#xff1a; 实例演示 需求&#xff1a;用每次读取…

【泛微系统】e-cology非标配功能概览

关于泛微非标功能的功能编号、功能名称及支持版本 编号名称支持版本001考勤功能4.500.0124-9.00+KB900190206002短信通用接口5.000.0327+KB50001003 及以上版本004计划任务接口5.0+KB50001003及以上版本005集成登录接口6.0及以上版本006流程中自定义浏览框5.0+KB50001003及以上…

小程序项目业务逻辑回忆4

用户查询积分 积分获取规则如下: 邀请其他用户购票参会,将获取该用户花费金额的10%获取积分。 邀请用户注册参观展览&#xff0c;需注册并现场签到&#xff0c;将获取10分的奖励积分。 邀请企业用户参展&#xff0c;将获取企业参展金额的5%获取到积分。 上述3条积分获取规…

诸茅的黄昏

内容提要 白酒大陆的坍塌终于到达茅台的地盘&#xff0c;一切发生得太快了。突然间&#xff0c;深厚的护城河消失了&#xff0c;医药茅、眼科茅、牙科茅、疫苗茅、酱油茅都挣扎于内需的泥沼中。旧茅衰退&#xff0c;新茅生长&#xff0c;在下行周期&#xff0c;内需仍有结构性…

c++中的substr函数

在C++中,substr() 是 std::string 类的一个成员函数,用于从字符串中提取子字符串。以下是 substr() 函数的一些基本用法: 语法 substr(size_t pos = 0, size_t len = npos) pos 是子字符串开始的位置(基于 0 的索引)。如果不提供,它默认为 0,即从字符串的开头开始。le…

C#修改 EXE 文件图标和 winForm 窗口图标

修改 EXE 文件图标 1.准备好图片&#xff0c;转换为 Icon 图片&#xff1b; 2.右键工程&#xff0c;选择属性&#xff1b; 3.选择 Icon 图标即可&#xff1b; 4.重新生成可执行文件&#xff0c;查看。 修改 winForm 窗口图标 1.选中 winForm &#xff0c;查看属性&#x…

计算机的发展简史

目录 1. 计算机的五代变化 2. 半导体存储器的发展 3. 微处理器的发展 4. 计算机的性能指标 总结 计算机的发展史是一部技术革新与应用拓展的壮丽篇章。自20世纪中叶以来&#xff0c;计算机经历了五代变革&#xff0c;每一代都带来了性能的飞跃和使用模式的变革。同时&…

「51媒体」时尚类媒体邀约宣发资源

传媒如春雨&#xff0c;润物细无声&#xff0c;大家好&#xff0c;我是51媒体网胡老师。 时尚类媒体邀约宣发资源可以多样化且针对性地满足品牌或活动的推广需求。以下是一些主要的资源及其特点&#xff1a; 时尚杂志&#xff1a;国内外知名时尚杂志&#xff0c;如《Vogue》、…

关于单片机那些事?

周期 时钟周期&#xff1a;也叫振荡周期&#xff0c;就是单片机外接晶振的倒数&#xff0c;如12Mhz&#xff0c;周期就是1/12us&#xff0c;最小的时间单位。频率越高&#xff0c;速度越快 指令周期&#xff1a;执行一条指令需要的时间&#xff0c;一般由若干个机器周期组成 …

【单片机】msp430g2553单片机, 用TA0定时器,让小灯P1.6呼吸灯,P1.6是TA0.1

要实现用MSP430G2553单片机的TA0定时器控制P1.6&#xff08;TA0.1&#xff09;的呼吸灯效果&#xff0c;可以按照以下步骤进行&#xff1a; 配置时钟系统&#xff1a;设置时钟源和分频器&#xff0c;以便定时器工作在合适的频率。 配置P1.6引脚&#xff1a;将P1.6引脚设置为TA…

【代码随想录训练营】【Day 52】【动态规划-11】| Leetcode 1143, 1035, 53,392

【代码随想录训练营】【Day 52】【动态规划-11】| Leetcode 1143, 1035, 53&#xff0c;392 需强化知识点 题目 1143. 最长公共子序列 注意还是要从0-i-1, 0-j-1 定义&#xff0c;因为我们需要遍历 0 class Solution:def longestCommonSubsequence(self, text1: str, text…

手机怎么自动切换ip地址

在数字化时代&#xff0c;网络IP地址不仅是设备在网络世界的标识&#xff0c;也是确保用户网络安全和数据隐私的关键因素。对于手机用户来说&#xff0c;在某些情境下可能需要自动切换IP地址&#xff0c;本文将为您介绍手机怎么自动切换IP地址。 随着网络技术的发展&#xff0c…

一些使用注意(XPTable控件使用说明十)

当XPTABLE放到线程中&#xff0c;列数据很多&#xff0c;不出现滚动条的解决代码&#xff1a; /// 这里神奇的代码&#xff0c;解决线程中XPTABLE 不出滚动条问题 , 执行UI相关的操作this.Invoke(new Action(() >{ // 列头&#xff0c;一行空的&#xff0c;这里列头设置…

前端面试题(基础篇六)

一、什么是事件代理 事件代理&#xff08;Event Delegation &#xff09;&#xff0c;又称事件委托。是JavaScript中常用的绑定事件的技巧。顾名思义&#xff0c;事件代理就是将原本要绑定的事件委托给父元素&#xff0c;让父元素担当事件监听的职务。原理就是DOM元素的事件冒…

蓝桥杯 经典算法题 求解完全背包问题

题目&#xff1a; 题解&#xff1a; 和01背包基本完全一样。小局部最优的策略也是一样&#xff1a;是否选当前局部的最后一项。唯一的不同点在于物品是无线的导致在表示选择当前物品的状态写法发生了改变&#xff1a;由dp[i-1][j-w[i]]变为了dp[i][j-w[i]]因为这样能够表示最后…

读AI新生:破解人机共存密码笔记08超级智能

1. 发现动作 1.1. 时间跨度长的智能行为&#xff0c;需要具备在多个抽象层次上分层规划和管理活动的能力&#xff0c;从攻读博士学位&#xff08;可能涉及1万亿个动作&#xff09;&#xff0c;到给一根手指发送一个运动控制指令&#xff0c;从而键入求职信的字符&#xff0c;无…

用户态协议栈04-定时arp-table的实现

之前有写过arp reply的实现&#xff0c;其中有写道&#xff0c;我们的系统内核中会维护一张ARP表&#xff0c;可以通过终端arp -a查看&#xff1a; 其中的dynamic和static是动态arp的类型&#xff0c;之前的udp实验就是添加了一条静态arp达到了发送的目的。在我们需要发送一个数…

压缩机吸/排气温度与压力异常的原因

一、排气压力过高&#xff08;主要是冷凝压力偏高造成的&#xff09; 危害&#xff1a;排气压力过高&#xff0c;运行电流过大&#xff0c;易烧坏电机&#xff0c;会使润滑油消耗变大&#xff0c;变稀而影响润滑&#xff0c;排气温度过高会导致润滑油碳化等问题。 排气压力过高…

探索Java Scanner类:全面解析及实用示例

在Java编程中&#xff0c;处理用户输入和文件读取是常见的任务。Scanner类是Java提供的一个强大的工具类&#xff0c;用于简化这些操作。本文将详细介绍Scanner类的基本用法、常见应用场景和高级功能&#xff0c;并提供代码示例帮助理解。 一、什么是Scanner类 Scanner类属于…