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

文章目录

Linux进程 VS 线程

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

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

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

进程的多个线程共享

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

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

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

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

进程和线程的关系

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

之前,我们都是以单线程进程学习为主,以后我们也将尝试解除单进程多线程学习。

线程创建 pthread_create

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

cpp 复制代码
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 选项。

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

cpp 复制代码
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。

函数原型如下

cpp 复制代码
pthread_t pthread_self(void);

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

cpp 复制代码
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函数专门对新线程处理。

函数原型如下

cpp 复制代码
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次后退出,主线程也随之退出。

cpp 复制代码
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进而改变。

cpp 复制代码
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指针接受。

cpp 复制代码
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错误,当该线程崩溃时,整个进程也将随即崩溃,此时再获取线程的退出码也没有意义。

cpp 复制代码
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函数的函数原型如下:

cpp 复制代码
void pthread_exit(void *retval);

参数说明

retval:线程退出时的退出码信息。

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

cpp 复制代码
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函数取消某一个线程,该函数原型如下:

cpp 复制代码
int pthread_cancel(pthread_t thread);

参数说明

thread:被取消线程的ID。

返回值说明

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

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

cpp 复制代码
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函数原型如下:

cpp 复制代码
int pthread_detach(pthread_t thread);

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

cpp 复制代码
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进行打印。

cpp 复制代码
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的值和地址。

cpp 复制代码
__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的地址是不同的。

相关推荐
ai产品老杨11 分钟前
打通各大芯片厂商相互间的壁垒,省去繁琐重复的适配流程的智慧工业开源了
人工智能·开源·音视频·能源
磊灬泽2 小时前
【日常错误】鼠标无反应
linux·windows
知白守黑2672 小时前
Ansible角色
运维·服务器·ansible
Jwest20212 小时前
工业显示器在地铁电力监控与运维中的应用
运维·计算机外设
老马啸西风2 小时前
v0.29.2 敏感词性能优化之基本类型拆箱、装箱的进一步优化的尝试
性能优化·开源·nlp·github·敏感词
抠脚学代码6 小时前
Ubuntu Qt x64平台搭建 arm64 编译套件
数据库·qt·ubuntu
GitCode官方6 小时前
直播预告 | 开源低代码框架 Erupt 全生态讲解
低代码·开源
Miracle&6 小时前
2.TCP深度解析:握手、挥手、状态机、流量与拥塞控制
linux·网络·tcp/ip
专注API从业者7 小时前
Python/Java 代码示例:手把手教程调用 1688 API 获取商品详情实时数据
java·linux·数据库·python
Ribou7 小时前
Ubuntu 24.04.2安装k8s 1.33.4 配置cilium
linux·ubuntu·kubernetes