在⼀个程序里的⼀个执行路线(或者叫执行流)就叫做线程 (Thread)
。更准确的定义是:线程是“⼀个进程内部的控制序列”。线程(Thread)
是操作系统能够进行运算调度的最小单位,它比进程更小,是进程内的一个执行单元,也是进程内的实际运作单位。一个线程可以执行某个程序段,或是在给定的数据集上运行一段程序。线程有时被称为轻量级进程(lightweight process)
,因为它们与同一进程中的其他线程共享资源(如内存地址空间、文件描述符等),这使得线程之间的切换和通信相比于进程来说更加高效。
如下图所示:
以下是线程的一些关键特点:
【优点】:
(1)创建⼀个新线程的代价要比创建⼀个新进程小得多; (2)与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多;
(3)线程占⽤的资源要⽐进程少很多; (4) 能充分利⽤多处理器的可并⾏数量; (5)在等待慢速I/O操作结束的同时,程序可执⾏其他的计算任务; (6) 计算密集型应⽤,为了能在多处理器系统上运⾏,将计算分解到多个线程中实现; (7) I/O密集型应⽤,为了提⾼性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。
【缺点】:
(1)性能损失
(2)健壮性降低
(3)缺乏访问控制
(4)编程难度提高
Text Segment、Data Segment
都是共享的,如果定义⼀个函数,在各线程中都可以调⽤,如果定义⼀个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境: 进程和线程的关系如下图:
也就是说之前学习的单进程是具有⼀个线程执行流的进程。
• 与线程有关的函数构成了⼀个完整的系列,绝⼤多数函数的名字都是以pthread_
打头的;
• 要使用这些函数,要通过引入头文件 <pthread.h>
;
• 链接这些线程函数库时要使⽤编译器命令的-lpthread
选项。
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *
(*start_routine)(void*), void *arg);
【功能】:
【参数】:
thread
:返回线程IDattr:
设置线程的属性,attr为NULL表⽰使⽤默认属性start_routine
:是个函数地址,线程启动后要执⾏的函数arg
:传给线程启动函数的参数【返回值】:
使用PS命令查看线程信息:
ps -aL | head -1 && ps -aL | grep myprocess
创建线程后我们就可以通过该命令在命令行查看线程相关信息:
PID LWP TTY TIME CMD
2711838 2711838 pts/235 00:00:00 mythread
2711838 2711839 pts/235 00:00:00 mythread
-L 选项:打印线程信息
PID是进程ID,LWP指线程ID。有⼀个线程ID和进程ID相同,这个线程就是主线程,主线程的栈在虚拟地址空间的栈上,而其他线程的栈在是在共享区(堆栈之间),因为pthread系列函数都是pthread库提供给我们的,而pthread库是在共享区的,所以除了主线程之外的其他线程的栈都在共享区。
如果需要只终止某个线程而不终止整个进程,可以有三种方法:
return
。这种方法对主线程不适用,从main函数return相当于调用exit,直接终止进程。pthread_ exit
终止自己。void pthread_exit(void *value_ptr);
value_ptr:value_ptr不要指向⼀个局部变量。也就是说指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了。
无返回值,跟进程⼀样,线程结束的时候无法返回到它的调用者(自身)
pthread_ cancel
终止同⼀进程中的另⼀个线程。int pthread_cancel(pthread_t thread);
thread:线程ID
成功返回0;失败返回错误码
因为已经退出的线程,其空间没有被释放,仍然在进程的地址空间内。如果不进行回收,新的线程不会复用刚才退出线程的地址空间,就会造成资源的浪费。
int pthread_join(pthread_t thread, void **value_ptr);
成功返回0;失败返回错误码
调用该函数的线程将挂起等待,直到id为thread的线程终⽌。thread
线程以不同的方法终止,通过pthread_join
得到的终⽌状态是不同的,总结如下:
return
返回,value_ ptr
所指向的单元里存放的是thread线程函数的返回值。pthread_ cancel
异常终掉,value_ ptr
所指向的单元⾥存放的是常数PTHREAD_ CANCELED
。thread
线程是⾃⼰调⽤pthread_exit
终⽌的,value_ptr
所指向的单元存放的是传给
pthread_exit
的参数。thread
线程的终止状态不感兴趣,可以传NULL
给value_ ptr
参数。默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。如果不关系线程的返回值,join是⼀种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源,函数如下:
int pthread_detach(pthread_t thread);
可以是线程组内其他线程对目标线程进⾏分离,也可以是线程自己分离:
pthread_detach(pthread_self());
joinable和分离是冲突的,⼀个线程不能既是joinable又是分离的
pthread_ create
函数会产⽣⼀个线程ID
,存放在第⼀个参数指向的地址中。线程库NPTL
提供了pthread_ self
函数,可以获得线程自身的ID:
pthread_t pthread_self(void);
通过对线程创建、终止、等待、分离等函数的学习,我们就可以利用这些接口封装一个便于理解和使用的线程库。 线程库类成员变量包括线程ID、进程ID、线程名字、线程当前所处状态、线程是否可以分离以及线程绑定的函数这六个。
#pragma once
#include <iostream>
#include<unistd.h>
#include<string>
#include<functional>
#include <pthread.h>
namespace ThreadModule
{
enum class TSTATUS
{
NEW,
RUNNING,
STOP
};
static int number = 1;//静态全局变量
class Thread
{
private:
using func_t = std::function<void()>;
static void* run(void* args)//static不用带this指针,所以可以传入一个void*类型
{
Thread *t = static_cast<Thread *>(args);//类型转换
t->_status = TSTATUS::RUNNING;
t->_func();//不直接pthread_create调用,而封装一层为了统一接口,方便调用
return nullptr;
}
public:
Thread(func_t func)
:_joinable(true),
_tid(-1),
_status(TSTATUS::NEW),
_func(func)
{
_name = "Thread-" + std::to_string(number++);
_pid = getpid();
}
bool Start()
{
if(_status == TSTATUS::RUNNING)
{
std::cout<<"线程已经Start..."<<std::endl;
return true;
}
int n = pthread_create(&_tid,NULL,run,this);
if(n != 0)
{
std::perror("线程Start失败...");
return false;
}
std::cout<<"线程Start成功..."<<std::endl;
return true;
}
bool Stop()
{
if(_status != TSTATUS::RUNNING)
{
std::cout<<"请开启线程..."<<std::endl;
return false;
}
int n = pthread_cancel(_tid);
if(n!= 0)
{
std::perror("线程Stop失败...");
return false;
}
_status = TSTATUS::STOP;
std::cout<<"线程Stop成功..."<<std::endl;
return true;
}
bool Join()
{
if(_joinable == false)
{
std::cout<<"请开启线程等待权限..."<<std::endl;
return false;
}
int n = pthread_join(_tid, NULL);
if(n!= 0)
{
std::perror("线程Join失败...");
return false;
}
_status = TSTATUS::STOP;
std::cout<<"线程Join成功..."<<std::endl;
return true;
}
void Detach()
{
if(_status != TSTATUS::RUNNING)
{
std::cout<<"请开启线程..."<<std::endl;
return;
}
if(_joinable == true)
{
int n = pthread_detach(_tid);
if(n!= 0)
{
std::perror("线程Detach失败...");
return;
}
_joinable = false;
std::cout<<"线程Detach成功..."<<std::endl;
}
else
std::cout<<"线程已经Detach..."<<std::endl;
return;
}
bool IsJoinable() { return _joinable; }
std::string Name() { return _name; }
~Thread(){}
private:
pthread_t _tid;
pid_t _pid;
bool _joinable;//线程是否可以等待,默认可以等待也就是不可分离
std::string _name;
TSTATUS _status;
func_t _func;//线程执行的函数
};
}
然后我们就可以使用封装好的线程库:
#include "thread.hpp"//封装好的线程库头文件
int main()
{
ThreadModule::Thread t([](){
while(true)
{
std::cout << "hello world" << std::endl;
sleep(1);
}
});
t.Start();
sleep(5);
t.Stop();//线程终止
sleep(1);
t.Join();//线程等待
return 0;
}
结果如下:
当然对于线程库我们也可以再加一个成员变量,并使用模板,所以该成员变量可以是任意类型的数据给线程绑定的函数使用。注意编译的时候要带上
-lpthread
选项。
线程就是一个执行流,一个进程内可以有多个线程。进程是划分资源的最小单位,而线程(Thread)
是操作系统能够进行运算调度的最小单位。通过对线程的各种函数的理解与认识,我们就可以封装便于理解和学习的线程库。以上就是今天的所有内容啦~ 完结撒花 ~ 🥳🎉🎉