【Linux】线程分离和线程互斥

news2024/11/25 4:29:57

终于到线程互斥了~

文章目录

  • 前言
  • 一、线程分离
  •         如何理解线程库和线程ID
  • 二、线程互斥
  • 总结


前言

在上一篇文章中我们学习了线程控制,比如创建一个线程,取消一个线程以及等待线程,这篇文章我们讲两个非常重要的概念,一个是线程分离,另一个是线程互斥


一、线程分离

分离线程
默认情况下,新创建的线程是 joinable 的,线程退出后,需要对其进行 pthread_join 操作,否则无法释放资源,从而造成系统泄漏。
如果不关心线程的返回值, join 是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。
int pthread_detach(pthread_t thread);
可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离,如下:
pthread_detach(pthread_self());

下面我们先写一个测试代码,让程序跑起来然后我们再测试线程分离接口:

#include <iostream>
#include <cstdio>
#include <cstring>
#include <string>
#include <pthread.h>
#include <unistd.h>
using namespace std;

void *threadRoutine(void* args)
{
    string name = static_cast<const char*>(args);
    int cnt = 5;
    while (cnt)
    {
        cout<<name<<" : "<<cnt--<<endl;
        sleep(1);
    }
    return nullptr;
}
int main()
{
    pthread_t tid;
    pthread_create(&tid,nullptr,threadRoutine,(void*)"thread 1");
    int n = pthread_join(tid,nullptr);
    if (n!=0)
    {
        cerr<<"error: "<<n<<" : "<<strerror(n)<<endl;
    }
    return 0;
}

 可以看到线程运行后5秒都退出了,下面下面我们加入分离接口:

 这段代码的意思是我们刚创建一个新线程就将新线程分离了,而我们将线程分离后如果还是正常的去join线程是会出错的,下面我们运行起来看看:

果然出错了,Invalid argument说明我们的参数是错误的,这是因为我们刚刚pthread_detach的参数是不合法的,所以:一个线程被设置为分离状态后,是不需要join的!

那么如果我们让这个线程自己分离自己呢?

 通过运行我们发现并没有什么问题,我们让主线程sleep(1)再看一下:

 可以看到又报参数错误了,出现这种错误的原因是:我们调度哪个线程是不确定的,像刚才的代码如果我们直接调度主线程导致新线程一行代码也没跑直接主线程就join了,就又会像之前那样出现参数错误。

下面我们总结一下线程分离:当我们想join一个线程的时候那就不要进行分离,当我们不想去join一个线程那就直接将这个线程分离即可。

如何理解线程库和线程ID:

线程库:

 首先我们学动静态库的时候知道,库是在磁盘中存放的,从磁盘映射到了物理内存然后经过页表的转化映射到进程地址空间的共享区当中,又因为我们的线程是共享进程的进程地址空间的,所以我们的线程是可以随时随地访问共享区中的库的。

那么线程库是如何管理线程的呢?先描述再组织。先给线程创建类似的管理线程的TCB(类似于PCB),下面我们看一张图:

 在这张图中,mmap区域就是我们的共享区,右边的动态库等信息就是我们的线程库,里面有管理线程的结构体等,而要找我们的线程ID该怎么去找呢?我们可以看到pthread_t tid的小箭头指向结构体,实际上pthread_t 就是一个地址数据,用来标识线程相关属性集合的起始地址。所以我们之前打印线程id的时候是很长的数据,为什么长呢因为那是地址!!下面我们将代码修改一下演示出id:

 可以看到确实打印出来的ID是很长,下面我们将这个ID转换为16进制的地址:

string hexAddr(pthread_t tid)
{
    char buffer[64];
    snprintf(buffer,sizeof(buffer),"0x%x",tid);
    return buffer;
}

 下面我们运行起来:

 这一次我们可以看到地址变的正常了。我们在上面线程的图中可以看到线程局部存储和线程栈,其实学过线程的都知道线程是有自己的私有栈的,只不过不知道这个栈在哪里,从图中我们可以看到这个栈是在线程自己的地址当中,每个线程有struct,线程局部存储和线程栈,通过地址找到这些内容。

