线程概念
(1)在一个程序里的一个执行路线就叫做线程(thread)。更准确的定义是:线程是"一个进程内部的控制序 列"
(2)一切进程至少都有一个执行线程
(3)线程在进程内部运行,本质是在进程地址空间内运行
(4)在Linux系统中,在CPU眼中,看到的PCB都要比传统的进程更加轻量化
(5)透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程 执行流
线程的实现方式
Linux 内核并不直接区分"线程"和"进程",而是通过 POSIX 线程库 在用户态实现了线程的标准接口。
线程创建
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
参数:
thread:指向线程标识符的指针。
attr:线程属性,通常设为 NULL使用默认值。
start_routine:线程运行的函数指针。
arg:传递给线程函数的参数。
等待线程结束
int pthread_join(pthread_t thread, void **retval);
需要线程等待的原因:(1)已经退出的线程,其空间没有被释放,仍然在进程的地址空间内。 (2)创建新的线程不会复用刚才退出线程的地址空间。
调用该函数的线程将挂起等待,直到id为thread的线程终止。thread线程以不同的方法终止,通过pthread_join得到的 终止状态是不同的,总结如下:
-
如果thread线程通过return返回,value_ ptr所指向的单元里存放的是thread线程函数的返回值。
-
如果thread线程被别的线程调用pthread_ cancel异常终掉,value_ ptr所指向的单元里存放的是常数
PTHREAD_ CANCELED。
- 如果thread线程是自己调用pthread_exit终止的,value_ptr所指向的单元存放的是传给pthread_exit的参
数。
- 如果对thread线程的终止状态不感兴趣,可以传NULL给value_ ptr参数。
线程终止
(1)主动退出:在线程函数中调用 pthread_exit(void *retval)。
(2)被动取消:其他线程调用 pthread_cancel(pthread_t thread)。
分离线程
概念:默认情况下,线程是可连接 的,这意味着:主线程必须调用 pthread_join等待它结束,以获取其返回值并回收其资源。如果不 join,线程结束后,其资源(如栈空间)不会被释放,会造成"资源泄漏"。分离线程 则是:线程结束时,操作系统会自动回收其资源。主线程无法、也无需调用 pthread_join等待它。
设置分离线程方法
(1)创建时指定属性
#include <pthread.h>
void* thread_func(void* arg) {
printf("我是分离线程,跑完就自动结束\n");
return NULL;
}
int main() {
pthread_t tid;
pthread_attr_t attr;
// 1. 初始化线程属性
pthread_attr_init(&attr);
// 2. 设置为分离状态
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
// 3. 用这个属性创建线程
pthread_create(&tid, &attr, thread_func, NULL);
// 销毁属性对象
pthread_attr_destroy(&attr);
// 注意:这里不能调用 pthread_join
sleep(1); // 主线程等待一下,确保分离线程执行完
return 0;
}
(2)创建后分离
先创建线程,然后在主线程 或线程自身 中调用 pthread_detach将其分离。
#include <pthread.h>
void* thread_func(void* arg) {
pthread_detach(pthread_self()); // 将自己分离
printf("我把自己分离了,跑完自动回收\n");
return NULL;
}
int main() {
pthread_t tid;
pthread_create(&tid, NULL, thread_func, NULL);
// 或者主线程分离它:
// pthread_detach(tid);
// 不能调用 pthread_join
sleep(1);
return 0;
}
线程ID及进程地址空间布局
Linux 中有两种常见的线程 ID 概念,分别对应不同的管理层次:
用户态线程
这是程序员在代码中直接操作的类型,由 POSIX 线程库 (pthread) 管理。
获取方式:使用 pthread_self()函数获取当前线程的 ID,或通过 pthread_create()的参数获取新线程的 ID。
本质:pthread_ create函数第一个参数指向一个虚拟内存单元,该内存单元的地址即为新创建线程的线程ID, 属于NPTL线程库的范畴。线程库的后续操作,就是根据该线程ID来操作线程的。 线程库NPTL提供了pthread_ self函数,可以获得线程自身的ID。
特点:只在当前进程内唯一,不同进程中的线程可能拥有相同的 pthread_t值。
内核态线程
这是操作系统内核调度线程时使用的真实 ID,也称为轻量级进程 ID (LWP ID)。
获取方式:使用系统调用 syscall(SYS_gettid)或非标准库函数 gettid()。
特点:在全系统范围内唯一。主线程的 TID 通常等于进程的 PID,而其他子线程的 TID 则是独立的、递增的整数。
理解资源分配整个线程
当操作系统创建一个进程时,它会一次性分配好内存、文件、信号等资源。当这个进程创建线程时,并不会再分配新的资源,而是让新线程直接使用进程已有的资源。所有线程共享进程的代码段、数据段、堆空间、文件描述符、信号处理方式。这就像公司里的员工共用同一套办公系统。每个线程只拥有自己独立的栈空间、寄存器状态和程序计数器。这就像每个员工有自己的工位(栈)和当前正在处理的任务(寄存器)。
本质:分配地址空间范围
主线程与新线程
主线程
主线程是程序的起点和默认执行流。
生命周期:主线程的结束通常意味着整个进程的结束。如果主线程先于其他线程结束,整个进程会被终止,导致其他线程也被强制结束。
新线程
新线程是由主线程(或其他线程)主动创建的额外执行流。
生命周期:独立于主线程运行,但通常需要与主线程同步(例如,主线程等待新线程完成工作)。
区别
|------|---------------|-------------------------|
| 特性 | 主线程 | 新线程 |
| 创建方式 | 进程启动时自动创建 | 由 pthread_create() 显式创建 |
| 入口函数 | main() | 用户自定义的函数 |
| 资源 | 拥有进程的初始资源 | 共享进程的资源(内存、文件描述符等) |
| 控制权 | 控制程序流程,管理其他线程 | 专注于执行特定任务 |
线程的优缺点
优点
(1)创建一个新线程的代价要比创建一个新进程小得多
(2)与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
(3)线程占用的资源要比进程少很多
(4)能充分利用多处理器的可并行数量
(5)在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
(6)计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
(7)I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作
缺点
(1)性能损失:一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。
(2)健壮性降低:编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。
(3)缺乏访问控制:进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。
(4)编程难度提高:编写与调试一个多线程程序比单线程程序困难得多
线程用途
(1)合理的使用多线程,能提高CPU密集型程序的执行效率
(2)合理的使用多线程,能提高IO密集型程序的用户体验
进程VS线程
进程是资源分配的基本单位,线程是调度的基本单位。
与进程的区别:
资源拥有方式:进程拥有独立的内存空间。一个进程崩溃,不会影响其他进程(因为内存是隔离的)。而线程共享进程的内存空间。一个线程崩溃,整个进程都会崩溃(因为大家共用一套内存)。
创建与销毁的开销:进程创建和销毁需要分配或回收独立的内存空间、文件描述符等,开销非常大。线程则是创建和销毁只需要分配一个栈和寄存器,开销极小。
通信方式:进程间通信 (IPC)因为内存不共享,必须通过复杂的机制,如管道、消息队列、共享内存等。线程间通信因为共享内存,可以直接读写全局变量。