线程的概念、优缺点和创建步骤
1. 什么是线程
线程的概念:轻量级的进程,一个进程内部可以有多个线程,默认情况下一个进程只有一个线程
线程是最小的执行单位,进程是最小的系统资源分配单位
线程就是轻量级的进程,如果说下图中的a.out
,是一个进程,那里面的那几根"弯弯曲曲"的线,就是所谓的线程,一般来说,一个进程中只有一个线程,当然,一个进程中也可以有多个线程,这种状况就叫做多线程,当然从图上也可以看出来,一个进程中的多个线程,也是公用的相同的资源的:
因为线程是公用进程的空间的,那么可以理解为,在内存上,除了栈(stack)这块,其他地方都是共享的!(上图的左侧所示),这是因为线程是有自己的执行目的的,每个线程的任务是非常明确的,后面会提到,一个线程实际上就是执行的一个函数,因为函数是存储在栈中的,所以这块是没法共享的,要进行区分,除此之外,其他区域都是可以共享的。
线程的好处也就可以看出来了,线程都在一个进程内部,随便一个变量所有线程都可以用;线程可以更有效的理由CPU(但是,要说的是,如果电脑只有一块CPU,一个核心数,线程再多也没办法同时"干活儿",因为在一个进程中,只能有一个“线程”在运行的(可以把线程理解成一个函数)),总之,多线程和多进程,都是为了更充分的利用CPU。
那么再做个关于"进程"和"线程"的形象的比喻,一个工厂,但可供的电量有限,只能供应三个车间,错开开工,不能同时开工,那么这个工厂,就好比是cpu,这三个车间,可以理解成"进程",那么车间里面的每个人(苦力,真正干活的),就相当于"线程"。那么每个"线程"都有非常明确的分工。在车间里的设备和资源,对每个人(线程)来说也都是公有的。
线程是最小的执行单位,进程是最小的系统资源分配单位 ,从内核角度看,进程和线程是没有区分的,内核实现都是通过 clone 函数实现的,线程也有自己的PCB
2. 线程共享资源与非共享资源
尽量不要让线程与信号放在一起使用,避免乱上加乱
线程共享资源:
- 文件描述符表
- 每种信号的处理方式
- 当前工作目录
- 用户ID和组ID
- 内存地址空间 (.text/.data/.bss/heap/共享库)
线程非共享资源:
- 线程id
- 处理器现场和栈指针(内核栈)
- 独立的栈空间(用户空间栈)
- errno变量(每个线程有自己独有的errno变量)
- 信号屏蔽字
- 调度优先级(可以设置线程的优先级)
每个线程有自己的errno
。
通过如下函数可以获得错误码对应的错误信息
char *strerror(int errnum);
3. 线程的优缺点
优点:
- 提高程序并发性(为了更好的利用CPU)
- 开销小(不必再申请空间,直接用的是进程的空间)
- 数据通信、共享数据方便(在一个进程中的线程,可以共享进程中所创建的变量来共用)
缺点:
- 库函数,不稳定 (因为早期Unix并没有线程的概念,线程概念是后加的,所以,是放在了库函数中)
- 调试、编写困难(可以让程序员后天学习克服)
- 对信号支持不好(大不了就不用信号了)
汇总:优点相对突出,缺点均不是硬伤。Linux下由于实现方法导致进程、线程差别不是很大。
4. 创建一个线程
int pthread_create( pthread_t *thread,
const pthread_attr_t *attr,
void *(*start_routine) (void *),
void *arg);
thread 线程的ID,传出参数
attr 代表线程的属性
第三个参数 函数指针, void *func(void*)
arg 线程执行函数的参数
返回值 成功 0 失败 errno
编译时需要加 -lpthread
注意:线程ID在进程内是唯一的,但是在整个操作系统内部不一定是唯一的。
#include<stdio.h>
#include<pthread.h>
#include<unistd.h>
void *thr(void* arg){
//无符号长整型数 %lu
printf("I am a thread! pid=%d, tid=%lu\n", getpid(), pthread_self());
}
int main(){
pthread_t tid;
pthread_create(&tid, NULL, thr, NULL);
printf("I am main thread, pid=%d, tid=%lu\n", getpid(), pthread_self());
sleep(1);
return 0;
}
上面的代码存在一个问题:
就是如果主线程不睡眠,则另一个线程没有机会去执行(因为主线程打印之后就直接执行return 0
了)。
将上面代码中的sleep(1)
换成如下语句即可:
pthread_exit(NULL);
4.1 线程退出函数pthread_exit
注意事项:
- 线程中使用
pthread_exit
来退出线程 - 线程中可以用
return
(主控线程不行) exit
代表退出整个进程
4.2 线程回收pthread_join
线程回收函数,阻塞等待
int pthread_join(pthread_t thread, void **retval);
thread 创建的时候传出的第一个参数
retval 代表的传出线程的退出信息(就是返回值)
#include<stdio.h>
#include<unistd.h>
#include<pthread.h>
void *thr(void *arg){
printf("I am a thread, tid=%lu\n", pthread_self());
sleep(5);
printf("I am a thread, tid=%lu\n", pthread_self());
return (void*)100;
}
int main(){
pthread_t tid;
pthread_create(&tid, NULL, thr, NULL);
void *ret;
//线程回收函数
pthread_join(tid, &ret);
printf("ret exit with %d\n",(int)ret);
pthread_exit(NULL);
return 0;
}
4.3 杀死线程pthread_cancel
#include<stdio.h>
#include<unistd.h>
#include<pthread.h>
void *thr(void*arg){
while(1){
printf("I am thread, very happy! tid=%lu\n", pthread_self());
sleep(1);
}
return NULL;
}
int main(){
pthread_t tid;
pthread_create(&tid, NULL, thr, NULL);
sleep(5);
//杀死线程
pthread_cancel(tid);
void *ret;
//阻塞等待回收线程
pthread_join(tid, &ret);
printf("thread exit with %d\n", ret);
return 0;
}
如果把上面的代码中的thr函数中的printf
动作和sleep
动作注释之后,线程就杀不死了。因为pthread_cancle
函数需要有一个取消点。
如果你的线程函数里面实在是没有取消点,你可以加上这样的一个函数:
pthread_testcancel();
通过这个函数可以强行添加一个取消点。
4.4 线程分离pthread_detach
#include<stdio.h>
#include<unistd.h>
#include<pthread.h>
#include<string.h>
void *thr(void *arg){
printf("I am a thread, self=%lu\n", pthread_self());
sleep(4);
printf("I am a thread, self=%lu\n", pthread_self());
return NULL;
}
int main(){
pthread_t tid;
pthread_create(&tid, NULL, thr, NULL);
//线程分离
pthread_detach(tid);
sleep(5);
int ret=0;
//阻塞失败
if((ret=pthread_join(tid, NULL))>0){
printf("join err:%d, %s\n", ret, strerror(ret));
}
return 0;
}
执行了线程分离之后,pthread_join
函数就回收失败了。
4.5 判断两线程ID是否相等pthread_equal
4.6 线程属性设置分离
创建那种易产生就直接是分离状的线程,不需要我们去执行detach
函数来进程线程分离(因为执行detach
函数还会有一些特殊情况,比如线程创建好之后很快就结束了,此时还没执行到detach
函数)
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <string.h>
void *thr(void *arg) {
printf("I am a thread \n");
return NULL;
}
int main() {
//设置线程属性
pthread_attr_t attr;
//初始化属性
pthread_attr_init(&attr);
//设置线程分离属性,这样线程创建好之后就直接分开了
pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED);//设置属性分离
pthread_t tid;
//创建线程 第二个参数是线程属性
pthread_create(&tid,&attr,thr,NULL);
int ret;
//阻塞回收线程失败
if((ret = pthread_join(tid,NULL)) > 0){
printf("join err:%d,%s\n",ret,strerror(ret));
}
//摧毁属性
pthread_attr_destroy(&attr);
return 0;
}
4.7 创建多个子线程
#include<stdio.h>
#include<unistd.h>
#include<pthread.h>
void *thr(void *arg){
int num=(int)arg;
printf("I am %d thread, self=%lu\n", num, pthread_self());
return (void*)(100+num);
}
int main(){
pthread_t tid[5];
for(int i=0; i<5; i++){
pthread_create(&tid[i], NULL, thr, (void*)i);
}
for(int i=0; i<5; i++){
void *ret;
pthread_join(tid[i], &ret);
printf("i =%d, ret=%d\n", i, (int)ret);
}
return 0;
}
5. 线程使用注意事项
- 主线程退出其他线程不退出,主线程应调用pthread_exit。
- 避免僵尸进程
pthread_join
pthread_detach
pthread_create指定分离属性
被join线程可能在join函数返回前就释放完自己的所有内存资源,所以不应当返回被回收线程栈中的值。 - malloc和mmap申请的内存可以被其他线程释放。
- 应避免在多线程模型中调用fork,除非马上exec,子进程中只有调用fork的线程存在,其他线程在子进程中均pthread_exit
- 信号的复杂语义很难和多线程共存,应避免在多线程引入信号机制。
- 编译要指定
-lpthread