目录
一,线程的概念
学习线程前,首先必须要明白进程的结构。我们先回顾最开始我们接触进程的时候,当创建一个进程时,需要创建PCB(task_struct),地址空间,页表等数据结构以及建立对应的关系和数据,这些流程下来使得时间和空间上的成本非常高,也就是说,进程创建,成本是非常高的。
系统中创建多进程无非就是为了让多执行流并发的执行相同或不同的代码,一次创建多个进程成本太高,这里可只创建PCB(task_struct)结构,它们都对应一个地址空间(mm_struct)和页表,在地址空间中,让代码区划分成多份,每一份对应一个PCB结构,而每个PCB结构执行其对应的代码块或函数,这些代码块或函数定义了每个PCB在运行时应该执行的具体任务。如此一来,正文代码的执行就从串行执行变为了并发式地执行,而这些PCB结构,就是所谓的线程。
线程在进程内部运行,本质是在进程地址空间内运行,而地址空间中存在着许多虚拟地址,每一份资源可能由虚拟地址标识,线程之间共享进程地址空间的资源,即共享同一个进程的所有资源,而一个程序里的一个执行路线就叫做线程。更准确的定义是:线程是一个进程内部的执行分支,是CUP调度的基本单位(CUP不管结构划分,只拿到task_struct结构放到内部种执行)。不难理解,线程的切换开销比进程小得多。
线程的这种 task_struct 结构简称为TCB,进程叫做PCB。不同的是TCB中保存的线程状态信息通常比PCB中保存的少,TCB的主要作用是为系统提供管理和调度线程所需的信息。透过进程虚拟地址空间,可以看到进程的大部分资源(进程地址空间和地址空间上的虚拟地址本质上是一种资源),将进程资源合理分配给每个执行流,就形成了线程执行流,CPU调度时并不区分进程和线程,都将其看做执行流。
一个进程可以包含多个线程,但一个线程只能属于一个进程。也就是说,进程与线程之间存在包含关系,一切进程至少都有一个执行线程,没有线程的进程可以看做是单线程的,线程是进程的一部分,所以在Linux系统的CPU眼中,看到的PCB都要比传统的进程更加轻量化,线程也被称为轻量级进程。
以前学习的进程内部都只有一个线程,即一个task_struct。线程的知识让我们发现,一个进程内部至少有一个线程。
二,Linux系统创建线程
pthread_create
是线程库中用于创建一个新线程的函数。该函数成功调用完后,主线程(即调用 pthread_create
的线程)将继续执行其后续代码,不会等待新线程完成。新创建的线程会执行start_routine
函数中的程序。可看出,pthread_create
函数在程序中可并行地执行多个任务,从而提高程序的执行效率。下面是对 pthread_create
函数的详细说明:
头文件:
#include <pthread.h>
格式:
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void* (*start_routine) (void *), void *arg);参数说明:
thread
:指向pthread_t
类型的指针,用于存储新创建的线程的标识符,其中,pthread_t
是一个不透明的数据类型,表示线程标识符。在调用pthread_create
之前,这个指针不需要初始化。函数成功执行后,它将包含新线程的标识符。
attr
:指向pthread_attr_t
类型的指针,用于指定线程的属性。如果传递NULL
,则使用默认属性(具体的属性信息这里先暂不考虑,下面的运用使用默认属性即可)。
start_routine
:新线程将要执行的函数的地址。其中,这个函数的参数是void*
类型,是arg
的值,用于向新线程传递数据。
arg
:传递给start_routine
函数的参数。该函数允许传递任何类型的数据,但在start_routine
函数内部,你需要将这个void*
类型的参数转换为适当的类型。返回值:
成功时返回0;失败时返回错误码。常见的错误码包括
EAGAIN
(系统资源不足,无法创建更多线程)、EINVAL
(无效的线程属性)等。
注意:pthread_create
函数不属于系统调用(不是Linux经过内核封装实现的)。该函数的实现来自pthread
库,是一个由操作系统所提供的原生线程库,不属于C/C++,也不属于操作系统内核,它是在用户空间层封装实现提供的(大多数库都是在用户层空间实现的),默认情况下系统不会链接此库,因此,当代码中存在该函数进行指令编译时,必须使用选项-lpthread,此选项表示链接pthread库,否则将会编译出错。
代码示例:
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>
using namespace std;
//新建立线程所执行的函数
void *newthreadrun(void *args)
{
while (true)
{
cout << "I am new thread, pid: " << getpid() << endl;
sleep(1);
}
}
//主线程程序入口
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, newthreadrun, nullptr);
while (true)
{
cout << "I am main thread, pid: " << getpid() << endl;
sleep(1);
}
}
从上面可看出,pthread_create
创建新的线程后将与主线程并发执行。主线程继续往下执行代码,而新线程执行 newthreadrun 函数中的代码。
上面的运行还发现一个问题,主线程的PID与新建线程的PID一样。再次强调下,一个进程包含一个或多个线程,因此进程中线程的PID都是一样的。为了区分不同线程,系统中使用LWP来表示。LWP是内核级别的线程,即轻量级进程,由操作系统内核直接管理和调度。用户态的线程库创建的线程会映射到内核级的LWP上进行调度,即操作系统使用LWP的ID进行调度,而不是PID。
注意,LWP表示线程本身,而不是线程的标识符。线程的标识符(TID)通常与LWP的ID相同。这意味着,当你查看一个线程的TID时,实际上看到的是该线程对应的LWP的ID,其中,于单进程而言,每个进程内部都只有一个执行流,此时的LWP等于PID;于多进程而言,它内部存在多个线程,主线程LWP等于PID。