Linux 多线程( 进程VS线程 | 线程控制 )

2023-09-16 20:52:34

Linux进程 VS 线程

  1. 进程是资源分配的基本单位。
  2. 线程是OS调度的基本单位。

线程共享进程数据,但也拥有自己的一部分数据:

  • 线程ID
  • 一组寄存器,用来保存每个线程的上下文数据,让每个线程能够合理调度。
  • ,每个线程入栈出栈产生的临时变量必须保存到每个线程的私有栈中,所以栈对于每个线程来说也是私有的。
  • errno
  • 信号屏蔽字
  • 调度优先级

进程的多个线程共享

因为在在同一个地址空间,所以所谓的代码段,数据段都是共享的。

  • 如果定义一个函数,各个线程都可以调用。
  • 如果定义一个全局变量,那么一个进程中的多个执行流都可以访问到。

除此之外,各线程还共以下资源和环境:

  • 文件描述符 ( 进程打开一个文件,其他线程也能够看到并访问。
  • 各种信号的处理方式了。( SIG_IGN,SIG_DFL 等默认处理的信号函数 或者 自定义的信号处理函数).
  • 当前工作目录
  • 用户ID和组ID。

进程和线程的关系

进程和线程的关系,例如:

在这里插入图片描述
之前,我们都是以单线程进程学习为主,以后我们也将尝试解除单进程多线程学习。

线程创建 pthread_create

创建线程的函数为pthread_create,原型如下:

int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);

参数说明

  • thread:获取创建成功的线程ID,该参数是一个输出型参数。
  • attr:用于设置创建线程的属性,传入NULL表示使用默认属性。
  • start_routine:返回值和参数均为void*的函数指针。该参数表示线程例程,即线程启动后要执行的函数。
  • arg:传给线程例程的参数。

返回值说明

  • 线程创建成功返回0,失败返回错误码。

注意
Linux不能真正意义上的帮我们提供线程的接口,但是Linux有原生线程库,使用此函数必须在编译时带上 -pthread 选项。

以下例子中,我们让主线程创建一个新线程,预计主线程与新线程分别去执行对应的函数代码。

void* Routine(void* arg)
{
	char* msg = (char*)arg;
	while (1){
		cout << " i am a thread 1 " << endl;
		sleep(1);
	}
}
int main()
{
	pthread_t tid;
	pthread_create(&tid, NULL, Routine, (void*)"thread 1");
	while (1){
	    cout << " I am a main thread " << endl;
		sleep(2);
	}
	return 0;
}

结果如下:
在这里插入图片描述
当然我们也可以使用 ps -ajx 命令来查看当前进程信息,但是,使用该命令只查到了mythread进程相关信息,没有显示其他的线程。
在这里插入图片描述
所以,我们可以使用 ps -aL 命令,来显示当前进程中的线程信息。其中LWP( Light Weight Process )就是代表该线程的ID,可以看到,这两个线程的PID是一样的,就代表它们同属于一个进程。
在这里插入图片描述
我们以前学习进程的时候认为OS调度的时候以PID为准,实际上OS调度的时候采用的是PWD,只不过主线程的PWD和PID是一样的,所以单线程进程调度时采用PID和PWD实际上是一样的。

获取线程ID pthread_self

我们可以调用pthread_self函数获取线程PWD。

函数原型如下

pthread_t pthread_self(void);

以下代码,我们通过pthread_self函数分别打印主线程和新线程的PID和PWD。

void *threadRun( void *args )
{
    const string name = ( char * )args;
    
    int count = 0;

    while( count < 5 )
    {
        cout << name << " pid: " << getpid()  << " PWd "<< pthread_self()<<  endl;
            
        sleep(1);

        ++count;
    }
    return nullptr;
}
int main()
{
    pthread_t tid[5];

    char name[64];

    for ( long long i = 0; i < 5; ++i )
    {
        snprintf( name, sizeof name, "%s - %d", "thread", i );

        pthread_create( tid + i,NULL,threadRun, (void *)name );

        sleep(1);
    }
     
    cout << " i am a main thread " << " getpid: " << getpid() << " PWD " << pthread_self() << endl;
    return 0;
}

结果如下:
在这里插入图片描述

线程等待 pthread_join

首先,我们应该注意的是,一个线程被创建出来,这个线程就如同进程一般,也是需要被等待的。如果主线程不对新线程进程等待,那么新线程资源是不会被回收的。此时,便有了pthread_join函数专门对新线程处理。

