一、什么是线程
1. 线程的定义

线程,有时也被称为轻量级进程(Lightweight Process, LWP),是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一个进程可以包含多个线程,这些线程共享进程的大部分资源,比如地址空间、打开的文件描述符等,但每个线程拥有自己独立的栈空间、寄存器状态和程序计数器。与进程不同,线程间的上下文切换开销相对较小,这使得多线程程序在并发执行任务时具有更高的效率。
- 一切进程至少都有一个执行线程
- Linux执行流,统一称为轻量级进程(LWP)
- 线程在进程内部运行,本质是在进程地址空间内运行
- 在linux系统中,在CPU眼中看到的PCB都要比传统的进程更轻量化
- 透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流。
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
创建线程pthread_create
pthread_create 是 POSIX 线程库(Pthreads)里用于创建新线程的函数。
函数原型
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg);
参数说明
thread:这是一个指向pthread_t类型变量的指针,其作用是存储新创建线程的线程 ID。attr:指向pthread_attr_t类型的指针,此参数用于设置线程的属性,像栈大小、调度策略等。若传入NULL,则采用默认属性。start_routine:这是一个函数指针,它指向新线程开始执行的函数。该函数需接受一个void*类型的参数,并且返回一个void*类型的值。arg:这是传递给start_routine函数的参数,其类型为void*。
返回值
- 若线程创建成功,
pthread_create会返回0。 - 若创建失败,会返回一个非零的错误码。
用法示例:
// 新线程要执行的函数
void *print_message_function(void *ptr) {char *message;message = (char *) ptr;printf("%s \n", message);return NULL;
}int main() {pthread_t thread1, thread2;char *message1 = "Thread 1";char *message2 = "Thread 2";int iret1, iret2;// 创建第一个线程iret1 = pthread_create(&thread1, NULL, print_message_function, (void*) message1);if (iret1) {fprintf(stderr,"Error - pthread_create() return code: %d\n", iret1);exit(EXIT_FAILURE);}// 创建第二个线程iret2 = pthread_create(&thread2, NULL, print_message_function, (void*) message2);if (iret2) {fprintf(stderr,"Error - pthread_create() return code: %d\n", iret2);exit(EXIT_FAILURE);}
//......后续操作
}
2. 分页式存储管理与线程的关系
(1)思考一下:如果没有虚拟内存和分页机制的情况下,每一个用户程序,在物理内存上的对应的空间就必须是连续的:
因为每个程序的代码,数据长都是不一样的,按照这样的映射方式,物理内存回被分割成各种离散的,大小不一的块。经过一段时间后,有些程序会退出,那么他们占据的物理内存空间,可以被回收,导致这些物理内存就可能以碎片的形式存在,不再连续了。
咋办呢?我们希望操作系统提供给用户的空间必须是连续的,但是物理内存最好不要连续。此时虚拟内存和分页便出现了:

分页式存储管理是一种内存管理方式,它将进程的逻辑地址空间划分为大小相等的页(Page),同时将物理内存也划分为同样大小的页框(Page Frame)。当进程被加载到内存时,操作系统会为进程的各个页分配对应的页框。线程作为进程的一部分,共享进程的分页式存储管理机制。这意味着所有线程都在同一个地址空间内运行,它们可以直接访问进程所拥有的内存区域,极大地方便了线程间的数据共享,但同时也需要注意同步问题,以避免数据竞争。

