什么是线程?
- 线程是参与系统调度的最小单位,包含在进程之中,是进程中的实际运行单位。
如何创建线程?
- 当一个程序启动时,就有一个进程被操作系统创建,与此同时主线程(Main Thread)也立刻运行。
- 只有主线程的进程称为单线程进程,多线程是指除了主线程以外,还包含其他的线程,通常由主线程来创建,这个创建的新线程就是主线程的子线程。
线程的特点?
- 线程时程序最基本的运行单位,可以认为进程仅仅是一个容器,包含了线程运行所需要的数据结构、环境变量等信息。
- 同一进程的多个线程将共享该进程中的全部系统资源,如虚拟地址空间、文件描述符和信号处理等。
- 同一进程的多个线程有各自的调用栈(call stack),自己的寄存器环境、自己的线程本地存储。
线程与进程之间
- 进程切换开销大
- 进程间通信较为麻烦,每个进程都在各自的地址空间中,相互独立、隔离,处在不同的地址空间中。
- 同一进程的多个线程间切换开销比较小
- 同一进程的多个线程间通信容易,共享了进程的地址空间,所以它们都是在同一个地址空间,通信容易
- 线程创建的速度远大于进程创建的速度
- 多线程在多核处理器上更有优势
线程ID
与每个进程都有一个进程ID一样,线程也有对应的标识,不同的是,进程ID在整个系统中是唯一的,线程ID只有在它所属的进程上下文中才有意义。
pthread_t pthread_self(void);
在linux系统中,使用无符整型(unsigned long int)来表示pthread_t数据类型。
创建线程
主线程可以使用库函数pthread_createf负责创建一个新的线程。
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
thread:pthread_t类型指针。
attr:定义了线程的各种属性
start_routine:是一个函数指针
arg:传给start_routine的参数
编写程序测试。
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
static void *new_thread_start(void *arg){
printf("新线程: 进程ID<%d> 线程ID<%lu>\n",getpid(),pthread_self());
return (void *)0;//返回指针类型
}
int main(void){
pthread_t tid;//创建线程id
int ret;
ret = pthread_create(&tid,NULL,new_thread_start,NULL);
if(ret){
fprintf(stderr,"ERROR:%s\n",strerror(ret));
exit(-1);
}
printf("主线程: 进程ID<%d> 线程ID<%lu>\n",getpid(),pthread_self());
sleep(1);
exit(0);
}
小tip:
fprintf(stderr,"ERROR:%s\n",strerror(ret));
之前有学过fprintf,但是并没有具体用到,现在是个好机会来看看这个函数了。
当一个用户进程被创建的时候,系统会自动为该进程创建三个数据流。分别是标准输出stdout、
标准输入stdin、标准错误stderr。
有了stderr存在,即使对标准输出进行了重定向,写到stderr中的输出通常也会显示在屏幕上。
stderr是无缓冲的。
再来看fprintf,它和printf的区别就是,printf打印输出到屏幕,fprintf是打印输出到文件。
来看一下运行结果。
终止线程
void pthread_exit(void *retval);
如果主线程调用了pthread_exit(),那么主线程也会终止,但是其他线程依然正常运行,知道进程中所有线程终止才会使得进程终止。
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
static void *new_thread_start(void *arg){
printf("新线程start\n");
sleep(1);
printf("新线程end\n");
pthread_exit(NULL);
}
int main(void){
pthread_t tid;//创建线程id
int ret;
ret = pthread_create(&tid,NULL,new_thread_start,NULL);
if(ret){
fprintf(stderr,"ERROR:%s\n",strerror(ret));
exit(-1);
}
pthread_exit(NULL);
printf("主线程end\n");
exit(0);
}
看一下运行结果。
跟我们预想的一样。
回收线程
在父、子进程中,父进程可通过wait()函数阻塞等待子进程退出并获取其终止状态,回收子进程资源。
那么在线程当中,也需要如此,通过调用pthread_join()函数来阻塞等待线程终止,并获取线程的退出码,回收线程资源。
int pthread_join(pthread_t thread, void **retval);
- 线程的关系是对等的。进程中的任意线程均可调用pthread_join函数来等待另一个线程的终止。这与进程间层次关系不同,父进程如果使用fork()创建了子进程,那么它是唯一能够对子进程调用wait()的进行。
- 不能以非阻塞的方式调用pthread_join()。对于进程,调用waitpid()既可以实现阻塞方式等待,也可以实现非阻塞方式等待。
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
static void *new_thread_start(void *arg){
printf("新线程start\n");
sleep(2);
printf("新线程end\n");
pthread_exit((void *)10);
}
int main(void){
pthread_t tid;//创建线程id
void *tret; //存放退出码
int ret;
ret = pthread_create(&tid,NULL,new_thread_start,NULL);
if(ret){
fprintf(stderr,"ERROR:%s\n",strerror(ret));
exit(-1);
}
ret = pthread_join(tid,&tret);
if(ret){
fprintf(stderr,"pthread_join error:%s\n",strerror(ret));
exit(-1);
}
printf("新线程终止,code = %ld\n",(long)tret);
exit(0);
}
结果。
线程安全
线程栈
进程中创建的每个线程都有自己的栈地址空间,称为线程栈。
那么每个线程运行过程中所定义的自动变量(局部变量)都是分配在自己的线程栈中的,不会互相干扰。
可重入函数
单线程程序只有一条执行流,而对于多线程程序而言,同一进程却存在多条独立、并发的执行流。
如果一个函数被同一进程的多个不同的执行流同时调用,每次函数调用总能产生正确的结果,这样的函数就成为可重入函数。
函数被多个执行流同时调用的两种情况:
- 在一个含有信号处理的程序中,主函数正在执行函数func(),此时进程接收到信号,主程序被打断,跳转到信号处理函数中执行,信号处理函数也调用了func()。
- 在多线程的环境下 ,多个线程并发调用同一个函数
绝对可重入函数:
- 函数内所使用到的变量均为局部变量。
- 函数参数和返回值均是值类型
- 函数内调用的其他函数也均是绝对可重入函数
线程安全函数
一个函数被多个线程同时调用时,它总会一直产生正确的结果,把这样的函数成为线程安全函数。
线程安全函数包括可重入函数。
参考正点原子应用开发教程