前言

目录
1.Linux下的线程概念
2.Linux线程控制:pthread线程库
在单执行流的进程中,此执行流独占了进程的所有资源

在一个进程内部,有时不一定只有一个执行流,在多执行流下,多个执行流共享了进程的地址空间,我们把“一个程序内部的控制序列”叫做线程

线程本质是在进程的地址空间内运行
进程的切换涉及到页表映射的切换,而线程的切换只是切换了指令序列而在同一个地址空间中进行

那么我们给出下面两个重要概念

  • 进程是操作系统分配资源的基本实体
  • 线程是进程内部的一个执行流

举个栗子:在这里插入图片描述
在现实生活中,假如我们把社会资源的基本单位看作是家庭,比如我们经常以家庭年收入统计社会财富的分配状况,那么此时:

  • 操作系统—>社会
  • 进程—>家庭
  • 线程—>家庭成员

家庭成员共享了家庭的资源,家庭成员之间有共享的资源,也有私人的小秘密。

透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流

线程可以被创建、等待、终止、控制
家庭有生老病死、家规等等…

1.Linux下的线程概念

在Linux下,其实没有真正意义上的线程概念,是用进程来模拟的

Linux的进程叫做轻量级进程

LWP是轻量级进程,在Linux下进程是资源分配的基本单位线程是cpu调度的基本单位,而线程使用进程pcb描述实现,操作系统在创建线程时给每个线程都创建一个pcb结构体,并且同一个进程中的所有pcb共用同一个虚拟地址空间,因此相较于传统进程更加的轻量化有了更多执行流之后。进程变成了分配资源的基本实体,进程一旦被创建好之后,里面可能有多个执行流

与进程相比,线程在CPU执行时可能更加轻量化:pcb上下文肯定要切换,但是线程的地址空间、页表不用换CPU调度时,看到的是LWP,也就是轻量级进程Light Weight Process

在这里插入图片描述

1.1 线程的优点

  • 创建一个新线程的代价要比创建一个新进程小得多
  • 与进程之间的切换相比,线程之间的切换需要OS的工作量更少
  • 线程占用的资源比进程少得多

1.2 线程能够看到进程的所有资源,因为所有PCB都共享地址空间

  • 好处:线程间通信成本低
  • 坏处:存在大量的临界资源,势必需要使用各种互斥和同步机制保证临界资源的安全

1.3 线程异常

线程是进程的一个执行分支,当发生野指针/除0等异常操作导致线程退出的同时,也意味着进程触发了该错误操作系统会给进程发送信号,终止进程。这体现了多线程下鲁棒性降低了。

1.4 线程的共享与独享

线程共享

  • 文件描述符表
  • 每种信号的处理方式
  • 工作目录
  • 用户id组id

独有:

  • 上下文数据(寄存器):体现了多个线程是可以切换的
  • 独立的栈结构:体现了线程是独立运行的,各自的上下文数据不会互相影响

2.Linux线程控制:pthread线程库

首先要强调一点:pthread库并不是系统库,而是Linux下为了模拟线程而采用的第三方库。本质是封装了对于轻量级进程的某些操作。

链接这些线程函数库时要使用编译器命令的“-lpthread”选项
接口:
pthread_create()
pthread_join()
pthread_cancel()
pthread_exit()
pthread_self()

2.1线程的创建

功能: 创建一个新的线程
原型

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

4个参数

  • thread:返回线程ID,这是进程地址空间的共享区的地址
  • attr:设置线程的属性,attr为NULL表示使用默认属性
  • start_routine:是个函数地址,线程启动后要执行的函数
  • arg:传给线程启动函数的参数,如果需要传入多个参数,可以用结构体封装

返回值: 成功返回0;失败返回错误码
在这里插入图片描述

让我们来玩一玩线程的创建,这边我们在main函数,也就是主线程创建了新线程,又在新线程中创建了另外5个更新的线程,使用ps -aL 命令查看轻量级进程