3. 线程的优点
- 提高并发性:多个线程可以并发执行不同的任务,充分利用多核 CPU 的计算资源,提高程序整体的执行效率。例如在图形处理软件中,一个线程负责处理用户界面交互,另一个线程进行图像渲染,两者同时工作,提升用户体验。
- 资源共享方便:由于线程共享进程资源,线程间的数据传递不需要像进程间那样进行复杂的通信机制(如管道、消息队列等),降低了编程复杂度。比如在一个服务器程序中,多个线程可以共享数据库连接池,提高资源利用率。
- 上下文切换开销小(线程切换):相比进程切换,线程切换时只需保存和恢复少量的寄存器状态和栈指针等,而无需切换整个地址空间,因此上下文切换速度更快,减少了系统开销。(一旦去切换上下文,处理器中的所有已经缓存的内存地址一瞬间都会作废)
- 最主要的区别是:线程的切换虚拟内存空间依然是相同的,但是进程切换是不同的。这两种上下文切换的操作都是通过操作系统内核完成的。内核的这种切换过程伴随的最显著的损耗就是将寄存器中的内容切换出来。
- 在等待慢IO操作结束的过程中,程序可执行其他的任务。
- TLB (快表,用于加速虚拟地址到物理地址的转换过程)在进程切换时需要重新更新,而在同一进程内的线程切换时一般不需要。
4. 线程的缺点
- 编程复杂度增加:多线程环境下,由于线程共享资源,容易出现数据竞争、死锁等问题。例如多个线程同时访问和修改共享变量,可能导致数据不一致。需要花费大量精力进行同步控制,如使用互斥锁、条件变量等机制,增加了编程难度。
- 调试困难:由于线程的并发执行特性,线程的执行顺序具有不确定性,这使得调试多线程程序变得异常困难。
- 线程安全问题(缺乏访问控制):若对共享资源的访问控制不当,很容易引发线程安全问题。比如在多线程操作链表时,如果没有正确的同步,可能导致链表结构损坏,程序崩溃。
——————————————————————————————————————————
面试题:
关于进程/线程切换的面试题:进程切换与线程切换的区别?
进程切换与线程切换的区别
进程切换与线程切换的一个最主要区别就在于进程切换涉及到虚拟地址空间的切换而线程切换则不会。因为每个进程都有自己的虚拟地址空间,而线程是共享所在进程的虚拟地址空间的,因此同一个进程中的线程进行线程切换时不涉及虚拟地址空间的转换。
因此我们可以形象的认为线程是处在同一个屋檐下的,这里的屋檐就是虚拟地址空间,因此线程间切换无需虚拟地址空间的切换;而进程则不同,两个不同进程位于不同的屋檐下,即进程位于不同的虚拟地址空间,因此进程切换涉及到虚拟地址空间的切换,这也是为什么进程切换要比线程切换慢的原因。
有的同学可能还是不太明白,为什么虚拟地址空间切换会比较耗时呢?
这也是面试官紧接会问的第二个问题:
什么是虚拟内存
虚拟内存是操作系统为每个进程提供的一种抽象,每个进程都有属于自己的、私有的、地址连续的虚拟内存,当然我们知道最终进程的数据及代码必然要放到物理内存上,那么必须有某种机制能记住虚拟地址空间中的某个数据被放到了哪个物理内存地址上,这就是所谓的地址空间映射,也就是虚拟内存地址与物理内存地址的映射关系,那么操作系统是如何记住这种映射关系的呢,答案就是页表,页表中记录了虚拟内存地址到物理内存地址的映射关系。有了页表就可以将虚拟地址转换为物理内存地址了,这种机制就是虚拟内存。
每个进程都有自己的虚拟地址空间,进程内的所有线程共享进程的虚拟地址空间。
——————————————————————————————————————————
5. 线程异常
- 在多线程编程中,线程异常处理较为复杂。与单线程程序不同,一个线程抛出异常时,如果没有在该线程内部进行恰当处理,异常可能不会终止整个程序,而是导致该线程异常终止。这可能会使共享资源处于不一致状态,影响其他线程的正常运行。
- 单个线程如果出现除零/野指针问题,导致线程崩溃,进程也会随之崩溃。
- 线程是进程的分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,该进程的所有线程也随之退出。
6. 线程用途
- 提高CPU密集型程序的执行效率。
- 服务器编程:服务器可以为每个客户端连接创建一个线程,独立处理客户端请求,实现并发处理大量用户请求的能力,提高服务器的吞吐量。
- 提高IO密集型程序的用户体验(如我们生活中一边写代码,一边下载开发工具,就是多线程运行的一种表现)
二、Linux 进程 VS 线程

