多线程控制讲解与代码实现

多线程控制

回顾一下线程的概念

线程是CPU调度的基本单位,进程是承担分配系统资源的基本单位。linux在设计上并没有给线程专门设计数据结构,而是直接复用PCB的数据结构。每个新线程(task_struct{}中有个指针都指向虚拟内存mm_struct结构,实现了共享同一份代码,拥有该进程的一部分资源)

在linux中,把所有执行流都看作是轻量级进程,故有一个用户级的原生线程库为用户提供“线程”接口(对OS是轻量级线程)。

从信号、异常和资源看线程的健壮性问题

一个线程出现异常,会影响其他线程

#include <iostream>
#include <string>
#include <pthread.h>
#include <unistd.h>

using namespace std;

void* start_routime(void* args)
{
    string name = static_cast<const char*>(args);//安全地进行强制类型转换
    // 如果是一个执行流,那么不可能同时执行2个死循环
    int count = 0;
    while(1)
    {
        cout << "new thread is created! name: " << name << endl;
        sleep(1);
        count++;

        if(count == 5)
        {
            int* p = nullptr;
            *p = 10;//故意写一个解引用空指针,我们知道是会报段错误
        }
    }
}

int main()
{
    pthread_t thread;
    pthread_create(&thread, nullptr, start_routime, (void*)"thread new");

    while(1)
    {
        cout << "new thread is created! name: main" << endl;
        sleep(1);
    }

    return 0;
}

命令行报错

[yyq@VM-8-13-centos 2023_03_18_multiThread]$ ./mythread 
new thread is created! name: main
new thread is created! name: thread new
new thread is created! name: main
new thread is created! name: thread new
new thread is created! name: main
new thread is created! name: thread new
new thread is created! name: main
new thread is created! name: thread new
new thread is created! name: main
new thread is created! name: thread new
new thread is created! name: main
Segmentation fault

由此可以看出,当一个线程出异常了,会直接影响其他线程的正常运行。因为信号是整体发送给进程的,本质是将信号发给对应进程的pid,而一个进程的所有线程pid值是相同的,就会给每个线程的PCB里写入相同的信号,接收到信号后,所有的进程就退了。

换个角度来说,每个线程所依赖的资源是进程给的,当一个线程出异常,进程会收到退出信号后,OS回收资源是回收整个进程的资源,而其他线程的资源是进程给的,故所有的线程就会全部退出。

以上是从信号+异常+资源的视角来看待线程健壮性的问题。

POSIX线程库的errno

我们学习的是POSIX线程库,有以下特征

1、与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是pthread_打头的;

2、要使用这些函数库,要通过引入头文<pthread.h>

3、链接这些线程函数库时要使用编译器命令的-l pthread选项。

用户级线程库的pthread这一类函数出错时不会设置全局变量errno(虽然大部分其他POSIX函数会这样做),而是将错误代码通过返回值返回。因为线程是共享一份资源的,如果多个线程对同一个全局变量进行访问(errno也是全局变量),就会因为缺乏访问控制而带来一些问题,因此对于pthreads函数的错误,建议通过返回值来判定。

简单了解clone

允许用户创建一个进程/轻量级进程,fork()/vfork()就是调用clone来实现的。

#include <sched.h>
功能:创建一个进程/轻量级进程

原型
	int clone(int (*fn)(void *), void *child_stack, int flags, void *arg, .../* pid_t *ptid, struct user_desc *tls, pid_t *ctid */ );

参数
    child_stack:子栈(用户栈)
    flags:
返回值
    创建失败,返回-1;创建成功,返回线程ID

/* Prototype for the raw system call */

long clone(unsigned long flags, void *child_stack, void *ptid, void *ctid, struct pt_regs *regs);

创建多线程

#include <iostream>
#include <string>
#include <vector>
#include <pthread.h>
#include <unistd.h>

using namespace std;

void* start_routime(void* args)
{
    string name = static_cast<const char*>(args);//安全地进行强制类型转换
    while(1)
    {
        cout << "new thread is created! name: " << name << endl;
        sleep(1);
    }
}

