什么是线程
- 线程是比进程的执行粒度更细的一个执行流(线程是进程的子集),是进程内部的执行分支(可理解为线程是在进程的地址空间内运行),是调度的基本单位。
- 一个进程至少有一个执行线程
- Linux没有真正的线程,但它用进程来模拟实现线程,而这种进程被称为轻量级进程。
- 线程用TCB管理,但是Linux下没有TCB,因此Linux用PCB模拟TCB,创建进程只有创建PCB,再分配资源即可,并且cup认为PCB为调度的基本单位。
线程和进程的比较
- 进程时资源竞争的基本单位
- 线程是程序执行的最小单位
- 线程共享同一进程数据,但也拥有自己独立的一部分数据,如自己的上下文数据(主要用于恢复数据)、线程ID、私有结构栈、寄存器、errno、信号屏蔽字、调度优先级,所以线程相对于进程而言,创建和调度成本低。
- 一个进程的多个线程共享以下数据:同一个地址空间、文件描述符表、每种信号的处理方式、当前工作目录、用户ID和组ID,一个进程只有一个主线程
线程的优点
- 创建成本低。
- 调度(选择、切换)成本低。
- 占用资源少。
- 能充分利用多处理器的可并行数量。
- 在等待慢速I/O操作结束的同时,程序可执行其他的计算机任务。
- 计算密集型应用的时候,为了能在多处理器系统上运行,将计算分解到多个线程中实现。
- 线程可以同时等待不同I/O操作。
线程的缺点
- 性能损失(主要体现在计算密集型线程,由于该线程不可和其他线程公用同一个处理器,所以当该线程数量超过处理器数量的时候,就会增加额外的同步和调度开销,而可用的资源不变)
- 健壮性降低(线程和线程间缺乏安全性)
- 缺乏访问控制(也就是一个线程在调用某些OS函数时,会影响整个进程,更通俗一点就是,一个线程的某些操作会影响整个进程,特别是当一个线程出错的时候,整个进程就会被释放)
- 编辑难度提高
线程控制
POSI线程库
- 和线程有关的函数构成可一个完整的系列,一般都以pthread_ 开头。
- 链接这些线程函数时需要使用编译器命令的“-lpthread”选项。
- 头文件”pthread.h”
创建线程
int pthread_create(pthread_t* thread, const pthread_attr_t* attr, void* (*start_routine)(void*) void* arg);
//thread:返回线程ID,ptread_t是无符号长整型
//attr:线程属性,一般设默认NULL
//start_routine:函数指针,表示线程要执行的函数
//arg:传个线程启动函数的参数,一般为NULL
//返回值:成功返回0,失败返回错误码
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
void* rout(void* arg){//必须要交上参数,否则会报错,并且编译时一定要加上-lpthread选项
(void)arg;
for(;;){
printf("I an thread1!\n");
sleep(2);
}
}
int main(){
pthread_t tid;
int ret;
if((ret = pthread_create(&tid, NULL, rout, NULL)) != 0){
printf("pthread_create error, error : %s\n", strerror(ret));
exit(EXIT_FAILURE);
}
for(;;){
printf("I am main thread\n");
sleep(1);
}
return 0;
}
线程ID
线程ID是pid_t类型的变量,而且用来唯一标识线程的一个整型变量。
Linux提供了gettid系统调用来返回其线程ID,但是gilbc并没有将该系统调用封装起来,在开放接口来供程序员使用,如果想要获得线程ID,可以使用如下方法:
#include <sys/syscall.h>
pid_t tid;
tid = syscall(SYS_gettid);
pthread_create函数会产生一个线程ID,存放在第一个参数指向的地址中,该线程ID和前面说的线程ID不是一回事,前面的线程ID属于进程调度范围,因为线程是轻量级进程,是操作系统的调度器的最小单位,所以需要一个唯一的值来表示该线程。
线程库NPTL提供了pthread_self函数来获得线程自身的ID
pthread_t pthread_self(void);
线程终止
止终止线程而不终止整个进程可以使用以下三个方法:
- 从线程函数处return。这种方法对主线程不适用,因为从main函数return相当于调用exit。
- 线程可以调用pthread_exit来终止自己。
- 一个线程可以调用pthread_cancle终止同一个进程中的另外一个线程。
pthread_exit函数:线程终止
void pthread_exit(void* value_ptr);
//value_ptr:不要将其指向一个局部变量
//返回值:无返回值,线程结束的时候无法返回到它调用者(自身)
注意:pthread_exit或者return返回指针所指向的内存单元必须是全局的或者使用malloc分配的,不能再线程函数栈上分配,因为当其他线程得到这个返回指针的时候线程函数已经退出了。
pthread_cancel函数:取消一个执行中的线程
int pthread_cancel(pthread_t thread);
//thread:线程ID
//返回值:成功放会0,失败返回错误码
进程等待
为什么?
- 已经退出的线程,其空间并没有被释放,仍然在进程的地址空间内。
- 创建新的线程不会复用刚才退出线程的地址空间。
因此,就需要等待线程真正终止。
方法:
int pthread_join(pthread_t thread, void** value_ptr);
返回值:成功返回0,失败返回错误码
thread线程以不同的方法终止,通过pthread_join的到的终止状态也有所不同:
- 通过return返回,value_ptr所指向的单元内存放的是thread线程函数的返回值
- thread线程被别的线程调用pthread_cancel异常终止,value_ptr指向的单元里存放的是常熟PTHREAD_CANCELED
- 被自己调用pthread_exit终止,其指向的单元存放的是传给pthread_exit的参数
- 如果对其不感兴趣,就传NULL给value_ptr
分离线程
为神马:
- 默认情况下,新创建的线程在线程退出后,需要进行pthread_join操作,否则无法释放资源,从而造成内存泄漏
- 如果不关心线程的返回值,join就是一种负担, 这时,我们就可以让操作系统,当线程退出时,自动释放资源。
进程内任意分离的线程出现异常,同样会影响到进程
//用于线程分离
int pthread_detach(pthread_t thread);
测试代码:
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
void* thread_run1(char* arg){
printf("I am %s\n", arg);
pthread_detach(pthread_self());
pthread_exit(0);
//return (void*)123;
}
void* thread_run2(char* arg){
printf("I am %s\n", arg);
return (void*)123;
}
int main(){
pthread_t tid1, tid2;
pthread_create(&tid1, NULL, thread_run1, "thread 1");
pthread_create(&tid2, NULL, thread_run2, "thread 2");
// pthread_cancel(tid);
void* ret1;
void* ret2;
pthread_join(tid1, &ret1);
pthread_join(tid2, &ret2);
printf("%lu %lu\n", pthread_self(), tid1);
printf("main thread run new thread ret:%lu\n", (size_t)ret1);
return 0;
}