函数原型如下

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

参数说明

  • thread: 被等待的线程ID。
  • retal:该retval为二级指针,一级指针指向线程的返回值。

返回值说明
线程等待成功返回0,失败返回错误码。

  • 如果thread线程通过return返回,retal所指向的单元里存放的是thread线程函数的返回值。

  • 如果thread线程被别的线程调用pthread_ cancel异常终掉,retal所指向的单元里存放的是常数PTHREAD_ CANCELED,该常数值为-1。

  • 如果thread线程是自己调用pthread_exit终止的,retal所指向的单元存放的是传给pthread_exiit的参数。

  • 如果对thread线程的终止状态不感兴趣,可以传NULL给retal参数。

例如,以下代码主线程创建一个新线程后,阻塞等待新线程打印10次后退出,主线程也随之退出。


void* threadRoutine( void* args )
{
    int i = 0;
    while( true )
    {
        cout << "新线程: " << ( char* )args << " running... " << endl;
        sleep(1);
        if( i++ == 10 ) break;
    }

    cout << "new thread quit... " << endl;

    return nullptr;
}

int main()
{
    pthread_t tid;
    
    pthread_create( &tid,nullptr,threadRoutine,(void*)"thread 1 ");

    pthread_join( tid,nullptr );
    
    cout<< " main thread wait done ... main quit " << endl;
}

结果如下:
在这里插入图片描述

pthread_join第二个参数

当新线程退出后,我们可以对新线程返回值设置特定值,但是需要将该值以地址的形式返回。新线程退出时,由主线程中的ret指针保存,但是如果需要改变一级指针保存的数据需要传入二级指针(ret的地址)才能获取到ret进而改变。

void* threadRoutine( void* args )
{
    int i = 0;
    while( true )
    {
        cout << "新线程: " << ( char* )args << " running... " << endl;
        sleep(1);
        if( i++ == 10 ) break;
    }

    cout << "new thread quit... " << endl;

    return (void*)10;
}

int main()
{
    pthread_t tid;
    
    pthread_create( &tid,nullptr,threadRoutine,(void*)"thread 1 ");

    void* ret = nullptr;

    pthread_join( tid,&ret );
    
    cout<< " main thread wait done ... main quit " << " exitcode: " <<  (long long )ret<<  endl;
}

结果如下:

在这里插入图片描述
我们知道,每个线程的栈是私有的,但是我们也可以通过 pthread_join第二个参数来获取,这更加体现了主新线程之间的数据传输。
例如: 我们在threadRoutine例程中创建了一个数组,并通过返回值返回由ret指针接受。

void* threadRoutine( void* args )
{
    int i = 0;
    int* data = new int[11];
    while( true )
    {
        cout << "新线程: " << ( char* )args << " running... " << endl;
        sleep(1);
        data[i] = i;
        if( i++ == 10 ) break;
    }

    cout << "new thread quit... " << endl;

    return (void*)data;
}

int main()
{
    pthread_t tid;
    
    pthread_create( &tid,nullptr,threadRoutine,(void*)"thread 1 ");

    int* ret = nullptr;

    pthread_join( tid,(void**)&ret );
    
    //cout<< " main thread wait done ... main quit " << " exitcode: " <<  endl;

    for( int i = 0; i < 10; i++  )
    {
        cout << ret[i] << endl;
    }
    return 0;
}

结果如下:
在这里插入图片描述

线程出现异常吗,整个进程也出现异常。

在以上的代码中,我们在例程中写出除0错误,当该线程崩溃时,整个进程也将随即崩溃,此时再获取线程的退出码也没有意义。

void* threadRoutine( void* args )
{
    int i = 0;
    int* data = new int[11];
    while( true )
    {
        cout << "新线程: " << ( char* )args << " running... " << endl;
        sleep(1);
        data[i] = i;
        if( i++ == 10 ) break;
         
        int a = 100;
        
        a /= 0;
        
    }

    cout << "new thread quit... " << endl;

    return (void*)data;
}

int main()
{
    pthread_t tid;
    
    pthread_create( &tid,nullptr,threadRoutine,(void*)"thread 1 ");

    int* ret = nullptr;

    pthread_join( tid,(void**)&ret );
    
    //cout<< " main thread wait done ... main quit " << " exitcode: " <<  endl;

    for( int i = 0; i < 10; i++  )
    {
        cout << ret[i] << endl;
    }
    return 0;
}