int main()
{
    vector<pthread_t> threadIDs (10, pthread_t());
    for(size_t i = 0; i < 10; i++)
    {
        pthread_t tid;
        char nameBuffer[64];
        snprintf(nameBuffer, sizeof(nameBuffer), "%s : %d", "thread", i);
        pthread_create(&tid, nullptr, start_routime, (void*)nameBuffer);//是缓冲区的起始地址,无法保证创建的新线程允许先后次序
        threadIDs[i] = tid;
        sleep(1);
    }

    for(auto e : threadIDs)
    {
        cout << e << endl;
    }

    while(1)
    {
        cout << "new thread is created! name: main" << endl;
        sleep(1);
    }
    return 0;
}

现象:当创建多个线程的循环中没有添加sleep语句时,我们可能看到的输出一直是某个线程。

分析:当我们创建新的线程时,每个线程是独立的执行流。首先,创建多个新线程,谁先运行是不确定的;其次,因为nameBuffer是被所有线程共享的,主线程是把缓冲区的起始地址传给每个线程,在循环里nameBuffer在一直被主进程更新,所以每个进程能拿到的都是被主进程更新后的最新的进程id。

多线程数据私有

所以,如果我们想让各个线程独立执行代码,这样的写法是不对的,那如何给线程传递正确的结构呢?既然nameBuffer是同一个变量一样的地址,那就每次传入不同的地址呀

//当成结构体使用
class ThreadData
{
public:
    pthread_t tid;
    char nameBuffer[64];    
};
//对应的操作函数如下
void* start_routime(void* args)//args传递的时候,也是拷贝了一份地址,传过去。不管是传值传参还是传引用传参,都会发生拷贝
{
    ThreadData* td = static_cast<ThreadData*>(args);//安全地进行强制类型转换
    int cnt = 10;
    while(cnt)
    {
        cout << "new thread is created! name: " << td->nameBuffer << " 循环次数cnt:" << cnt << endl;
        cnt--;
        sleep(1);
    }
    delete td;
    return nullptr;
}
int main()
{
    vector<ThreadData*> threads;
    for(size_t i = 0; i < 10; i++)
    {
        // 此处 td是指针,传给每个线程的td指针都是不一样的,实现数据私有
        ThreadData* td = new ThreadData();
        snprintf(td->nameBuffer, sizeof(td->nameBuffer), "%s : %d", "thread", i + 1);
        pthread_create(&td->tid, nullptr, start_routime, (void*)td);
        threads.push_back(td);
    }

    for(auto& e : threads)
    {
        cout << "create thread name: " << e->nameBuffer << " tid:" << e->tid << endl;
    }

    int cnt = 7;
    while(cnt)
    {
        cout << "new thread is created! name: main" << endl;
        cnt--;
        sleep(1);
    }
    return 0;
}

通过传new出来的结构体指针,实现多线程数据私有!

重入状态

start_routime()函数同时被10个线程访问,在程序运行期间处于重入状态。站在变量的角度,由于函数没有访问全局变量,访问的都是局部变量,故是可重入函数。【严格来说,这不算可重入函数,因为cout是访问文件的,而我们只有一个显示器,在输出到显示器的时候有可能会出错】

对全局变量进行原子操作的是可重入函数。

独立栈空间

每个线程都有自己独立的栈空间

线程ID

线程id是它独立栈空间的起始地址

线程等待pthread_join

**join是阻塞式等待。**线程也是要被等待的,如果不等待,会造成类似僵尸进程的问题–内存泄漏。作用1、获取线程退出信息;2、回收线程资源。但与进程不同的是,线程不用获取退出信号,因为一旦线程出异常,收到信号了,整个进程都会退出。

pthread_join不考虑异常问题,线程出异常了进程直接来处理。

start_routime返回值的类型是void*,pthread_join()中retval参数的类型是void**。两者之间有关联。

#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
参数
    thread:线程id
    retval输出型参数:用于获取线程函数结束时,返回的退出结果
返回值
    成功返回0,失败返回错误码
        