总结:线程库的作用是给用户提供操作线程的接口,在我们创建线程的时候会在线程库里面给我们创建一个描述线程相关的struct,然后还会创建一个轻量级进程,线程结构体里会包含线程自己的栈结构,局部存储等信息。线程的ID就是描述线程结构体TCB的起始地址,每个线程都有自己的栈在库当中存放。

下面我们编写代码验证一下每个线程中的私有栈:

int main()
{
    pthread_t t1,t2,t3;
    pthread_create(&t1,nullptr,threadRoutine,(void*)"thread 1");
    pthread_create(&t2,nullptr,threadRoutine,(void*)"thread 2");
    pthread_create(&t3,nullptr,threadRoutine,(void*)"thread 3");
    pthread_join(t1,nullptr);
    pthread_join(t2,nullptr);
    pthread_join(t3,nullptr);
    return 0;
}

 我们开3个线程,然后每个线程进入routine函数的时候都打印一下cnt这个变量的地址,如果不一样则说明他们有自己独立的栈:

仔细观察可以看到地址是不一样的,地址很相似是因为他们都在共享区。下面我们再看看全局变量的地址:

 我们可以看到全局变量的地址一样说明3个线程都是同一个全局变量,这就证明了线程共享进程的地址空间。当然我们也可以在全局变量前面加上__thread让全局变量变成每个线程的局部存储:

 运行后我们可以看到地址确实不一样了。下面我们进入互斥的内容

二、线程互斥

在多线程中,有一个全局的变量,是被所有执行流共享的,而线程中大部分资源都会直接或者间接共享,而这就可能会存在并发访问的问题,如下图:

 当我们要对一个全局变量进行--操作时,先将内存中的代码加载到CPU的寄存器当中,计算后将结果再写会内存中,这样内存中的100就变成了99:

 这个时候另一个线程过来了,这个线程是将100减到10所以在寄存器中减到10后将10写入内存中,然后线程B的时间片到了就重新调度线程A:

 本来线程B好不容易将数减到10了结果A线程回来后数据又变成了99。所以当我们对全局变量做--操作时,如果没有保护的话,会存在并发访问的问题,进而导致数据不一致问题。所以为了解决这样的问题,引入了互斥这个概念。

进程线程间的互斥相关背景概念:
临界资源:多线程执行流共享的资源就叫做临界资源。
临界区:每个线程内部,访问临界资源的代码,就叫做临界区。 
互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用。
原子性:不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完 成。
下面我们用一个多线程并发抢票的代码来演示互斥问题:
int tickets = 10000;

void *threadRoutine(void* name)
{
    string tname = static_cast<const char*>(name);
    while (true)
    {
        if (tickets>0)
        {
            usleep(2000);  //模拟抢票花费的时间
            cout<<tname<<" get a ticket: "<<tickets--<<endl;
        }
        else 
        {
            break;
        }
    }
    return nullptr;
}
int main()
{
    pthread_t t[4];
    int n = sizeof(t)/sizeof(t[0]);
    for (int i = 0;i<n;i++)
    {
        char* data = new char[64];
        snprintf(data,64,"thread-%d",i+1);
        pthread_create(t+i,nullptr,threadRoutine,data);
    }
    for (int i = 0;i<n;i++)
    {
        pthread_join(t[i],nullptr);
    }
    return 0;
}

上面代码中我们创建线程第一个参数是t+i是因为这个参数是指针类型,t是首元素地址所以这样写,join中第一个参数是线程id所以直接用t[i]即可。usleep可以让线程休眠:

 usleep休眠的时间是微秒为单位,所以我们相当于休眠0.002秒。下面我们运行起来:

 运行后经过多次抢票我们发现最后票数变成了负数,为什么是负数呢?这就是我们提到的并发问题了,和我们之前说的那个全局变量一样,由于没有对全局变量的上下问进行保护,所以会减到负数去,(比如说我们现在的票数是1,四个线程都进入到这个判断逻辑tickets>0,然后四个线程都进行--操作,这样票数就变成了-3 )要解决这个问题需要三点:

代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。
如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区。
如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。

下面我们演示如何完成互斥:

