线程概念
- 线程是进程内的一个执行流。只创建PCB,不再单独创建父进程共享虚拟内存和页表,能够执行父进程代码的一部分。
- 线程在进程内部运行(在进程的地址空间中运行),拥有该进程的一部分资源。
- 线程是CPU调度的基本单位,而进程是分配系统资源的基本实体。进程用来申请资源,线程向进程要资源。
- Linux内核中没有真正意义上的线程,而是通过进程PCB来模拟,拥有属于自己的独立线程方案,称为轻量级进程。
- 对于CPU而言,每一个PCB都可以称为轻量级进程。
在调度角度而言,线程和进程有很多地方是重叠的。因此Linux的设计中并不给线程专门设计对应的数据结构,而是复用PCB。这样实现可以复用进程的结构和代码,不用再去刻意的实现进程和线程之间的关系,降低编写的难度,的维护成本大大降低,既可靠又高效。但是这样的缺点就在于,Linux并不能直接提供创建线程的系统调用接口,只能提供创建轻量级进程的接口。因此想要实现线程的操作就需要调用原生库 — pthread
线程控制
需要注意,因为Linux没有真正意义上的线程,因此关于线程的调用接口都是用户级的线程库所提供的。这个库叫: pthread ,任何的Linux操作系统都默认携带这个库,原生线程库。因此在编译有线程的程序时需要加上 -lpthread 选项
创建
创建:
一个进程里可以有多个线程,而main函数里的执行流也就是主线程,其余线程需要被创建出来
pthread_create:创建新线程
#include <pthread.h> int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
参数一为线程id,需要传地址,
参数二位线程属性,不需要则设为nullptr
参数三为函数指针,也就是这个线程被创建出来后执行的任务
参数四为参数三的参数
成功返回0
void* thread_pp(void* args){ char* s = (char*)args; while(1){ cout << s << endl; sleep(1); } } int main(){ pthread_t tid; int n = pthread_create(&tid,nullptr,thread_pp, (void*)"I am new thread"); assert(n == 0); while(1){ cout << "I am old thread" << endl; sleep(1); } return 0; }
创建多个线程
//定义线程的属性归并成类 class ThreadDate{ public: pthread_t tid; char name[64]; }; void* thread_pp(void* args){ //强转回线程对象的类型后就可以访问到线程对象的属性了 ThreadDate* td = (ThreadDate*)args; while(1){ cout <<"新线程:" << td->name << endl; sleep(1); } } int main(){ //为了方便管理和调用,将每一个创建好的线程放到数组中 vector<ThreadDate*> threads; for(int i = 0; i < 10; ++i){ //创建新线程前先创建出一个线程对象,将线程的属性自定义好之后在创建线程 ThreadDate* td = new(ThreadDate); //将线程的名字自定义 snprintf(td->name, sizeof(td->name),"%s: %d", "I am new Thread", i + 1); //创建线程,将整个对象作为参数传入 pthread_create(&td->tid,nullptr, thread_pp, (void*)td); threads.push_back(td); } while(1){ cout << "I am old Thread" << endl; sleep(1); } return 0; }
终止
线程可以被终止掉,但是不能使用exit,exit是进程的终止,如果进程被终止了那么所有的线程都没有了
#include <pthread.h> void pthread_exit(void *retval);
void* thread_pp(void* args){ //强转回线程对象的类型后就可以访问到线程对象的属性了 ThreadDate* td = ( ThreadDate*)args; int cnt = 10; while(cnt--){ cout << "新线程: " << td->name << endl; sleep(1); } pthread_exit(nullptr); }
等待
线程也是需要被等待的,也是需要回收对应的PCB,不回收则会导致和僵尸进程类似的问题—内存泄漏
- 获取线程的推出信息,可以不关心
- 回收线程对应的PCB,防止内存泄漏
利用 pthread_join 等待回收线程
#include <pthread.h> int pthread_join(pthread_t thread, void **retval);
等待成功返回0
参数一为线程的id
参数二为输出型参数,用来获取线程函数结束时返回的退出结果。不关心则设为nullptr
pthread_join不考虑异常问题,异常问题只由进程考虑
void* thread_pp(void* args){ //强转回线程对象的类型后就可以访问到线程对象的属性了 ThreadDate* td = (ThreadDate*)args; int cnt = 10; while(cnt--){ cout << "新线程: " << td->name << endl; sleep(1); } return (void*)22; } int main(){ //为了方便管理和调用,将每一个创建好的线程放到数组中 vector<ThreadDate*> threads; for(int i = 0; i < 10; ++i){ //创建新线程前先创建出一个线程对象,将线程的属性自定义好之后在创建线程 ThreadDate* td = new(ThreadDate); //将线程的名字自定义 snprintf(td->name, sizeof(td->name), "%s: %d", "I am new Thread", i + 1); //创建线程,将整个对象作为参数传入 pthread_create(&td->tid, nullptr, thread_pp, (void*)td); threads.push_back(td); } for(auto& iter : threads) { //用于接收线程退出的返回值,也就是线程函数的返回值; void *ret = nullptr; int n = pthread_join(iter->tid, (void**)&ret); assert(n == 0); //要注意Linux的指针是8个字节的,所以不能强转为int cout << "join : " << iter->name << " success, exit_code: " << (long long)ret << endl; delete iter; } while(1){ cout << "I am old Thread" << endl; sleep(1); } return 0; }
在线程的函数里设置一个返回值,等待成功后就会拿到这个返回值。
取消
线程是可以被其他线程取消的,前提是该线程已经跑起来了。
pthread_cancel:取消一个运行中的线程
#include <pthread.h> int pthread_cancel(pthread_t thread);
参数为线程的id
线程如果是被取消的,退出码为-1(宏定义:PTHREAD_CANCELED)
void* thread_pp(void* args){ //强转回线程对象的类型后就可以访问到线程对象的属性了 ThreadDate* td = (ThreadDate*)args; int cnt = 10; while(cnt--){ cout << "新线程: " << td->name << endl; sleep(1); } return (void*)22; } int main(){ //为了方便管理和调用,将每一个创建好的线程放到数组中 vector<ThreadDate*> threads; for(int i = 0; i < 10; ++i){ //创建新线程前先创建出一个线程对象,将线程的属性自定义好之后在创建线程 ThreadDate* td = new(ThreadDate); //将线程的名字自定义 snprintf(td->name, sizeof(td->name), "%s: %d", "I am new Thread", i + 1); //创建线程,将整个对象作为参数传入 pthread_create(&td->tid, nullptr, thread_pp, (void*)td); threads.push_back(td); } //五秒后取消一半线程 sleep(5); for(int i = 0; i < threads.size() / 2; i++){ //传入对应的线程id pthread_cancel(threads[i]->tid); cout << "pthread_cancel : " << threads[i]->name << " success" << endl; } for(auto& iter : threads){ //用于接收线程退出的返回值,也就是线程函数的返回值; void *ret = nullptr; int n = pthread_join(iter->tid, (void**)&ret); assert(n == 0); //要注意Linux的指针是8个字节的,所以不能强转为int cout << "join : " << iter->name << " success, exit_code: " << (long long)ret << endl; delete iter; } while(1){ cout << "I am old Thread" << endl; sleep(1); } return 0; }
分离
一个线程默认为joinable,一旦设置了分离就不能进行等待了。但是需要注意一种场景,因为主线程将新线程创建出来后并不能确定谁先运行,所以有可能在新线程设置分离前主线程就开始等待了,此时即使新线程设置了分离后退出,主线程仍然会成功等待。一旦线程设置为分离后就不需要再关心其退出问题了
pthread_detach — 分离
#include <pthread.h> int pthread_detach(pthread_t thread);
参数即为需要设置分离的线程id
获取当前线程id
pthread_self() — 获取当前线程的id
#include <pthread.h> pthread_t pthread_self(void);
线程的基本性质
线程对比进程而言,线程之间由于大部分的数据都是共享的因此通信较为方便,而进程的通信就比较麻烦。但是线程的大部分数据都是共享的也就导致了数据缺乏保护
虽然线程之间的大部分数据是共享的,但线程也是会有自己独立的数据:
- 自身的属性是私有的
- 私有的上下文结构
- 每一线程都有自己独立的栈区,也就是说每个线程的执行函数可以创建临时变量
CPU在调度的时候,是以LWP标识一个执行流。当只有一个执行流时,LWP == pid,因此这种情况下两个标识是等价的。
线程的优点:
- 创建一个新线程的代价要比创建一个新进程小得多
- 线程不需要创建新的地址空间和页表,能够执行父进程的一部分代码
- 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
- 进程的切换需要切换页表,虚拟地址空间,PCB,上下文,以及CPU中的cache需要全部更新
- 线程的切换需要切换PCB和上下文,但是CPU中的cache不需要更新,这也是最关键的一点
- 线程占用的资源要比进程少很多
- 能充分利用多处理器的可并行数量
- 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
- 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
- I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作
线程的缺点:
- 如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变
- 线程之间是缺乏保护的,健壮性降低,一个线程出异常就会影响其他的线程
- 缺乏访问控制
- 编程难度提高
线程的独立栈区是由线程库去调用底层接口创建的,和主线程的栈区是不一样的。因为用户所关心的线程属性都是在库中的,而内核提供线程执行流的调用,在Linux中:用户级线程:内核轻量级进程 = 1:1
当在一个全局变量前加上 __thread 之后,该全局变量就不再是主线程和新线程所共享的了,而是在新线程中属于线程局部存储修改这个变量,主线程读取到的也不会更改
__thread int res = 10; void* thread_pp(void* args){ while(1){ cout << "新线程: " << res << " &res: " << &res << endl; sleep(1); ++res; } } int main(){ pthread_t tid; int n = pthread_create(&tid, nullptr, thread_pp, (void*)10); while(1){ cout << "主线程:" << res << " &res: " << &res << endl; sleep(1); } pthread_join(tid, nullptr); return 0; }