具体使用:
void* retval = nullptr;//相当于把start_routime返回的指针(这里的指针是指针地址,是个字面值)存到ret(这里的ret是指针变量)里面去
int n = pthread_join(tid, &retval);
assert(n == 0);

线程终止时,可以返回一个指针(比如堆空间的地址、对象的地址等),并可以被主线程取到,由此可以完成信息交互。

例如进程,有阻塞式等待和非阻塞式等待,用信号捕捉函数,设置成signal(SIGCHLD, SIG_IGN);就可;而进程没有非阻塞式等待。

线程分离pthread_detach

默认情况下,我们创建的新线程都是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏,如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。

功能:分离线程,与joinable是互斥的
#include <pthread.h>
原型:
	int pthread_detach(pthread_t thread);
返回值:
    成功返回0;失败返回错误码,但不设置错误码,不被设置到errno
//使用1:线程自己分离自己
pthread_detach(pthread_self());
//使用2:主线程分离其他线程

当线程自己分离自己后,主线程再调用pthread_join()【需要主动让主进程的join后与detach执行】,此时jion函数会返回22,表示Invalid argument

为什么要先让detach执行,因为新线程和主线程谁先执行是不确定的,当新线程去执行自己的任务时,假设新线程还没来得及执行detach,而主线程就已经join了,那么detach就无效了。

功能:获取调用该函数的线程ID
#include <pthread.h>
pthread_t pthread_self(void);

线程终止

线程退出return/pthread_exit

  1. return nullptr; return返回就表示该线程终止。

  2. pthread_exit(nullptr); 线程退出的专用pthread_exit()函数。

    #include <pthread.h>
    void pthread_exit(void *retval);

exit用于终止进程,不能用于终止线程。任何一个执行流调用exit都会让整个进程退出。

发现了没,return和pthread_exit都有个nullptr参数!这个返回值会放在pthread库里面的。

后续线程等待时,就是到pthread库里取到这个值。

线程取消pthread_cancel

线程被取消的前提是线程已经在运行了,由主线程给对应的线程发送取消命令。收到的退出码retval是-1,-1实际上是宏#define PTHREAD_CANCELED ((void*) -1)

原生线程库pthread

站在上层的角度(从语言层面)来看原生线程库:

在linux上,任何语言如果要实现多线程,必定要用到pthread库,如何看待C++11中的多线程呢?C++11中的多线程在linux环境下本质是对pthread库的封装

#include <iostream>
#include <thread>
#include <unistd.h>
using namespace std;

void thread_run()
{
    int cnt = 5;
    while(cnt)
    {
        cout << "我是新线程" << endl;
        sleep(1);
    }
}

int main()
{
    thread t1(thread_run);

    while(true)
    {
        cout << "我是主线程" << endl;
    }

    t1.join();
    return 0;
}
//这份代码用g++编译,如果不带-lpthread选项,就会报错!说明C++就是封装了原生线程库

用原生线程库写出来的代码,是不可跨平台的,但是效率更高;用C++写出来的多平台通用,但是效率偏低。

原生线程库是共享库,可以同时被多个用户使用。那么如何对用户创建出来的线程做管理呢?

linux给出的解决方案是,让原生线程库采用一定的方法对用户创建的线程做管理,只不过在库里需要添加的线程属性比较少(包括线程id等),会存在一个union pthread_attr_t{};结构体里,然后与内核中的轻量级进程一一对应,内核提供线程执行流的调度。linux用户级线程:内核轻量级进程=1:1

在这里插入图片描述

线程局部存储__thread

全局变量保存在进程地址空间的已初始化数据区的,被__thread修饰的全局变量保存在线程局部存储中(共享区的线程结构体里)。这个是线程独有的,介于全局变量和局部变量之间的一种存储方案。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

你真的掌握到“优先级队列“的精髓了吗?

前文如果我们给每个元素都分配一个数字来标记其优先级&#xff0c;不妨设较小的数字具有较高的优先级&#xff0c;这样我们就可以在一个集合中访问优先级最高的元素并对其进行查找和删除操作了。这样&#xff0c;我们就引入了优先级队列 这种数据结构。一&#xff0c;priority_…