在我们对临界资源进行加锁前需要学习一下互斥锁的概念:

互斥量的接口:
初始化互斥量
初始化互斥量有两种方法:
方法 1 ,静态分配: pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
方法 2 ,动态分配:
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict
attr);
参数: mutex:要初始化的互斥量          attr: NULL
销毁互斥量
销毁互斥量需要注意:
使用 PTHREAD_ MUTEX_ INITIALIZER 初始化的互斥量不需要销毁
不要销毁一个已经加锁的互斥量
已经销毁的互斥量,要确保后面不会有线程再尝试加锁
int pthread_mutex_destroy(pthread_mutex_t *mutex)
互斥量加锁和解锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
返回值 : 成功返回0,失败返回错误号
调用 pthread_ lock 时,可能会遇到以下情况:
互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功。
发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么pthread_ lock 调用会陷入阻塞 ( 执行流被挂起 ) ,等待互斥量解锁。

我们可以看到参数是pthread_mutex_t类型,这个类型被称为互斥锁。每一把锁用完后都要用pthread_mutex_destroy进行销毁。

 然后我们有了锁后还需要知道加锁的接口:pthread_mutex_lock:

 对于加锁这个接口如果加锁成功就会对临界资源进行加锁,失败就会将当前线程阻塞住。当我们用完临界资源需要对这个资源进行解锁操作,我们解锁的时候必须保证一定能解锁,所以修改代码如下:

下面我们将代码运行起来:

这次我们可以看到没有并发访问的情况了,但是为什么只有一个线程在抢票呢?这是因为我们抢完票还要将票放入用户的数据库当中,但是我们的代码并没有这个场景,下面我们用usleep模拟一下:

 这样就让多个线程一起抢票了,以上就是我们对互斥锁的接口的使用,下面我们补充一些互斥锁的细节,我们先把代码修改一下:

 我们先创建一个局部的锁,然后对这个锁进行初始化,在结束前将这个锁销毁,那么局部的锁该如何被所有线程看到呢?我们用类解决这个问题:

class TData
{
public:
    TData(const string& name,pthread_mutex_t* mutex)
        :_name(name)
        ,_pmutex(mutex)
    {

    }
public:
    string _name;
    pthread_mutex_t* _pmutex;
};

 我们的思想很简单,就是让所有的线程都能看到我们的局部锁,所以我们定义了一个对象,对象中有线程的名字和锁,每个线程进入回调函数后都会给自己进行加锁解锁操作,下面我们运行起来看:

 运行起来后我们可以看到和我们一开始的局部变量的效果一模一样,对于加锁我们总结四点:

1.凡是访问同一个临界资源的线程,都要进行加锁保护,而且必须加同一把锁,这是一个都遵守的规则,不能有例外。

2.每一个线程访问临界区之前,得加锁,加锁本质是给临界区加锁,加锁的粒度尽量要细一些。

3.线程访问临界区的时候,需要先加锁。所有线程都必须要先看到同一把锁,锁本身就是公共资源,锁如何保证自己的安全呢?因为锁是原子性的,所以无需保证。

4.临界区可以是一行代码,可以是一批代码。当一个线程已经申请到锁了,那么这个线程有可能被切换吗?当然是可能的,加锁只是保护这个线程的上下文数据。那么切换会有影响吗?不会。因为在我不在期间任何人都没有办法进入临界区,并且他无法成功的申请到锁,因为锁被原先申请到的那个进程拿走了。

5.加锁解锁正是体现互斥带来的串行化表现,站在其他线程的角度,对其他线程有意义的状态就是:锁被我申请(持有锁),锁被我释放了(不持有锁),原子性就体现在这里,要不有锁,要不没锁。


总结

以上就是线程分离和线程互斥的全部知识,本篇文章重点在于:

1.线程共享进程的地址空间。

2.线程有自己独立的栈结构(其实不只是栈,还有寄存器等)

3.线程分离后不需要join,如果不想join某个线程可以将它分离

还有就是我们加锁所总结的5点内容。

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

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

相关文章

CDH 之 hive 升级至 hive-3.1.3 完美踩坑过程