1. 进程和线程
进程是资源分配的基本单位,拥有独立的地址空间、文件描述符表、进程控制块(PCB)等资源。进程的创建开销较大,因为它需要复制父进程的所有资源。
而线程是进程内的执行单元,共享进程的大部分资源,创建和销毁的开销较小。从调度角度看,进程和线程都可以被操作系统调度执行,但线程由于共享资源,调度时的上下文切换开销更小。
线程共享进程数据,但也有自己的一部分数据:
线程ID 一组寄存器 栈 errno 信号屏蔽字 调度优先级
2. 进程的多个线程共享
进程中的多个线程共享以下资源:
- 地址空间:所有线程都在同一个虚拟地址空间内运行,可以直接访问进程内的全局变量、堆内存等,方便数据共享。
- 打开的文件描述符:线程继承进程打开的文件描述符,这意味着一个线程打开的文件,其他线程也可以访问和操作,在进行文件读写等 I/O 操作时无需重复打开文件。
- 信号处理函数:进程内的所有线程共享相同的信号处理机制,一个线程接收到的信号,会按照进程设置的信号处理函数进行处理。
3. 基于进程线程的问题
- 资源竞争:由于线程共享资源,当多个线程同时访问和修改共享资源时,容易发生资源竞争。例如多个线程同时向共享链表中插入节点,可能导致链表结构混乱。
- 死锁:线程间如果对资源的获取顺序不当,可能会陷入死锁状态。比如线程 A 持有资源 1 并等待资源 2,而线程 B 持有资源 2 并等待资源 1,双方都不会释放自己持有的资源,导致程序无法继续执行。
- 内存一致性问题:在多线程环境下,由于缓存机制和指令重排序等因素,不同线程对共享内存的访问可能出现不一致的情况。例如一个线程修改了共享变量,但另一个线程可能无法立即看到修改后的结果,这需要使用内存屏障等技术来保证内存一致性。
~~~~~~~~~~
C++能支持多线程,本质是封装了pthread库!!!
三、Linux 线程控制
1. POSIX 线程库
POSIX 线程库(Pthreads)是一套通用的线程编程接口,在 Linux 系统中被广泛使用。要使用 Pthreads 库,需要在程序中包含头文件<pthread.h>,并且在编译时链接该库,通常使用-lpthread选项。
2. 创建线程
在 Pthreads 库中,使用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是新线程开始执行的函数指针;arg是传递给start_routine函数的参数。
3. 线程终止
线程可以通过以下几种方式终止:
- 从线程函数返回:在线程函数中使用
return语句,返回值可以被pthread_join函数获取,用于判断线程的执行结果。 - 调用
pthread_exit函数:线程可以在任何时候调用pthread_exit函数来终止自己,其函数原型为void pthread_exit(void *retval);,retval参数用于返回线程的退出状态。 - 被其他线程取消:其他线程可以调用
pthread_cancel函数来取消指定线程,被取消的线程会在执行到取消点(如某些系统调用、库函数等)时终止。
4. 线程等待
使用pthread_join函数可以等待一个线程终止,并获取其返回值。
int pthread_join(pthread_t thread, void **retval);
thread参数为要等待的线程 ID,retval参数用于存储被等待线程的返回值。如果retval为NULL,则不关心线程的返回值。
#include <pthread.h>
#include <stdio.h>void* thread_function(void* arg) {int* value = (int*)arg;int result = *value * 2;pthread_exit((void*)result);
}int main() {pthread_t new_thread;int value = 5;int result = pthread_create(&new_thread, NULL, thread_function, (void*)&value);if (result != 0) {printf("Thread creation failed.\n");return 1;}void* thread_result;
//定义一个 void* 类型的变量 thread_result,用于存储新线程的返回值pthread_join(new_thread, &thread_result);
//调用 pthread_join 函数,使主线程等待新线程执行完毕。printf("Thread returned: %d\n", (int)thread_result);return 0;
}
5. 分离线程
使用pthread_detach函数可以将线程设置为分离状态。处于分离状态的线程在终止时,系统会自动回收其资源,无需其他线程调用pthread_join等待。函数原型为:
int pthread_detach(pthread_t thread);
#include <pthread.h>
#include <stdio.h>void* thread_function(void* arg) {printf("Detached thread running.\n");pthread_exit(NULL);
}int main() {pthread_t new_thread;int result = pthread_create(&new_thread, NULL, thread_function, NULL);if (result != 0) {printf("Thread creation failed.\n");return 1;}result = pthread_detach(new_thread);//分离线程if (result != 0) {printf("Thread detachment failed.\n");return 1;}printf("Main thread continues without joining.\n");sleep(1); // 防止主线程提前退出return 0;
}
四、线程 ID 及进程地址空间布局
每个线程在创建时都会被分配一个唯一的线程 ID(TID),在 Pthreads 库中,线程 ID 的类型为pthread_t。线程 ID 主要用于线程的标识和管理,如在pthread_join、pthread_cancel等函数中需要指定线程 ID。
进程地址空间布局通常包括以下几个部分:
- 代码段:存放程序的可执行指令,该区域通常是只读的,防止程序运行过程中意外修改代码。
- 数据段:用于存放已初始化的全局变量和静态变量,其值在程序运行期间保持不变。
- BSS 段:存放未初始化的全局变量和静态变量,在程序加载时,系统会自动将该段清零。
- 堆:用于动态内存分配,由程序员通过
malloc、free等函数进行管理,堆内存从低地址向高地址增长。 - 栈:每个线程都有自己独立的栈空间,用于存放局部变量、函数参数、返回地址等。栈内存从高地址向低地址增长,当函数调用时,会在栈上分配空间,函数返回时,栈空间会被释放。线程间通过栈空间的隔离,保证了各自局部变量的独立性。
五、线程封装
Thread.hpp
#ifndef _THREAD_HPP__
#define _THREAD_HPP__#include <iostream>
#include <string>
#include <pthread.h>
#include <functional>
#include <sys/types.h>
#include <unistd.h>// v2
namespace ThreadModule
{static int number = 1;enum class TSTATUS{NEW,RUNNING,STOP};template <typename T>class Thread{using func_t = std::function<void(T)>;private:// 成员方法!static void *Routine(void *args){Thread<T> *t = static_cast<Thread<T> *>(args);t->_status = TSTATUS::RUNNING;t->_func(t->_data);return nullptr;}void EnableDetach() { _joinable = false; }public:Thread(func_t func, T data) : _func(func), _data(data), _status(TSTATUS::NEW), _joinable(true){_name = "Thread-" + std::to_string(number++);_pid = getpid();}bool Start(){if (_status != TSTATUS::RUNNING){int n = ::pthread_create(&_tid, nullptr, Routine, this); // TODOif (n != 0)return false;return true;}return false;}bool Stop(){if (_status == TSTATUS::RUNNING){int n = ::pthread_cancel(_tid);if (n != 0)return false;_status = TSTATUS::STOP;return true;}return false;}bool Join(){if (_joinable){int n = ::pthread_join(_tid, nullptr);if (n != 0)return false;_status = TSTATUS::STOP;return true;}return false;}void Detach(){EnableDetach();pthread_detach(_tid);}bool IsJoinable() { return _joinable; }std::string Name() { return _name; }~Thread(){}private:std::string _name;pthread_t _tid;pid_t _pid;bool _joinable; // 是否是分离的,默认不是func_t _func;TSTATUS _status;T _data;};
}#endif
Main.cc
#include "Thread.hpp"
#include <unordered_map>
#include <memory>#define NUM 10// using thread_ptr_t = std::shared_ptr<ThreadModule::Thread>;class threadData
{
public:int max;int start;
};void Count(threadData td)
{for (int i = td.start; i < td.max; i++){std::cout << "i == " << i << std::endl;sleep(1);}
}int main()
{threadData td;td.max = 60;td.start = 50;ThreadModule::Thread<threadData> t(Count, td);// ThreadModule::Thread<int> t(Count, 10);t.Start();t.Join();// 先描述,在组织!// std::unordered_map<std::string, thread_ptr_t> threads;// // 如果我要创建多线程呢???// for (int i = 0; i < NUM; i++)// {// thread_ptr_t t = std::make_shared<ThreadModule::Thread>([](){// while(true)// {// std::cout << "hello world" << std::endl;// sleep(1);// }// });// threads[t->Name()] = t;// }// for(auto &thread:threads)// {// thread.second->Start();// }// for(auto &thread:threads)// {// thread.second->Join();// }// ThreadModule::Thread t([](){// while(true)// {// std::cout << "hello world" << std::endl;// sleep(1);// }// });// t.Start();// std::cout << t.Name() << "is running" << std::endl;// sleep(5);// t.Stop();// std::cout << "Stop thread : " << t.Name()<< std::endl;// sleep(1);// t.Join();// std::cout << "Join thread : " << t.Name()<< std::endl;return 0;
}