#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;
void* routine(void* args) {
  while(true) {
    cout << "I am a thread" <<  endl;
    sleep(1);
  }
  return nullptr;
}
void* ThreadRoute(void* args) {
  pthread_t tids[5];
  for(int i = 0; i < 5; i++) {
    pthread_create(tids+i, nullptr, routine, nullptr);
  }
  for(int i = 0; i < 5; i++) {
    pthread_join(tids[i], nullptr);
  }

  return nullptr;
}
int main()
{
  pthread_t tid; 
  int thread_id = pthread_create(&tid, nullptr, ThreadRoute, (void*)"I am thread");
  while(true) {
    std::cout << "I am main process" << std::endl;

    sleep(2);
  }
  return 0;
}

在这里插入图片描述

2.2 线程的终止

  • 从自己的例程中return,线程退出
  • 主线程退出,进程退出

有三种方法终止某个线程而不终止进程:

  1. 从线程函数return这种方法对主线程不适用,从main函数return相当于调用exit。
  2. 线程可以调用pthread_ exit终止自己。
  3. 一个线程可以调用pthread_ cancel终止同一进程中的另一个线程

线程一般终止之后,main thread等待,不等待会造成僵尸

为防内存泄漏,要保证主线程最后退出,让新线程正常结束

retral从pcb中提取退出码

  • 已经退出的线程,其空间没有被释放,仍然在进程的地址空间内。
  • 创建新的线程不会复用刚才退出线程的地址空间。
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
using namespace std;

void* ThreadRoute(void* args) {
  int a = 0;
  int b = 10;
  b = b/a;
  return nullptr;
}

int main()
{
  if(fork() == 0) {
    pthread_t tid; 
    int thread_id = pthread_create(&tid, nullptr, ThreadRoute, (void*)"I am thread");
    exit(11);
  }
  int status;
  pid_t id = waitpid(-1, &status, 0);
  cout << "exit code: " << ((status>>8)&0xff) << endl;
  cout << "sig: " << ((status)&0x7f) << endl;
  return 0;
}

我们精心设计了除0错误:
在这里插入图片描述
在进程exit code中,存有退出码+信号,而信号是针对进程的,线程崩溃进程随之崩溃

子进程创建的线程的除0错误导致OS给子进程发送信号,子进程的主线程崩溃,退出码收不到

关于pthread_cancel

  • sleep新线程被创建了但是没有被调度你就cancel了,建议一定要让新线程被调度跑起来
  • cancel具有一定的延时性,并不一定立即执行
  • cancel建议不要再开头或结尾使用

2.3 线程等待

为什么要等待?

  • 已经退出的线程,其空间没有被释放,仍然在进程的地址空间内。
  • 创建新的线程不会复用刚才退出线程的地址空间。

功能:等待线程结束
原型

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

参数

  • thread:线程ID
  • value_ptr:它指向一个指针,后者指向线程的返回值

返回值成功返回0;失败返回错误码

调用该函数的线程将挂起等待,直到id为thread的线程终止。thread线程以不同的方法终止,通过pthread_join得到的
终止状态是不同的,总结如下:

  1. 如果thread线程通过return返回,value_ ptr所指向的单元里存放的是thread线程函数的返回值。
  2. 如果thread线程被别的线程调用pthread_ cancel异常终掉,value_ ptr所指向的单元里存放的是常数PTHREAD_ CANCELED。
  3. 如果thread线程是自己调用pthread_exit终止的,value_ptr所指向的单元存放的是传给pthread_exit的参数。
  4. 如果对thread线程的终止状态不感兴趣,可以传NULL给value_ ptr参数。

我们让线程return一个new出来的5

#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
using namespace std;