一、准备工作 1.1 前言 这是博主在升级过程中遇到的问题记录&#xff0c;大家不一定遇到过&#xff0c;如果不是 CDH 平台的话&#xff0c;单是 hive 服务升级应该是不会有这些问题的&#xff0c;且升级前博主也参考过几篇相关 CDH 升级 hive 服务的博文&#xff0c;前面的升级…

go语言操作以太坊智能合约

操作中要注意版本问题 geth版本、golang版本等 在remix环境中写好合约后&#xff0c;进行编译得到abi文件 简单举例 Hello.sol合约 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0;contract Hello {string Msg;function setMsg(string memory _msg) public{Msg_msg…

SM2密码算法规范介绍

目录 1、SM2的密钥对 1.1、SM2公钥 1.2、SM2私钥 2、密钥数据格式 2.1、 GMT0009 2.2、 GMT0016 2.3、 GMT0018 3、加密数据格式 3.1、GMT0009 3.2、GMT0016 3.3、GMT0018 4、签名数据格式 4.1、GMT0009 4.2、GMT0016 4.3、GMT0018 1、SM2的密钥对 1.1、SM2公钥…

java版DVD影碟片出租赁系统C/S模式 java电影购票系统课程设计

系统采用c/s架构&#xff0c;当然&#xff0c;你可以服务端、客户端都在同一台电脑上运行&#xff1b; 也可以在同一局域网内服务端、客户端在其它不同电脑上运行&#xff1b; 如果你有云服务器&#xff0c;可将Service端代码部署至云服务器上&#xff0c;客户端在任何一台有…

不愧是阿里P8出手的并发编程笔记!颠覆了我以往“正确“的认知

对于一个Java程序员而言&#xff0c;能否熟练掌握并发编程是判断他优秀与否的重要标准之一。因为并发编程是Java语言中最为晦涩的知识点&#xff0c;它涉及操作系统、内存、CPU、编程语言等多方面的基础能力&#xff0c;更为考验一个程序员的内功。 那到底应该怎么学习并发编程…

浅析给水管网动态水力建模数据及其管理

摘要&#xff1a;给水管网在城市生产及生活中发挥着十分重要的作用&#xff0c;所以&#xff0c;保证其正常运行便显得尤为必要了。本文将基于给水管网动态水力建模数据及其管理展开相应的分析&#xff0c;以期促进城市给水管网运行质量的提高&#xff0c;为同行提供一些有益的…

HTML4

前序知识 认识两位先驱 计算机基础知识 计算机俗称电脑&#xff0c;是现代一种用于高速计算的电子计算机器&#xff0c;可以进行数值计算、逻辑计算&#xff0c;还具有存储记忆功能。计算机由 硬件 软件组成 C/S架构与B/S架构 上面提到的应用软件&#xff0c;又分为两大类…

软件测试之登录测试详解

一、功能测试--登录功能性测试用例包括&#xff1a;1.什么都不输入&#xff0c;点击提交按钮&#xff0c;看提示信息。&#xff08;非空检查&#xff09;2.输入已注册的用户名和正确的密码&#xff0c;验证是否登录成功&#xff1b;3.输入已注册的用户名和不正确的密码&#xf…

PerformanceRunner即将发布2023年新版本,具体功能有哪些?

PerformanceRunner(简称PR)是国内专业的支持http、https、websocket、tcp/ip、MQ等各种协议、10万海量并发、可靠的性能测试工具/压力测试工具&#xff0c;降低了应用系统运行风险。 PerformanceRunner即将发布2023年新版本&#xff0c;具体功能有哪些&#xff1f; 1.录制时单机…

【Python入门篇】——Python函数(函数介绍,函数的定义,函数的参数和函数的返回值)

作者简介&#xff1a; 辭七七&#xff0c;目前大一&#xff0c;正在学习C/C&#xff0c;Java&#xff0c;Python等 作者主页&#xff1a; 七七的个人主页 文章收录专栏&#xff1a; Python入门&#xff0c;本专栏主要内容为Python的基础语法&#xff0c;Python中的选择循环语句…

都2023年了,网络安全还能入门吗?