QT/C++调试技巧:内存泄漏检测

文章目录内存泄漏方案一方案二&#xff1a;CRT调试定位代码位置方法1方法2其它问题方案三&#xff1a;使用vs诊断工具方案四&#xff1a;使用工具VLD&#xff08;Visio Leak Detector&#xff09;方案五Cppcheck内存泄漏 内存泄漏&#xff1a;指的是在程序里动态申请的内存在使…

STM32学习(八)

STM32串口与电脑USB口通信 特别注意&#xff1a;两个设备之间的TXD和RXD&#xff0c;必须交差连接&#xff0c;方可正常通信 RS-232异步通信协议 启动位&#xff1a;必须占1个位长&#xff0c;必须保持逻辑0电平。有效数据位&#xff1a;可选5、6、7、8、9个位长&#xff0c;L…

【嵌入式烧录/刷写文件】-1-详解Motorola S-record(S19/SREC/mot/SX)格式文件

目录 1 什么是Motorola S-record 2 Motorola S-record的格式 2.1 Motorola S-record的结构 2.1.1 “Record type记录类型”的说明 2.1.2 “Record length记录长度”的说明 2.1.3 如何计算“Checksum校验和” 2.2 Record order记录顺序 2.3 Text line terminator文本行终…

HTTP/2.x:最新的网页加载技术,快速提高您的SEO排名

2.1 http2概念HTTP/2.0&#xff08;又称HTTP2&#xff09;是HTTP协议的第二个版本。它是对HTTP/1.x的更新&#xff0c;旨在提高网络性能和安全性。HTTP/2.0是由互联网工程任务组&#xff08;IETF&#xff09;标准化的&#xff0c;并于2015年发布。2.2 http2.x与http1.x区别HTTP…

spring5(三):IOC操作Bean管理(基于xml方式)

IOC操作Bean管理&#xff08;基于xml方式&#xff09;前言一、基于 xml 方式创建对象二、基于 xml 方式注入属性1. 使用 set 方法进行属性注入2. 使用有参数构造进行属性注入3. p 名称空间注入简化操作&#xff08;了解&#xff09;三、xml 注入其它类型属性1. 字面量2. 注入属…

【云原生之企业级容器技术 Docker实战一】Docker 介绍

目录一、Docker 介绍1.1 容器历史1.2 Docker 是什么1.3 Docker 和虚拟机&#xff0c;物理主机1.4 Docker 的组成1.5 Namespace1.6 Control groups1.7 容器管理工具1.8 Docker 的优势1.9 Docker 的缺点1.10 容器的相关技术1.10.1 容器规范1.10.2 容器 runtime1.10.3 容器管理工具…

看齐iOS砍掉祖传功能,Android 16G内存也危险了

手机内存发展是真的迅速&#xff0c;12GB 没保持几年现在又朝着 16GB 普及。 相比 iOS 的墓碑机制&#xff0c;Android 后台就主打一个真实&#xff0c;只是可惜 APP 不那么老实。 如果你较早接触 Android 机&#xff0c;各种系统管理、优化 APP 的一键加速、清理应该还历历在…

AD9235芯片手册阅读笔记

特征 单个3 V电源操作&#xff08;2.7 V至3.6 V&#xff09; SNR70 dBc至65 MSPS时的奈奎斯特 SFDR85 dBc至65MSPS时奈奎斯特低功率&#xff1a; 300 mW至65 MSPS差分输入&#xff0c;带500 MHz带宽 片上参考和SHA DNL0.4 LSB 灵活模拟输入&#xff1a;1 V p-p至2 V p-p范围 偏…

Python的加密与解密,你知道几类?

人生苦短&#xff0c;我用python python 安装包资料:点击此处跳转文末名片获取 据记载&#xff0c; 公元前400年&#xff0c; 古希腊人发明了置换密码。 1881年世界上的第一个电话 保密专利出现。 在第二次世界大战期间&#xff0c; 德国军方启用“恩尼格玛”密码机&#xff0…

[数据结构] 用两个队列实现栈详解