结果如下:
在这里插入图片描述

终止线程

如果需要只终止某个线程而不是终止整个进程,可以有三种方法:

  • 从线程函数return。
  • 线程可以自己调用pthread_exit函数终止自己。
  • 一个线程可以调用pthread_cancel函数终止同一进程中的另一个线程。

终止线程pthread_exit

pthread_exit函数的功能就是终止线程,pthread_exit函数的函数原型如下:

void pthread_exit(void *retval);

参数说明
retval:线程退出时的退出码信息。

例如: 我们使用Pthread_exit函数终止进程,并将退出码设为10。

void* threadRoutine( void* args )
{
    int i = 0;
    int* data = new int[11];
    while( true )
    {
        cout << "新线程: " << ( char* )args << " running... " << endl;
        sleep(1);
        data[i] = i;
        if( i++ == 10 ) break;
         
    }

    cout << "new thread quit... " << endl;

   pthread_exit((void*)10);
}

int main()
{
    pthread_t tid;
    
    pthread_create( &tid,nullptr,threadRoutine,(void*)"thread 1 ");

    int* ret = nullptr;

    pthread_join( tid,(void**)&ret );
    
    cout<< " main thread wait done ... main quit " << " exitcode: " << ( long long ) ret  <<  endl;
    
    return 0;
}

结果如下
在这里插入图片描述

注意
exit函数的作用是终止进程,任何一个线程调用exit函数也代表的是整个进程终止。

终止进程 pthread_cancel

我们可以通过pthread_cancel函数取消某一个线程,该函数原型如下:

int pthread_cancel(pthread_t thread);

参数说明

thread:被取消线程的ID。

返回值说明

线程取消成功返回0,失败返回错误码。

例如: 我们让新线程执行一段时间,随后主线程调用pthread_cancel函数取消该新线程,我们一般都是由主线程取消新线程,( 这是pthread_cancel 的常规用法 )


void* threadRoutine( void* args )
{
    int i = 0;
    int* data = new int[11];
    while( true )
    {
        cout << "新线程: " << ( char* )args << " running... " << endl;
        sleep(1);
        data[i] = i;
        if( i++ == 10 ) break;
         
    }

    cout << "new thread quit... " << endl;

   pthread_exit((void*)10);
}

int main()
{
    pthread_t tid;
    
    pthread_create( &tid,nullptr,threadRoutine,(void*)"thread 1 ");

    int count = 0;
    
    while( true )
    {
        cout << "main线程: " << "running..." << endl;
        sleep(1);
        count++;
        if( count >= 5 ) break;
    }
    pthread_cancel(tid);

    int* ret = nullptr;

    pthread_join( tid,(void**)&ret );
    
    cout<< " main thread wait done ... main quit " << " exitcode: " << ( long long ) ret  <<  endl;
    
    return 0;
}

结果如下
我们可以看出,此时的新线程返回值不再是我们原先设置的10,因为该新线程是由pthread_cancel函数取消终止的,OS默认设置其返回值为-1.
在这里插入图片描述

进程分离

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

pthread_detach函数原型如下:

int pthread_detach(pthread_t thread);

可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离,但是常规情况下,我们一般让新线程自己分离。

void* threadRoutine( void* args )
{
    pthread_detach(pthread_self());
    while( true )
    {
        cout << "新线程: " << ( char* )args << endl;
    
        sleep(1);
    }

    cout << "new thread quit... " << endl;

   pthread_exit((void*)10);
}

int main()
{
    pthread_t tid;
    
    pthread_create( &tid,nullptr,threadRoutine,(void*)"thread 1 ");

    int count = 0;
    
    while( true )
    {
        cout << " main 线程 " << endl;
        sleep(1);
        count++;
        if( count >= 5 ) break;
    }
    
    cout<< " main thread wait done ... main quit " <<  endl;
    
    return 0;
}

注意
joinable和分离是冲突的,一个线程不能既是joinable又是分离的,并且在常规线程分离的场景中,主线程一般用来创建新线程处理任务和回收资源,一般都是最后退出的。如果主线程先退出,就意味着进程退出,那么新线程也立刻会随即退出。

线程ID及进程地址空间布局