前言 随着互联网的发展&#xff0c;网络安全逐渐成为重要领域之一。由于网络攻击已成为全球规模的问题&#xff0c;这使得网络安全专业成为各企业的重中之重。在这篇文章中&#xff0c;我们将从网络安全的发展趋势、就业前景和薪资水平方面入手&#xff0c;分析网络安全入门的…

【集群】部署LVS-DR群集

文章目录 一、LVS-DR工作原理1. 数据包流向分析2. DR模式的特点3. ARP 解析问题3.1 问题1&#xff1a;ip地址冲突3.2 问题2&#xff1a;真实服务器的第二次ARP请求3.3 解决ARP的两个问题的设置方法 二、LVS-DR部署过程1. 配置环境2. 部署服务器2.1 环境配置2.2 LVS-DR调度器2.2…

【基础知识】一文看懂深度优先算法和广度优先算法

概览 先上个图 现在我们要访问图中的每个节点&#xff0c;即图的遍历。 图的遍历是指&#xff0c;从给定图中任意指定的顶点&#xff08;称为初始点&#xff09;出发&#xff0c;按照某种搜索方法沿着图的边访问图中的所有顶点&#xff0c;使每个顶点仅被访问一次&#xff…

三、django-vue-admin开源项目二次开发——后端快速实现curd及接口

上一章&#xff1a; 二、django-vue-admin开源项目二次开发——修改默认菜单_做测试的喵酱的博客-CSDN博客 下一章&#xff1a; 一、背景 我想实现接口自动化用例的管理功能。一共涉及到两张表&#xff0c;一张项目表&#xff0c;是所有的项目列表。 一张是case列表&#…

Postgresql关于JSON、JSONB的操作符、处理函数(持续更新)

一、postgresql实用函数 &#xff08;只列举本次用到的函数&#xff0c;其他函数在文章后面有详解&#xff09; 1.1、将jsonb格式的数组中的值展开/分解成单独的数据/对象&#xff1f; SELECT answer_id, jsonb_array_elements(option_ids)::INTEGER AS option_id FROM db_l…

Java HashMap初始化大小设置多少合适

修改公司老代码的时候&#xff0c;发现阿里编码规约插件提示HashMap初始化时尽量指定初始值大小&#xff0c;因为设置合理的初始值可以提升性能&#xff1a; HashMap继承自AbstractMap类&#xff0c;实现了Map、Cloneable、java.io.Serializable接口&#xff0c;是基于散列表实…

Prompt 工程指南(三)—— 高级技术篇之零样本和少样本提示

到目前为止&#xff0c;显而易见的是&#xff0c;改进提示有助于在不同任务上获得更好的结果。这就是提示工程背后的理念和目标。 虽然上篇教程介绍的基本示例已经很有趣&#xff0c;但在接下来的几篇教程中&#xff0c;我们将介绍更高级的 Prompt 提示工程技巧&#xff0c;使…

LabVIEW编程开发PCB测试仪

LabVIEW编程开发PCB测试仪 使用PXI和LabVIEW的PCB钉床测试仪 用于PCB&#xff08;印刷电路板&#xff09;的钉床测试仪&#xff0c;使用PXI和LabVIEW。一家电子制造公司需要测试仪来测试他们的PCB产品。钉床测试仪是一种具有连接到电路板上各个测试点的引脚的测试。电路板需要…

【大数据学习篇12】在linux上安装jupyter

下面介绍在liunx怎么安装jupyter&#xff0c;一步到位介绍。 1、安装Anaconda3 1.1 自己选择一个位置下载 wget https://repo.continuum.io/archive/Anaconda3-5.0.1-Linux-x86_64.sh1.2 进入自己的安装目录&#xff0c;安装anaconda bash Anaconda3-5.2.0-Linux-x86_64.sh一…

小程序模板语法样式与页面配置

文章和代码已经归档至【Github仓库&#xff1a;https://github.com/timerring/front-end-tutorial 】或者公众号【AIShareLab】回复 小程序 也可获取。 文章目录 小程序模板与配置WXML 模板语法数据绑定事件绑定小程序中常用的事件事件对象的属性列表target 和 currentTarget 的…