【Liunx专栏_6】Linux线程概念与控制
目录
- 1、线程是什么?通过一个图来理解……
- 2、Linux进程和线程?
- 2.1、之间的关系和区别
- 2.2、线程的优缺点?
- 3、线程的创建
- 3.1、POSIX线程库
- 3.2、创建线程
- 3.3、PS查看运行的线程
- 4、线程的终止
- 5、线程的等待
- 6、线程分离
- 7、线程封装
1、线程是什么?通过一个图来理解……
首先,我们知道进程等于PCB+自己的数据和代码,在创建进程的时候,操作系统要对进程进行描述,则创建一个结构体,里面包含进程的所有属性信息(标志符、状态信息、优先级等……),该结构体在Linux中称为“PCB”,即进程控制块,在Litnux中结构体名叫task_strcut
。该信息会存储在程序地址空间中,也就是所说的虚拟地址空间。里面就存储了该进程的所有信息和自己的代码和数据信息。它们通过页表进行映射到物理内存空间,也就是加载内存,进行CPU的调度执行。
上面描述的整个流程,从进程的创建(PCB的创建)、程序的代码和数据通过页表进行的映射关系的建立、到CPU的调度执行,该流程就可以看做是一个执行路线,只不过只有一个执行流,我么把该执行线路就称之为线程
。
即线程就是一个进程中的一天控制序列。并且一个进程中至少有一个执行线程。
之前描述的PCB
只有一个,此处就称为该进程有一个执行线程,但在一个进程中可以存在多个执行流,所有执行流会公用一个虚拟内存空间,在虚拟内存中,操作系统会将资源合理分配给多个执行流。
2、Linux进程和线程?
2.1、之间的关系和区别
- 进程:是系统资源分配的基本单位。
- 线程:是CPU调度执行的基本单位。
- 线程共享进程数据的同时,线程也有自己的一部分数据信息,用来描述不同线程。(线程ID、调度优先级、信号屏蔽字等……)
- 进程中的多线程共享:即在一个进程中,有一个程序地址空间,该空间被所有线程共享,因此Text Segment、Data Segment都是共享的,因此只要定义一个函数后,所有线程都是可以调用的,同时定义了一个全局变量后也是共享的,此外多线程之间还共享了文件描述符表、当前工作目录、用户id和组id等……
- 线程是进程的执行分支,只要一个线程出现异常情况,也就影响到整个进程,从而导致整个进程的崩溃,进而终止退出。
2.2、线程的优缺点?
- 线程的优点:
从创建的角度看:创建一个新线程比创建一个新进程的代价小的多,因为从上面的关系和区别可以看出,创建一个线程不需要从新分配虚拟地空间,没有数据的大量拷贝,而创建进程需要创建新的程序地址空间,同时还要拷贝原始数据,增加的系统的消耗。
从切换的角度看:与进程间切换相比,线程间切换需要操作的工作量更小,主要区别就是线程之间的切换,虚拟地址空间是相同的。
从执行效率上看:在多线程的情况下可以实现并发执行,提高执行的效率。
- 线程的缺点:
主要是性能上,一个处理器上有密集型线程的数量进行执行的时候,会有较大的性能损失,会增加额外的同步和调度开销。但是,合理的使⽤多线程,能提⾼CPU密集型程序的执⾏效率。
健壮性上:在多线程的情况下,由于虚拟地址空间是共享的,会造成一些空间的数据在同一时刻被多个线程访问,即缺少资源的保护,因此在后面会提到互斥与信号量,用来应对多线程的情况下共享资源的多次访问。
3、线程的创建
3.1、POSIX线程库
- 在Linux中,创建线程就会用到里面的库,需要包含头文件<pthread.h>。
- 同时在链接这些线程函数库的时候,编译时需要加
-lpthread
选项。
3.2、创建线程
- 函数接口是:
pthread_create()
- 在代码中的使用:
#include<iostream>
#include<pthread.h>
#include<string.h>
#include<unistd.h>void *work(void *arg)
{while(1){std::cout<<"我是线程-1: "<<pthread_self()<<"……"<<std::endl;sleep(1);}
}int main()
{pthread_t tid;int ret=pthread_create(&tid,nullptr,work,nullptr);if(ret!=0){std::cout<<"create failed!,code_num:"<<strerror(ret)<<std::endl;exit(-1);}while(1){std::cout<<"我是主线程: "<<pthread_self()<<"……"<<std::endl;sleep(1);}return 0;
}
- 上面的代码就是在主线程中创建一个新的线程,总共两个线程,因此运行后有两个执行流,整个程序称为一个进程,每个线程有自己的唯一表示符号,上面使⽤是是
pthread_self()
,得到的这个数实际上是⼀个地址,在虚拟地址空间上的⼀个地址,通过这个地址, 可以找到关于这个线程的基本信息,包括线程ID,线程栈,寄存器等属性。即线程id。我们通过打印看到确实有两个同时输出在显示器上,下面通过ps
查看是不是真有两个线程在运行。
3.3、PS查看运行的线程
while :; do ps -aL | head -1 && ps -aL | grep 可执行程序名 | grep -v grep ; echo "************" ; sleep 1 ; done
循环监控查看线程情况。
通过布局监控,运行程序可以看到,确实有两个线程,它们的PID都是一样的,说明这两个线程拥有同一个父进程,看到LWP
,其中一个和PID
相同,说明该线程就是主线程
,另一个就是创建的新线程。LWP
是什么呢?LWP
得到的是真正的线程ID
。
注意:
主线程的栈在虚拟地址空间的栈上,⽽其他线程的栈在是在共享区(堆栈之间),因为pthread
系列函数都是pthread
库提供给我们的。⽽pthread
库是在共享区的。所以除了主线程之外的其他线程的栈都在共享区。
4、线程的终止
只终止其中的某个线程,而不是终止整个进程,有一下三种方法:
- 在创建的线程函数中调用
return
,注意不要在主线程中调用return
,在主线程中调用retrun
,就相当于调用exit
。
void *work(void *arg)
{int num=5;while(num--){std::cout<<"我是线程-1: "<<pthread_self()<<"……"<<std::endl;sleep(1);}return nullptr;
}
- 直接调用退出接口
pthread_exit()
终止线程。
value_ptr
:value_ptr不要指向⼀个局部变量
- 调用
pthread_cancel()
取消一个在执行的线程
参数就是传入线程id。成功返回0。
5、线程的等待
-
为什么要等待?
因为退出的线程,其空间没有被释放,依旧在进程地址空间中的。若创建新的线程并不会使用刚退出的线程的地址空间,因此就造成了浪费。 -
等待的函数接口:
pthread_join()
thread
:线程ID
value_ptr
:它指向⼀个指针,后者指向线程的返回值,返回值会根据调用不同的终止接口返回不同的值。
- 通过代码演示:创建三个线程,调用不同的终止接口,看等待的返回接收参数值是什么……
#include <iostream>
#include <pthread.h>
#include <string.h>
#include <unistd.h>void *thread1(void *arg)
{std::cout << "thread1 running……" << std::endl;int *p = (int *)malloc(sizeof(int));*p = 1;return (void *)p;
}void *thread2(void *arg)
{std::cout << "thread2 running……" << std::endl;int *p = (int *)malloc(sizeof(int));*p = 2;pthread_exit((void *)p);
}void *thread3(void *arg)
{while (1){std::cout << "thread3 running……" << std::endl;sleep(1);}return nullptr;
}int main()
{pthread_t tid;void *ret;if (pthread_create(&tid, nullptr, thread1, nullptr) != 0){std::cout << "创建失败……" << std::endl;exit(-1);}pthread_join(tid, &ret);printf("thread1 退出,thread1 id=%X,return code:%d\n", tid, *(int *)ret);free(ret);if (pthread_create(&tid, nullptr, thread2, nullptr) != 0){std::cout << "创建失败……" << std::endl;exit(-1);}pthread_join(tid, &ret);printf("thread2 退出,thread2 id=%X,return code:%d\n", tid, *(int *)ret);free(ret);if (pthread_create(&tid, nullptr, thread3, nullptr) != 0){std::cout << "创建失败……" << std::endl;exit(-1);}sleep(3);pthread_cancel(tid);pthread_join(tid, &ret);if (ret == PTHREAD_CANCELED)printf("thread return, thread id=%X, return code:PTHREAD_CANCELED\n",tid);elseprintf("thread return, thread id=%X, return code:NULL\n", tid);while(1){std::cout<<"我是主线程……"<<std::endl;sleep(1);}return 0;
}
- 如果thread线程通过return返回,value_ ptr所指向的单元⾥存放的是thread线程函数的返回值。
- 如果thread线程被别的线程调⽤pthread_ cancel异常终掉,value_ ptr所指向的单元⾥存放的是常数PTHREAD_ CANCELED。
- 如果thread线程是⾃⼰调⽤pthread_exit终⽌的,value_ptr所指向的单元存放的是传给pthread_exit的参数。
- 如果对thread线程的终⽌状态不感兴趣,可以传NULL给value_ ptr参数。
6、线程分离
上面看到线程的等待,线程退出后需要对线程进行等待,保证资源的释放,避免系统资源的泄露。可以看到通过等待线程进行资源的释放不是太方便,因此线程创建在默认情况下是joinable的,也就是可以线程分离的,指将一个线程从其创建者(通常是主线程)中分离出来,使其成为一个独立的执行实体。分离后的线程在终止时会自动释放其资源,而不需要其他线程显式地等待或回收它。
主要特点
1、资源自动回收:分离的线程在结束时系统会自动回收其资源
2、无需join:其他线程不需要调用join()或类似函数来等待分离线程结束
3、独立性:分离后的线程运行独立于创建它的线程
线程分离的接口函数:pthread_detach(线程ID)
下面代码描述:即通过线程分离技术,让线程自己执行5次后自动分离,自动释放资源,分离后依然显示调用join等到,依旧会等待的,只不过不会执行后面的代码,因为是阻塞式等待的。
#include <iostream>
#include <pthread.h>
#include <string.h>
#include <unistd.h>void* thread1(void* arg)
{int num=5;while(num--){std::cout<<(char*)arg<<std::endl;sleep(1);}pthread_detach(pthread_self());return nullptr;
}int main()
{pthread_t tid;if(pthread_create(&tid,nullptr,thread1,(void*)"thread1线程running……")!=0){std::cout<<"创建线程失败……"<<std::endl;}sleep(1);if(pthread_join(tid,nullptr)==0){std::cout<<"等待成功……"<<std::endl;return 0;//不return 就会继续执行后面的主线程}else{std::cout<<"等待失败……"<<std::endl;return -1;}while(1){std::cout<<"主线程运行中……"<<std::endl;sleep(1);}return 0;
}
使用场景
1、后台任务(如日志记录、监控)
2、不需要与主线程同步的一次性任务
3、长时间运行的服务线程
注意事项
1、分离后无法再join该线程
2、分离线程不能返回结果给创建者线程
3、主线程退出可能导致分离线程被强制终止(取决于平台和设置)
4、需要谨慎处理共享资源,因为缺乏同步机制
7、线程封装
下面对创建线程步骤做一次封装:
#pragma once#include <iostream>
#include <cstring>
#include <functional>
#include <pthread.h>namespace ThreadMoodule
{using work_t = std::function<void(std::string)>;static int NameId = 1;// 枚举线程状态enum STATUS{NEW,RUNNING,STOP};// 封装线程class Thread{private:// 此处需要注意,写为成员函数的时候,函数的第一个参数默认是this*,若下面线程函数就需要用static修改,或者写在类外,由于封装,因此加staticstatic void *Routine(void *args){Thread *td = static_cast<Thread *>(args);td->_task(td->_name);return 0;}void EnableDetach(){_joinable=false;}public:Thread(work_t task): _task(task), _status(STATUS::NEW), _joinable(true){_name = "thread_" + std::to_string(NameId++);}bool StartThread(){// 启动线程if (_status != STATUS::RUNNING){// int n = pthread_create(&_tid, nullptr, Routine, nullptr);//第四个参数由于线程函数写在类中,// 被static修饰无法使用this*指针,因此无法访问成员变量,因此该参数传入this,传递给函数。int n = pthread_create(&_tid, nullptr, Routine, this);if (n != 0){return false; // 创建失败}_status = STATUS::RUNNING;return true;}return false;}bool StopThread() // 即取消{if (_status == STATUS::RUNNING){int n = pthread_cancel(_tid);if (n != 0){return false;}_status = STATUS::STOP;return true;}return false;}bool JoinThread(){if (_joinable){int n = pthread_join(_tid, nullptr);if (n != 0){return false;}_status = STATUS::STOP;return true;}return false;}void DetachThread(){//前提是没有分离的EnableDetach();pthread_detach(_tid);}std::string Name(){return _name;}bool IsJoinAble(){return _joinable;}~Thread(){}private:std::string _name;pthread_t _tid;pid_t _pid; // 多线程,所有pid都是一样的bool _joinable; // 默认是不可分离的work_t _task;STATUS _status;};
}
#include "Thread.hpp"
#include <unistd.h>
#include <vector>#define THREAD_NUM 5int main()
{std::vector<ThreadMoodule::Thread> threads;for (int i = 0; i < THREAD_NUM; i++){ThreadMoodule::Thread t([](std::string name){while(true){std::cout<<name<<"执行任务……"<<std::endl;sleep(1);} });threads.emplace_back(t);}for(auto& n:threads){n.StartThread();}sleep(1);for(auto& e:threads){e.StopThread();}sleep(1);for(auto& e:threads){e.JoinThread();}return 0;
}