线程ID本质上是一个地址

  • pthread_read函数会产生一个线程ID,存放在第一个参数指向的地址中,但是线程ID与前面所说的线程ID LWP 不同。
  • 前面讲的线程ID属于进程调度的范畴。因为线程是轻量级进程,是操作系统调度器的最小单位,所以需要一个数值来唯一表示该线程。
  • pthread_ create函数第一个参数指向一个虚拟内存单元,该内存单元的地址即为新创建线程的线程ID,属于NPTL线程库的范畴。线程库的后续操作,就是根据该线程ID来操作线程的。
  • 线程库NPTL提供了pthread_ self函数,可以获得线程自身的ID。

当进程运行时,pthread共享库即加载到物理内存中,再根据页表,映射到进程地址空间中的共享区。

在这里插入图片描述

主线程和新线程都含有各自的独立栈结构来保存每一个线程都是独立的,主线程用的是内核级的栈结构,每一个新线程都含有共享区中独有的pthread库中的栈结构。为了对这些属性数据进行管理,OS采用了“先描述,再组织”的方式,该动态库中包含了一个个struct pthread结构体,其中包含了线程栈,上下文等数据,而线程ID(tid)便是动态库中每一个struct pthread结构体的首地址,进而CPU通过tid来找到对应的线程。

在这里插入图片描述

打印线程ID

我们现在可以对线程ID进行打印。

void* threadRoutine( void* args )
{
    int i = 0;
    int* data = new int[11];
    while( true )
    {
        cout << "新线程: " << ( char* )args << " running... " << endl;
        sleep(1);
        data[i] = i;
        if( i++ == 10 ) break;
         
    }

    cout << "new thread quit... " << endl;

   pthread_exit((void*)10);
}

int main()
{
    pthread_t tid;
    
    pthread_create( &tid,nullptr,threadRoutine,(void*)"thread 1 ");

    printf( " %lu , %p \n ",tid,tid );

    int count = 0;
    
    while( true )
    {
        cout << "main线程: " << "running..." << endl;
        sleep(1);
        count++;
        if( count >= 5 ) break;
    }
    pthread_cancel(tid);

    int* ret = nullptr;

    pthread_join( tid,(void**)&ret );
    
    cout<< " main thread wait done ... main quit " << " exitcode: " << ( long long ) ret  <<  endl;
    
    return 0;
}

结果如下
可见,线程ID本质上就是一个地址。
在这里插入图片描述

线程的局部存储

我们知道,全局变量,已初始化数据,未初始化数据等都是线程间共享的。但是,我们可以在全局变量前添加__pthread 代表每一个线程都含有该独有的全局变量保存在每一个线程局部存储变量中。

例如: 我们分别通过主线程和新新线程打印全局变量g_val的值和地址。

__thread  int g_val = 0;
void* threadRoutine( void* args )
{
    int i = 0;
    int* data = new int[11];
    while( true )
    {
        cout << "新线程: " << ( char* )args << " g_val: " << g_val <<  " &g_val "  << &g_val <<  endl;
        
        ++g_val;
              
        sleep(1);
    }

    cout << "new thread quit... " << endl;

   pthread_exit((void*)10);
}

int main()
{
    pthread_t tid;
    
    pthread_create( &tid,nullptr,threadRoutine,(void*)"thread 1 ");

    int count = 0;
    
    while( true )
    {
        cout << "main线程: " << " g_val " << g_val << " &g_val " << &g_val <<  endl;
        sleep(1);
        count++;
        if( count >= 5 ) break;
    }
    pthread_cancel(tid);

    int* ret = nullptr;

    pthread_join( tid,(void**)&ret );
    
    cout<< " main thread wait done ... main quit " << " exitcode: " << ( long long ) ret  <<  endl;
    
    return 0;
}

结果如下:
我们可以看到,主线程g_val值没有变化,而新线程g_val每一次打印都增加了1,并且主新线程中的g_val的地址是不同的。
在这里插入图片描述

更多推荐

Selenium+python怎么搭建自动化测试框架、执行自动化测试用例、生成自动化测试报告、发送测试报告邮件

本人在网上查找了很多做自动化的教程和实例,偶然的一个机会接触到了selenium,觉得非常好用。后来就在网上查阅各种selenium的教程,但是网上的东西真的是太多了,以至于很多东西参考完后无法系统的学习和应用。以下整理的只是书中自动化项目的知识内容,介绍怎么搭建自动化测试框架、执行自动化测试用例、生成自动化测试报告、