文章目录 一、队列实现栈的特点分析 1、1 具体分析 1、2 整体概括 二、队列模拟实现栈代码的实现 2、1 手撕 队列 代码 queue.h queue.c 2、2 用队列模拟实现栈代码 三、总结 &#x1f64b;‍♂️ 作者&#xff1a;Ggggggtm &#x1f64b;‍♂️ &#x1f440; 专栏&#xff1…

入职第一天就被迫离职,找工作多月已读不回,面试拿不到offer我该怎么办?

大多数情况下&#xff0c;测试员的个人技能成长速度&#xff0c;远远大于公司规模或业务的成长速度。所以&#xff0c;跳槽成为了这个行业里最常见的一个词汇。 前言 前几天&#xff0c;我们一个粉丝跟我说&#xff0c;正常入职一家外包&#xff0c;什么都准备好了&#xff0…

Portainer堪称最优秀的容器化管理平台

一、Portainer是什么&#xff1f; Portainer是一款开源的容器管理平台&#xff0c;支持多种容器技术&#xff0c;如Docker、Kubernetes和Swarm等。它提供了一个易于使用的Web UI界面&#xff0c;可用于管理和监控容器和集群。Portainer旨在使容器管理更加简单和可视化&#xf…

WinForm | C# 界面弹出消息通知栏 (仿Win10系统通知栏)

ApeForms 弹出消息通知栏功能 文章目录ApeForms 弹出消息通知栏功能前言全局API通知栏起始方向通知排列方向通知栏之间的间隔距离无鼠标悬停时的不透明度消息通知窗体的默认大小示例代码文本消息提示栏文本消息提示栏&#xff08;带选项&#xff09;图文消息提示栏图文消息提示…

【Spring-boot源码剥析】| 启动原理之侠客行篇

目录一. 传说篇二. 快速启动原理三. 自动配置原理3.1 准备阶段3.2 配置阶段3.3 运行阶段三. Pefect Ending一. 传说篇 江湖传说&#xff0c;有一个神秘的江湖大侠&#xff0c;他名叫SpringBoot&#xff0c;擅长于开发出快速启动的应用程序。这个侠客的江湖名号传遍了整个江湖&a…

did not find expected key while parsing a block mapping at line 2 column 1的解决方法

问题描述 真的是困扰了好久的一个问题&#xff0c;真的是邪乎了&#xff0c;报的错误实际上是错的 完整报错&#xff1a; Error: YAML Exception reading /path_to_your_blog/_publications/2020-08-21.md: (<unknown>): did not find expected key while parsing a b…

JQuery

概述&#xff1a; JQuery&#xff1a;JavaScript和查询&#xff0c;他是辅助JavaScript开发的js类库。 他的的核心思想就是write less&#xff0c;do moire 实现了很多浏览器兼容问题 JQuery的核心函数 $(参数) 1 参数是函数&#xff1a;$(function(){}) window.onlooad fun…

AI风暴 :文心一言 VS GPT-4

&#x1f497;wei_shuo的个人主页 &#x1f4ab;wei_shuo的学习社区 &#x1f310;Hello World &#xff01; 文心一言 VS GPT-4 文心一言&#xff1a;知识增强大语言模型百度全新一代知识增强大语言模型&#xff0c;文心大模型家族的新成员&#xff0c;能够与人对话互动&#…

TryHackMe-Zeno(boot2root)

Zeno 你有和伟大的斯多葛派哲学家芝诺一样的耐心吗&#xff1f;试试吧&#xff01; 端口扫描 循例 nmap Web枚举 进到12340端口 目录扫描 /rms是一个业务站点 在admin登录页面尝试弱口令和注入&#xff0c;也都没有成功 SQLI 在点餐这发现了个get参数id&#xff0c;尝试sql…

八大排序算法之归并排序(递归实现+非递归实现)

目录 一.归并排序的基本思想 归并排序算法思想(排升序为例) 二.两个有序子序列(同一个数组中)的归并(排升序) 两个有序序列归并操作代码: 三.归并排序的递归实现 递归归并排序的实现:(后序遍历递归) 递归函数抽象分析: 四.非递归归并排序的实现 1.非递归归并排序算法…