线程
什么是线程
- 在一个程序里的一个执行路线就叫做线程(thread)。更准确的定义是:线程是“一个进程内部的控制序列”
- 所有进程至少都有一个执行线程,线程是进程内部的一个执行流,进程是承担分配系统资源的基本实体
- 线程在进程内部运行,本质是在进程地址空间内运行
- 在Linux系统中,在CPU眼中,看到的PCB都要比传统的进程更加轻量化
- 透过进程虚拟地址空间,可以看到进程的大部分资源,将进程- 资源合理分配给每个执行流,就形成了线程执行流
- 线程是一个执行分支,执行粒度比进程更细,调度成本更低
- 线程是CPU调度的基本单位,进程是承担分配系统资源的基本实体
我们之前讲的进程就是只有一个pcb,讲的是特例,一个进程可以有很多个pcb的,一个线程一个pcb。
现在讲的线程叫做执行流,也叫LWP(Lightweight Process,轻量级进程),只有一个pcb,每个线程的pcb都共用一个地址空间,每个线程执行代码中不同的函数
LWP叫做轻量级进程id,这个程序只有一个线程,所以LWP和PID是相同的,如果有多个线程,就可以看到所有线程的PID是一样的,而LWP是不同的
LWP和PID相同的就是主进程,线程调度的时候实际上是看LWP的,而不是之前只有进程的概念的时候说的看PID,只有一个线程时PID和LWP是一样的,所以有时看PID也是可以的
线程就是进程的子集,每个进程包含了很多个线程
不同的操作系统对于线程进程的设计不一样,Windows下,为线程创造了TCB,要对线程管理,线程的调度设计数据结构等,Linux中,线程TCB是复用了进程PCB的代码,Linux中没有真正意义上的线程,是用进程模拟的线程
线程的优缺点
优点:
- 创建一个新线程的代价要比创建一个新进程小得多
- 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
- 线程占用的资源要比进程少很多
- 能充分利用多处理器的可并行数量
- 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
- 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
- I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作
缺点:
- 性能损失:一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。
- 健壮性降低:编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。
- 缺乏访问控制:进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。
- 编程难度提高:编写与调试一个多线程程序比单线程程序困难得多
线程的使用可以带来以下几个优势:
-
提高程序的并发性:通过多线程的方式,可以将程序的不同部分并行地执行,从而提高程序的运行效率。
-
提高程序的响应性:将耗时的操作放在后台线程中执行,使得程序在执行这些操作的同时,仍然能够响应用户的输入和其他事件。
-
方便的资源共享和通信:线程之间可以共享同一进程的资源,可以通过共享内存、消息队列、管道等方式进行数据共享和通信,从而简化了多任务编程的复杂性。
-
轻量级的创建和切换:相比于进程,线程的创建和切换的开销更小,因此可以更高效地利用系统资源。
线程编程也存在一些注意事项:
-
线程安全问题:多个线程同时访问和修改共享数据时,可能会导致数据不一致或竞态条件等问题,需要采取同步机制(如互斥锁、信号量)来保证线程安全。
-
死锁问题:当多个线程相互等待对方释放资源时,可能会导致死锁,造成程序无法继续执行,需要仔细设计和管理线程的同步和资源使用。
-
上下文切换开销:线程之间的切换需要保存和恢复执行上下文,这涉及到一定的开销,如果线程数量过多,频繁的切换可能会影响程序的性能。
-
调试和测试困难:多线程程序的调试和测试相对复杂,由于线程之间的交互和并发执行,可能出现一些难以重现和排查的问题。
线程异常
我们试着使用一下和线程有关的接口,创造几个线程
makefile:
mythread:mythread.cc
g++ -o $@ $^ -std=c++11 -lpthread #这里要链接pthread库要不然会报错
.PHONY:clean
clean:
rm -f mythread
mythread.cc:
#include <iostream>
#include <unistd.h>
#include <pthread.h>
using namespace std;
void *thread1(void* args)
{
while(true)
{
sleep(1);
cout << "线程一" << getpid() << endl;
}
}
void *thread2(void* args)
{
while(true)
{
sleep(1);
cout << "线程二" << getpid() << endl;
}
}
int main()
{
pthread_t t1, t2, t3;
pthread_create(&t1, nullptr, thread1, nullptr);
pthread_create(&t2, nullptr, thread2, nullptr);
while(true)
{
sleep(1);
cout << "主线程" << getpid() << endl;
}
return 0;
}
运行之后可以看到所有线程都同时执行起来了,并且执行的都是死循环,所有线程的PID都是一样的,主线程的PID和LWP是一样的
我们修改一下代码,在线程二中加入这样一句代码,让线程二越界
可以看到线程二崩溃了,整个进程都都没有运行了,所以多线程中,任何一个线程崩溃都会导致整个进程崩溃
从系统角度来看,线程是进程的分支,线程崩溃了,就是进程崩溃了
从信号的角度来看,线程越界访问,就是硬件上MMU查页表发现没有权限访问,发送硬件异常产生的信号给整个进程,整个进程就崩溃了
Linux下进程和线程
进程和线程关系:
- 进程是资源分配的基本单位
- 线程是调度的基本单位
- 线程共享进程数据,但也拥有自己的一部分数据:
- 线程ID
- 一组寄存器
- 私有栈
- errno
- 信号屏蔽字
- 调度优先级
进程和线程共有属性:
-
标识符(ID):每个进程和线程都有一个唯一的标识符。进程使用进程ID(PID)来标识,而线程使用线程ID(LWP)来标识。
-
状态(State):进程和线程都有不同的状态,例如运行(Running)、就绪(Ready)、阻塞(Blocked)等。这些状态描述了进程或线程当前的运行状态。
-
优先级(Priority):进程和线程可以分配不同的优先级,用于确定它们在调度时的相对重要性。较高优先级的进程或线程在竞争CPU时间时通常更容易被调度。
-
资源:进程和线程都可以拥有一些资源,例如打开的文件描述符、内存空间、CPU时间等。这些资源可以被进程或线程使用,但受到系统限制。
-
上下文(Context):进程和线程都具有自己的上下文信息,包括寄存器值、程序计数器(PC)、栈指针等。上下文用于保存进程或线程的执行状态,在切换时进行恢复。
-
父子关系:进程和线程之间可以存在父子关系。一个进程可以创建子进程,而一个线程可以创建子线程。父进程或线程可以对其子进程或线程进行控制和管理。
-
调度信息:进程和线程都有与调度相关的信息,例如调度策略(如先来先服务、时间片轮转等)和调度参数(如时间片大小、优先级值等)。
Linux下没有真正意义上的线程,而是用进程模拟的线程,所以Linux不会提供直接创建线程的系统调用,只会提供创建轻量级进程的接口
因为Linux中无法在内核中实现多线程,所以提供了用户级线程库(pthread.h),也叫原生线程库,对Linux接口进行封装实现了线程,并将接口提供给用户
所以要使用线程相关的接口要链接pthread库
如:g++ -o test test.c -std=c++11 -lpthread#链接pthread库
线程控制
线程创建pthread_create
pthread_create
是 pthread
库中用于创建新线程的函数
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
:线程执行的函数的函数指针,该函数在新线程创建后会被调用。arg
:传给线程执行的函数的参数,可以是任意类型的指针。
pthread_create
函数的作用是创建一个新的线程,并使其开始执行指定的线程入口函数。
注意事项:
- 线程入口函数的返回类型必须为
void*
,并且接受一个void*
类型的参数。 - 线程入口函数可以通过返回
NULL
或调用pthread_exit
函数来结束线程。 - 如果不需要关心新线程的退出状态,可以将线程属性设置为分离状态,或者使用
pthread_detach
函数将线程设置为分离状态。这样线程结束后会自动释放资源,无需调用pthread_join
。 - 创建线程时,可以使用线程属性对象
pthread_attr_t
进行一些属性的设置,例如设置线程的堆栈大小、调度策略等。
我们来使用一下pthread_create
makefile:
mythread:mythread.cc
g++ -o $@ $^ -std=c++11 -lpthread#链接pthread库
.PHONY:clean
clean:
rm -f mythread
mythread.cc:
#include <iostream>
#include <unistd.h>
#include <pthread.h>
using namespace std;
void *thread1(void* args)
{
while(true)
{
sleep(1);
cout << "线程一" << endl;
}
}
int main()
{
pthread_t t1;
pthread_create(&t1, nullptr, thread1, nullptr);
while(true)
{
sleep(1);
cout << "主线程, 新线程id" << t1 << endl;
}
return 0;
}
可以看到线程成功被创建,并且两个线程一起运行
我们修改一下住进程的代码,让主进程打印一次后,等3秒后退出
可以看到主线程退出了,进程也就退出了
线程等待pthread_join
pthread_join
函数是POSIX线程库中的一个函数,用于等待一个指定的线程终止并获取其返回值
int pthread_join(pthread_t thread, void **retval);
thread
:要等待的目标线程的标识符,即线程ID。retval
:输出型参数,将目标线程的返回值带出来
pthread_join
函数的作用是阻塞当前线程,直到目标线程终止。如果目标线程已经终止,那么调用pthread_join
函数将立即返回。当目标线程终止时,它的返回值可以通过retval
参数返回给调用者。
pthread_join
:
- 调用
pthread_join
函数的线程(称为"调用线程")会阻塞,等待指定的目标线程结束。 - 如果目标线程已经结束,那么
pthread_join
函数立即返回,并将目标线程的返回值存储在retval
指向的位置。 - 如果目标线程尚未结束,那么调用线程将被阻塞,直到目标线程结束为止。
- 当目标线程结束时, 它的返回值将被存储在
retval
指向的位置,并且pthread_join
函数将返回0,表示成功。 - 如果在调用
pthread_join
函数时传递了一个无效的线程ID,或者目标线程无法被等待,那么pthread_join
函数可能会返回一个非零的错误码。
需要注意的是,pthread_join
函数只能等待一个线程的终止。如果需要等待多个线程的终止,可以多次调用pthread_join
函数。
在使用pthread_join
函数等待线程终止时,需要确保目标线程在终止之前不会被分离(通过pthread_detach
函数)。否则,pthread_join
函数将返回错误。
例:
#include <iostream>
#include <unistd.h>
#include <pthread.h>
using namespace std;
void *thread1(void* args)
{
while(true)
{
sleep(1);
cout << "线程一" << endl;
}
}
int main()
{
pthread_t t1;
pthread_create(&t1, nullptr, thread1, nullptr);
cout << "主线程, 新线程id" << t1 << endl;
pthread_join(t1, nullptr);
return 0;
}
可以看到主线程并不会退出,一直在等待新线程退出
线程终止pthread_exit
线程终止有多种情况,比如,正常的退出
void *thread1(void* args)
{
while(true)
{
sleep(1);
cout << "线程一" << endl;
}
return nullptr;
}
exit退出,注意exit是让整个进程退出,所有线程都会退出,不要让线程随意调用
void *thread1(void* args)
{
while(true)
{
sleep(1);
cout << "线程一" << endl;
}
exit(1);
}
pthread_exit
函数是POSIX线程库中的一个函数,用于终止当前线程并返回一个指定的值。它的函数原型如下:
void pthread_exit(void *retval);
retval
:线程的返回值,可以是任意类型的指针。可以通过线程等待函数pthread_join
获取
pthread_exit
函数的作用是立即终止当前线程的执行,并将指定的返回值传递给等待该线程的其他线程(通过pthread_join
函数获取返回值)。
如果主线程调用pthread_exit
函数,那么整个进程将终止。
注意,pthread_exit
函数在不同线程库和操作系统中的行为可能会有所不同。在一些系统中,它可能会执行一些清理工作,例如关闭文件描述符或释放资源。确保在调用pthread_exit
函数之前完成必要的清理工作,并遵循线程库的规范和操作系统的要求。
通过pthread_exit
函数返回,让pthread_join
函数拿到结果并输出
#include <iostream>
#include <unistd.h>
#include <pthread.h>
using namespace std;
void *thread1(void* args)
{
int cnt = 6;
while(cnt--)
{
sleep(1);
cout << "线程一" << endl;
}
pthread_exit((void*)10);
}
int main()
{
pthread_t t1;
pthread_create(&t1, nullptr, thread1, nullptr);
cout << "主线程, 新线程id" << t1 << endl;
void* ret = nullptr;
pthread_join(t1, &ret);
cout << "进程退出结果:" << (size_t)ret << endl;
return 0;
}
线程取消pthread_cancel
pthread_cancel
函数是POSIX线程库中的一个函数,用于请求取消指定线程的执行。它的函数原型如下:
int pthread_cancel(pthread_t thread);
thread
:要取消的线程的标识符。
pthread_cancel
函数的作用是向指定的线程发送一个取消请求,以请求线程在适当的时机终止执行。线程在接收到取消请求后,可以选择在适当的取消点(cancellation point)处终止执行,或者忽略取消请求继续执行。
需要注意的是,pthread_cancel
函数只是向线程发送一个取消请求,不会等待线程终止。如果需要等待线程终止并获取其返回值,可以使用pthread_join
函数。
需要注意的是,线程必须在适当的取消点处进行检查取消请求,以响应取消请求
#include <iostream>
#include <unistd.h>
#include <pthread.h>
using namespace std;
void *thread1(void* args)
{
int cnt = 6;
while(cnt--)
{
sleep(1);
cout << "线程一" << endl;
}
pthread_exit((void*)10);
}
int main()
{
pthread_t t1;
pthread_create(&t1, nullptr, thread1, nullptr);
cout << "主线程, 新线程id" << t1 << endl;
sleep(3);//让新线程跑3秒后在取消
pthread_cancel(t1);
void* ret = nullptr;
pthread_join(t1, &ret);
cout << "进程退出结果:" << (int64_t)ret << endl;
return 0;
}
可以看到线程一跑了3秒后退出了,并且返回了-1,并没有返回我们设置的10,其实是返回了宏,PTHREAD_CANCELED
((void*)-1)
线程获取自己的idpthread_self
pthread_self
是POSIX线程库中的一个函数,用于获取当前线程的线程ID(Thread ID)。它的函数原型如下:
pthread_t pthread_self(void);
pthread_self
函数返回调用线程的线程ID,即pthread_t
类型的值。每个线程都有一个唯一的线程ID,可以用来标识不同的线程。
#include <iostream>
#include <unistd.h>
#include <pthread.h>
using namespace std;
void *thread1(void* args)
{
int cnt = 6;
while(cnt--)
{
sleep(1);
cout << "线程一id:" << pthread_self() << endl;
}
pthread_exit((void*)10);
}
int main()
{
pthread_t t1;
pthread_create(&t1, nullptr, thread1, nullptr);
cout << "主线程, 新线程id:" << t1 << endl;
void* ret = nullptr;
pthread_join(t1, &ret);
cout << "进程退出结果:" << (int64_t)ret << endl;
return 0;
}
线程分离pthread_detach
pthread_detach
是一个函数,用于将一个线程分离(detach)出来,可以自己把自己分离,也可以别的线程来分离它,使其成为一个"分离线程",从而使得线程的资源在其运行结束后可以自动释放,无需也不能其他线程调用pthread_join
来回收资源。
#include <pthread.h>
int pthread_detach(pthread_t thread);
参数thread
是要设置为分离线程的目标线程的标识符。
当一个线程被设置为分离线程后,线程的状态信息和资源将在线程运行结束后自动被系统回收,而无需其他线程通过调用pthread_join
来进行回收。这意味着分离线程不会成为"僵尸线程",也不会占用系统资源。
需要注意的是:
- 使用
pthread_detach
函数设置线程为分离线程的操作应当在线程创建后但尚未运行之前进行。一旦线程开始运行,它就不能被设置为分离线程。 - 分离线程一旦被设置,就不能再通过
pthread_join
函数来等待其结束。因此,对于需要获取线程的退出状态或进行其他处理的情况,应该使用非分离线程,并在适当的时候通过pthread_join
来等待线程的结束。
#include <iostream>
#include <unistd.h>
#include <cstdio>
#include <cerrno>
#include <cstring>
#include <pthread.h>
using namespace std;
void *thread1(void* args)
{
int cnt = 6;
while(cnt--)
{
sleep(1);
cout << "线程一id:" << pthread_self() << endl;
}
return nullptr;
}
int main()
{
pthread_t t1;
pthread_create(&t1, nullptr, thread1, nullptr);
cout << "主线程, 新线程id:" << t1 << endl;
pthread_detach(t1);
sleep(4);//让新线程跑4秒后再等待新线程
int n = pthread_join(t1, nullptr);
if(n != 0)
{
cout << "errno : " << n << ":" << strerror(n) << endl;
}
return 0;
}
可以看到,线程被分离后如果还进行线程等待会报错
线程id
如图,线程id实际上就是线程在程序地址空间上的起始位置的地址,所以打出的数字才会那么大
#include <iostream>
#include <pthread.h>
using namespace std;
void *thread(void* args)
{
return nullptr;
}
int main()
{
pthread_t t1;
pthread_create(&t1, nullptr, thread, (void*)"线程一");
cout << "主线程, 新线程id:" << t1 << endl;
return 0;
}