JSP ssm 网上求职管理系统myeclipse开发mysql数据库springMVC模式java编程计算机网页设计

一、源码特点JSPssm网上求职管理系统是一套完善的web设计系统(系统采用SSM框架进行设计开发,spring+springMVC+mybatis),对理解JSPjava编程开发语言有帮助,系统具有完整的源代码和数据库,系统主要采用B/S模式开发。开发环境为TOMCAT7.0,Myeclipse8.5开发,数据库为M

Spring学习(三):MVC

一、什么是MVCMVC(Model-View-Controller)是一种软件设计模式,用于组织和管理应用程序的代码结构。它将应用程序分为三个主要部分,即模型(Model)、视图(View)和控制器(Controller),每个部分都有特定的职责和功能。以下是MVC模式中各个组成部分的概述:模型(Model):模型代表

软件机器人助力企业产地证自动化申报,提高竞争力,降低成本

在国际贸易中,产地证是一项重要的文件,它用于证明货物的原产地,有助于企业在海外清关时获得优惠税率。然而,产地证的申报过程通常涉及繁琐的数据整理和报文生成,消耗了大量时间和精力。本文将探讨如何利用博为小帮软件机器人实现产地证的自动化申报,以提高工作效率和优惠税率的获取。软件机器人简介软件机器人是一种自动化软件机器人,可以

RFID产线自动化升级改造管理方案

应用背景在现代制造业中,产线管理是实现高效生产和优质产品的关键环节,产线管理涉及到生产过程的监控、物料管理、工艺控制、质量追溯等多个方面,有效的产线管理可以提高生产效率、降低成本、改善产品质量,并满足市场需求的变化。产线管理的难点和挑战数据采集和记录的准确性和效率低下:传统的手工记录和条码扫描方式需要大量的人工操作,非

七天学会C语言-第二天(数据结构)

1.If语句:If语句是一种条件语句,用于根据条件的真假执行不同的代码块。它的基本形式如下:if(条件){//条件为真时执行的代码}else{//条件为假时执行的代码}写一个基础的If语句#include<stdio.h>intmain(){intx=10;if(x>5){printf("x大于5\n");}else{

【深度学习】Pytorch 系列教程(十一):PyTorch数据结构:3、变量(Variable)介绍

目录一、前言二、实验环境三、PyTorch数据结构0、分类1、张量(Tensor)2、张量操作(TensorOperations)3、变量(Variable)一、前言ChatGPT:PyTorch是一个开源的机器学习框架,广泛应用于深度学习领域。它提供了丰富的工具和库,用于构建和训练各种类型的神经网络模型。下面是PyT

【C++】详解std::mutex

2023年9月11日,周一中午开始2023年9月11日,周一晚上23:25写完目录概述头文件std::mutex类的成员类型方法没有std::mutex会产生什么问题问题一:数据竞争问题二:不一致lock和unlock死锁概述std::mutex是C++标准库中提供的一种同步原语,用于保护共享资源的访问。std::mu

防火墙 (五十四)

目录前言一、防火墙作用二、防火墙分类三、防火墙性能四、硬件防火墙五、软件防火墙5.1iptables六、iptables应用前言本文就简单的介绍了防火墙的基础内容和一些简单案例的操作。提示:以下是本篇文章正文内容,下面案例可供参考一、防火墙作用在计算机领域,防火墙是用于保护信息安全的设备,其会依照用户定义的规则,允许或

Ascend-pytorch插件介绍及模型迁移

Ascend-pytorch插件介绍及模型迁移用于昇腾适配PyTorch框架,为使用PyTorch框架的开发者提供昇腾AI处理器的超强算力。links:AscendPyTorch官方仓库PyTorch官方主页PyTorch官方文档PyTorch官方仓库当前(2023.9.20)AscendPyTorch支持的pytor

数字孪生行业相关政策梳理--工业领域相关政策(可下载)

&nbsp;&nbsp;&nbsp;&nbsp;自2021年国家“十四五”规划纲要提出“探索建设数字孪生城市”以来,国家发展和改革委员会、工业和信息化部、住房和城乡建设部、水利部、农业农村部等部门纷纷出台政策,大力推动数字孪生在千行百业的落地发展。这些政策不仅为数字孪生的应用提供了广阔的舞台,也为相关产业的发展提供了坚

热文推荐