1.什么是线程
- 在Linux操作系统中,线程是进程内部的一个执行流。
- 在Linux操作系统下,执行流统称为轻量级进程(也是task_struct作为数据结构,简称LWP)(也就是说在Linux下线程是通过LWP模拟实现的)。
2.线程vs进程
- 进程是承当资源分配的基本实体,线程是os调度的基本单位。
- 线程共享进程的地址空间(即共享数据)。
(1)文件描述符 (2)信号处理方式 (3)当前工作目录 (4)用户id和组id- 线程也有自己独立的一些数据
(1)栈 (2)寄存器 (3)线程ID (4)调度优先级 (5)errno (6)信号屏蔽字
3.线程调度
- 如果线程所属的进程不被调度,那么该进程内的所有线程(包括主线程和其他任何线程)通常也不会被调度执行。
- 线程调度也是受到优先级、调度策略等影响。
- 线程调度的时机
时间片用完:操作系统为每个线程分配一个时间片,当线程的时间片用完时,操作系统会将调度权交给其他线程。
阻塞操作:当线程执行了一个阻塞操作,例如等待输入、等待磁盘读写等,操作系统会将线程切换到阻塞状态,同时调度其他可运行线程。
优先级调度:线程的优先级可能会影响调度。当高优先级的线程就绪时,操作系统可能会将其切换到运行状态,同时调度低优先级的线程。
睡眠和唤醒:线程可以通过调用sleep()或wait()等方法主动让出CPU,进入睡眠状态。当休眠时间到期或条件满足时,线程会被唤醒并重新调度。
中断处理:当线程遇到硬件中断时,操作系统可能会中断当前线程的执行,切换到中断处理程序的上下文。
4.线程控制
4.1 POSIX线程库
- 与线程有关的函数构成了⼀个完整的系列,绝⼤多数函数的名字都是以“pthread_”打头的。
- 要使⽤这些函数库,要通过引⼊头⽂ <pthread.h>。
- 链接这些线程函数库时要使⽤编译器命令的“-lpthread”选项。
4.2创建线程
函数原型
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void*), void *arg);
参数
- thread:指向 pthread_t 类型的指针。
用途:用于存储新创建的线程的标识符(ID)。
调用成功后,这个参数指向的位置会被赋予新线程的ID。
注意:在使用这个线程ID之前,不需要对其进行任何初始化。- attr:指向 pthread_attr_t 类型的指针,该类型是一个结构体,用于指定线程的属性。
用途:允许调用者设置新线程的属性,如是否分离(detach)、堆栈大小、调度策略和优先级等。
默认值:如果设置为 NULL,则使用默认属性创建线程。- vstart_routine:函数指针,指向一个返回 void类型并接受一个 void 类型参数的函数。
用途:这是新线程将要执行的函数的地址。该函数称为线程的启动例程(start
routine)或线程函数。 参数:该函数接受一个 void* 类型的参数(arg),允许调用者向线程函数传递任意数据。
返回值:线程函数的返回值将被传递给 pthread_exit 函数(如果线程调用它)或作为线程的退出状态(如果线程直接返回)。- arg :void* 类型的指针。
用途:传递给线程函数的参数。它允许调用者向线程函数传递任意数据。
注意:在实际使用中,通常需要将这个参数转换为适当的类型,以便在线程函数内部使用。
返回值
- 成功:返回 0。
- 失败:返回一个非零的错误码,这些错误码通常定义在 <pthread.h> 头文件中,并且可以通过 strerror 或 perror 函数转换为可读的错误信息。
功能
用于创建新线程的函数。
例子
创建一个线程并执行函数
#include<iostream>
#include<pthread.h>
#include <unistd.h>
//3.线程会执行到这里
void * func(void *arg)
{
while(true)
{
printf("%s\n",(char*)arg);
sleep(1);
}
}
int main()
{
//1.创建线程
pthread_t pt;
pthread_create(&pt,nullptr,func,(void*)"我是新线程");
//2.主线程
while(true)
{
printf("我是一个主线程\n");
sleep(1);
}
return 0;
}
现象:主线程和新线程都在执行各自的任务。
查看LWP
ps -aL | head -1 && ps -aL | grep test
现象:pid都相同说明是同一个进程,lwp区分不同线程。
补充:
在系统内核中只认识LWP,而我们用户需要使用线程就需要用thread标识符去操作线程,这是因为 POSIX线程库给我们用户进行了封装这类似于文件描述符bf和FILE*的关系。
4.3线程终止
4.3.1 从线程函数return。这种方法对主线程不适用,从main函数return相当于调用exit(结束进程)。
4.3.2pthread_ exit
功能
结束线程。
函数原型
void pthread_exit(void *retval);
参数
retval: 是一个指向返回值的指针,这个返回值可以被其他线程通过 pthread_join 函数(线程等待的函数)获取。如果线程没有设置返回值(即不关心其退出状态),可以传递 NULL 作为 retval 的参数。
4.3.3pthread_ cancel
功能
请求取消指定线程的函数。当一个线程调用 pthread_cancel 并向另一个线程发送取消请求时,被请求的线程可以选择在合适的时机终止执行。
函数原型
int pthread_cancel(pthread_t thread);
参数
thread: 线程标识符。
返回值
- 成功时返回 0。
- 失败时返回一个非零的错误码。
4.4线程等待
4.4.1为什么进行等待呢
- 释放线程的空间(有点像进程等待)。
- 获取线程的退出状态。
4.4.2pthread_join
功能
用于等待指定线程终止的函数。调用 pthread_join 的线程(通常是主线程或另一个线程)会被阻塞,直到被等待的线程结束执行或取消,并取到被等待线程的返回值或处理其终止后的资源清理。
函数原型
int pthread_join(pthread_t thread, void **retval);
参数
- thread : 线程标识符。
- 被等待线程的返回值。
返回值
- 成功时返回 0。
- 失败时返回一个非零的错误码,例如 ESRCH(无此线程)、EINVAL(线程不是可连接的)或 EDEADLK(检测到死锁)。
- 死锁是指两个或两个以上的进程(或线程)在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。
具体解释
- 如果thread线程通过return返回,value_ ptr所指向的单元⾥存放的是thread线程函数的返回值。
- 如果thread线程被别的线程调⽤pthread_ cancel异常终掉,value_ ptr所指向的单元⾥存放的是常 数PTHREAD_ CANCELED。
- 如果thread线程是⾃⼰调⽤pthread_exit终⽌的,value_ptr所指向的单元存放的是传给 pthread_exit的参数。
- 如果对thread线程的终⽌状态不感兴趣,可以传NULL给value_ ptr参数。
例子
创建新线程,再使用pthread_ exit结束线程,再用主线程进行等待。
#include<iostream>
#include<pthread.h>
#include <unistd.h>
#include<stdio.h>
//3.线程会执行到这里
void * func(void *arg)
{
//4.结束自己
char * ret = new char[50];
ret[0] = 'a';
ret[1] = 'b';
ret[2] = '\0';
pthread_exit((void*)ret);
return nullptr;
}
int main()
{
//1.创建线程
pthread_t pt;
pthread_create(&pt,nullptr,func,(void*)"我是新线程");
//3.主线程进程等待
void *ret;
int n = pthread_join(pt,&ret);
(void)n; //防止报警告
// 5.输出返回值
printf("%s\n",(char*)(ret));
return 0;
}
4.5线程分离
4.5.1为什么要进行线程分离
- 默认情况下,新创建的线程是joinable的,线程退出后,需要对其进⾏pthread_join操作,否则无法释放资源,从⽽造成系统泄漏。
- 如果不关心线程的返回值,join是⼀种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。
4.5.2pthread_detach
功能
用于将指定的线程与调用线程(通常是主线程)分离
函数原型
int pthread_detach(pthread_t thread);
参数
thread:线程标识符。
返回值
- 成功时,返回 0。
- 失败时,返回错误码。常见的错误码包括:
ESRCH: 指定的线程标识符无效或不存在。
EINVAL:线程已经是分离状态或已经是 joinable 状态但已经终止。
补充:
- 可以有自己分离也可以由其他线程分离。
- pthread_self():或者自己的线程标识符。
- 等待和分离是冲突的。
5、线程封装
使用面向对象的方式对线程进行封装。
#ifndef _THREAD_HPP__
#define _THREAD_HPP__
#include <iostream>
#include <string>
#include <pthread.h>
#include <functional>
#include <sys/types.h>
#include <unistd.h>
namespace ThreadModule
{
//函数包装
using func_t = std::function<void()>;
//线程个数
static int number = 1;
//状态
enum class TSTATUS
{
NEW,
RUNNING,
STOP
};
class Thread
{
private:
//执行线程函数
static void *Routine(void *args)
{
//强制类型转换
Thread* thread = (Thread*)args;
//调用任务
thread->_func();
return nullptr;
}
public:
Thread(func_t func) : _func(func), _status(TSTATUS::NEW), _joinable(false)
{
_name = "Thread-" + std::to_string(number);
_pid = getpid();
}
//创建线程
bool Start()
{
if (_status != TSTATUS::RUNNING) // 保证线程处于非运行状态
{
int n = pthread_create(&_tid, nullptr, Routine, (void *)this); // 创建线程
if (n != 0)
{
return false;
}
_status = TSTATUS::RUNNING; // 更新状态
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 && _status == TSTATUS::RUNNING)
{
//等待线程,默认不关心线程状态
int n = pthread_join(_tid,nullptr);
if(n != 0)
{
return false;
}
_status = TSTATUS::STOP; // 更新状态
return true;
}
return false;
}
//线程分离
void Detach()
{
//保证线程不处于分离状态且处于运行状态
if(!_joinable && _status == TSTATUS::RUNNING)
{
int n = pthread_detach(_tid); //进行线程分离
if(n != 0)
{
return ;
}
std::cout<<"完成线程分离\n"<<std::endl;
_joinable = true; //更新分离状态
}
}
bool IsJoinable()
{
return _joinable;
}
std::string Name()
{
return _name;
}
~Thread() {}
private:
std::string _name; // 线程name
pthread_t _tid; // 线程id
pid_t _pid; // 进程id
bool _joinable; // 是否是分离的,默认不是
func_t _func; // 线程执行的任务
TSTATUS _status; // 线程状态
};
}
#endif