void* ThreadRoute(void* args) {
  int* p = new int(5);
  cout << "threadID: " << pthread_self() << endl;
  sleep(2);
  return (void*)p;
}
int main()
{  
  if(fork() == 0) {
    pthread_t tid; 
    void* ret;
    int thread_id = pthread_create(&tid, nullptr, ThreadRoute, (void*)"I am thread");
    pthread_join(tid, &ret);
    cout << "ret: " << *(int*)ret << endl;
    delete (int*)ret;
    exit(11);
  }

  int status;
  pid_t id = waitpid(-1, &status, 0);
  cout << "exit code: " << ((status>>8)&0xff) << endl;
  cout << "sig: " << ((status)&0x7f) << endl;
  return 0;
}

在这里插入图片描述

2.4 获取线程ID

pthread_ create函数会产生一个线程ID,存放在第一个参数指向的地址中。该线程ID和前面说的线程ID不是一回事。

前面讲的线程ID属于进程调度的范畴。因为线程是轻量级进程,是操作系统调度器的最小单位,所以需要一个数值来唯一表示该线程。

pthread_ create函数第一个参数指向一个虚拟内存单元,该内存单元的地址即为新创建线程的线程ID,属于NPTL线程库的范畴。线程库的后续操作,就是根据该线程ID来操作线程的。

线程库NPTL提供了pthread_ self函数,可以获得线程自身的ID:

pthread_t pthread_self(void);
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
using namespace std;

void* ThreadRoute(void* args) {
  cout << "threadID: " << pthread_self() << endl;
}
int main()
{
  if(fork() == 0) {
    pthread_t tid; 
    int thread_id = pthread_create(&tid, nullptr, ThreadRoute, (void*)"I am thread");
    exit(11);
  }

  int status;
  pid_t id = waitpid(-1, &status, 0);
  return 0;
}

在这里插入图片描述

2.5 线程分离

线程分离的本质是让主线程不join新线程,不关心返回值,从而让新线程退出的时候自动回收

如果一个线程被设置为分离状态,他就不该被join
如果你join,结果就是未定义
即便线程被设置为分离状态,但是如果该线程依旧出错崩溃,还是会影响主线程和其他正常线程 -> 所有线程在同一个地址空间中运行

int pthread_detach(pthread_t thread);

可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离:

pthread_detach(pthread_self());

线程分离后,主线程等待不到新线程:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
void* thread_run(void* arg)
{
	pthread_detach(pthread_self());
	printf("%s\n", (char*)arg);
	return NULL;
}
int main(void)
{
	pthread_t tid;
	if (pthread_create(&tid, NULL, thread_run, "thread1 run...") != 0) {
		printf("create thread error\n");
		return 1;
	}
	int ret = 0;
	sleep(1);//很重要,要让线程先分离,再等待
	if (pthread_join(tid, NULL) == 0) {
		printf("pthread wait success\n");
		ret = 0;
	}
	else {
		printf("pthread wait failed\n");
		ret = 1;
	}
	return ret;
}

在这里插入图片描述

3.总结补充

为什么要有pthread原生线程库?

linux没有真正的线程,是用进程来模拟的

操作系统是不会直接提供类似的线程创建、退出、分离、等待相关的system call 接口,但是会提供创建轻量级进程的接口,但是用户需要有所谓的线程创建、退出、分离、等待相关的接口啊,所以为了更好的适配轻量级进程的接口,就模拟封装了一个用户层原生线程库NPTL

可是进程是PCB去管理的,用户层先要以管理线程的办法来管理轻量级进程,就得知道,线程id,状态,优先级,其他属性,从而用来进行用户线程管理!

所以tcb不用内核维护,而在用户层维护

曾经的pthead_t 是用户层的概念,是pthread库中的地址


相当于警察派了几个线人去犯罪集团当卧底,警察不懂行话,但是线人懂,所以线人充当了和犯罪分子沟通的角色,而线人反手会把情报用人话反馈给警察

警察只需要来操纵、管理线人,就能得到情报

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