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

文章目录

  • Linux进程 VS 线程
    • 进程的多个线程共享
  • 进程和线程的关系
    • 线程创建 pthread_create
    • 获取线程ID pthread_self
    • 线程等待 pthread_join
    • 终止线程
    • 进程分离
    • 线程ID及进程地址空间布局

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的地址是不同的。
在这里插入图片描述

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

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

相关文章

Discrod账号为什么被封?怎么解封?

Discord作为海外社交产品的新晋王者&#xff0c;近两年来非常受欢迎&#xff0c;据统计&#xff0c;每个月使用Discord的用户数超过3000万。而在跨境电商领域&#xff0c;”内容社群”的打法已经见怪不怪&#xff0c;营销推广少不了Discord&#xff0c;拥有一个或者多个成熟的D…

01_网络编程_传统IO

网络编程 1.什么是网络编程 在网络通信协议下&#xff0c;不同计算机上运行的程序&#xff0c;进行的数据传输。 如果想把一个计算的结果&#xff0c;或者是电脑上的文件通过网络传递给你的朋友&#xff0c;就需要用到网络编程。 在实际生活中&#xff0c;网络通信无处不在…

JavaWeb基础学习(5)

JavaWeb基础学习 一、Filter1.1 Filter介绍1.2 Filter快速入门1.3、Filter执行流程1.4、Filter使用细节1.5、Filter-案例-登陆验证 二、Listener2.1 Listener介绍2.2、ServletContextListener使用 三、AJAX3.1 AJAX介绍与概念3.2 AJAX快速入门3.3 Axios异步架构3.4 JSON-概述和…

《C和指针》笔记24: 指针和间接访问

本文主要讲指针和间接访问&#xff0c;标题对应《C和指针对应的章节》&#xff0c;引用的地方是自己写的一些注释、理解和总结。 指针、间接访问和左值 先回顾一下左值和右值 左值代表着一个位置。右值代表着一个值。赋值等号左边是个左值&#xff0c;赋值等号右边是一个右值…

PCB layout在布线上的设计规范有哪些?

PCB Layout是一项技术活&#xff0c;也是经验活&#xff0c;良好的PCB Layout布线可帮助工程师确保最终的电路板性能、可靠性和制造质量&#xff0c;因此是很多电子工程师的学习重点&#xff0c;下面我们来盘点下PCB Layout关于布线的规范有哪些。 1、地管的引脚接地越短越好&a…

Linux下安装和使用MySQL的详细教程

✅作者简介&#xff1a;2022年博客新星 第八。热爱国学的Java后端开发者&#xff0c;修心和技术同步精进。 &#x1f34e;个人主页&#xff1a;Java Fans的博客 &#x1f34a;个人信条&#xff1a;不迁怒&#xff0c;不贰过。小知识&#xff0c;大智慧。 &#x1f49e;当前专栏…

linux相关知识以及有关指令3

在linux的世界中我们首先要有万物皆文件的概念&#xff0c;那么在系统中有那么多的文件&#xff0c;我们该怎么区分呢&#xff1f;文章目录 1. 文件分类2. 文件的权限1). 拥有者和所属组以及other2). 文件的权限3). 粘滞位4). 对于权限修改的拓展知识点a.修改权限b.修改拥有者所…

直播视频处理过程

视频其实就是快速播放一连串连续的图片。 每一张图片&#xff0c;我们称为一帧。只要每秒钟帧的数据足够多&#xff0c;也即播放得足够快。比如每秒 30 帧&#xff0c;以人的眼睛的敏感程度&#xff0c;是看不出这是一张张独立的图片的&#xff0c;这就是我们常说的帧率&#…

SpringMVC工程之非web部分代码复用,并独立运行

文章目录 概述一、独立运行前提二、实现步骤1. WebApplicationContext上下文配置文件定义2.servletContext上下文配置文件3. 定义独立运行的类main方法4. 开发环境运行main结果5. class文件运行6. jar运行&#xff08;推荐&#xff09; 概述 springMVC是位于spring web端的一个…

Llama2-Chinese项目:2.1-Atom-7B预训练

虽然Llama2的预训练数据相对于第一代LLaMA扩大了一倍&#xff0c;但是中文预训练数据的比例依然非常少&#xff0c;仅占0.13%&#xff0c;这也导致了原始Llama2的中文能力较弱。为了能够提升模型的中文能力&#xff0c;可以采用微调和预训练两种路径&#xff0c;其中&#xff1…

【Shiro】入门概述

1.是什么 Apache Shiro 是一个功能强大且易于使用的 Java 安全(权限)框架。Shiro 可以完 成&#xff1a;认证、授权、加密、会话管理、与 Web 集成、缓存 等。借助 Shiro 您可以快速轻松 地保护任何应用程序——从最小的移动应用程序到最大的 Web 和企业应用程序。 官网&…

项目上线部署--》服务器部署流程(一)

目录 &#x1f31f;准备工作 服务器购买 域名购买 域名解析&#xff08;配置 DNS&#xff09; &#x1f31f;服务器环境搭建 配置服务器 安装 CentOS 开发人员相关包 ​编辑 配置免密登陆 &#x1f31f;写在最后 &#x1f31f;准备工作 服务器购买 国内服务器&#x…