关于线程和进程的区别:进程和线程的详解和区别_StudyWinter的博客-CSDN博客_任务进程线程的区别
1 什么是线程
(1)轻量级进程(light-weight process),也有 PCB,创建线程使用的底层函数和进程一样,都是 clone;
(2)从内核里看进程和线程是一样的,都有各自不同的 PCB,但是 PCB 中指向内存资源的三级页表是相同的(下图区别进程)
(3)进程可以蜕变成线程;
(4)线程可看做寄存器和栈的集合;
(5)在 linux 下,线程最是小的执行单位;进程是最小的分配资源单位
参考:《Linux 内核源代码情景分析》
对于进程来说,相同的地址(同一个虚拟地址)在不同的进程中,反复使用而不冲突。原因是他们虽虚拟址一样,但,页目录、页表、物理页面各不相同。相同的虚拟址,映射到不同的物理页面内存单元,最终访问不同的物理页面。但!线程不同!两个线程具有各自独立的 PCB,但共享同一个页目录,也就共享同一个页表和物理页面。所以两个 PCB 共享一个地址空间。实际上,无论是创建进程的 fork,还是创建线程的 pthread_create,底层实现都是调用同一个内核函数clone。
如果复制对方的地址空间,那么就产出一个“进程”;如果共享对方的地址空间,就产生一个“线程”。因此:Linux 内核是不区分进程和线程的。只在用户层面上进行区分。所以,线程所有操作函数pthread_* 是库函数,而非系统调用。
线程概念:
进程:有独立的 进程地址空间。有独立的pcb。 分配资源的最小单位。
线程:有独立的pcb。没有独立的进程地址空间。 最小单位的执行。
ps -Lf 进程id ---> 线程号。LWP --》cpu 执行的最小单位。
ps -Lf 进程号 # 查看进程的线程
打开浏览器
2 线程共享资源
1. 文件描述符表
2. 每种信号的处理方式
3. 当前工作目录
4. 用户 ID 和组 ID
5. 内存地址空间 (.text/.data/.bss/heap/共享库)
3 线程间非共享资源
1. 线程 id
2. 处理器现场和栈指针(内核栈)
3. 独立的栈空间(用户空间栈)
4. errno 变量
5. 信号屏蔽字
6. 调度优先级
4 线程的优缺点
优点:
1. 提高程序并发性
2. 开销小
3. 数据通信、共享数据方便
缺点:
1. 线程不稳定(第三方库函数实现)
2. 线程调试困难
3. 等待使用共享资源时造成程序运行速度变慢,主要是一些独占性的资源
4. 线程的死锁,较长时间的等待或者资源竞争造成死锁
5 线程控制原语
编译的时候记得后面 -l pthread 毕竟第三方库实现。
5.1 pthread_self 函数
作用:获取线程 ID。其作用对应进程中 getpid() 函数。
pthread_t pthread_self(void);
// 成功返回本线程id
5.2 pthread_create 函数
创建一个新线程,其作用,对应进程中fork()函数。
int pthread_create(pthread_t *thread,
const pthread_attr_t *attr,
void *(*start_routine) (void *),
void *arg);
// 成功返回0,失败返回errno
// 参数一:表示传出参数,表示创建的子线程id
// 参数二:线程属性,传NILL表使用默认属性
// 参数三:函数指针,指向线程主函数(线程体),该函数运行结束,则线程结束。
// 参数四:参数三函数的参数,空传NULL
测试
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
// 子线程回调函数
void *tfn(void *arg)
{
printf("pthread: pid = %d, tid = %lu\n", getpid(), pthread_self());
return NULL;
}
int main(int argc, char *argv[])
{
pthread_t tid;
printf("main: pid = %d, tid = %lu\n", getpid(), pthread_self());
int res = pthread_create(&tid, NULL, tfn, NULL); // 创建一个线程
if (res < 0)
{
perror("pthread_create error\n");
exit(1);
}
return 0;
}
执行
gcc test.c -l pthread
可以看到,子线程的打印信息并未出现。原因在于,主线程执行完之后,就销毁了整个进程的地址空间,于是子线程就无法打印。简单粗暴的方法就是让主线程睡1秒,等子线程执行。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
// 子线程回调函数
void *tfn(void *arg)
{
printf("pthread: pid = %d, tid = %lu\n", getpid(), pthread_self());
return NULL;
}
int main(int argc, char *argv[])
{
pthread_t tid;
printf("main: pid = %d, tid = %lu\n", getpid(), pthread_self());
int res = pthread_create(&tid, NULL, tfn, NULL); // 创建一个线程
if (res < 0)
{
perror("pthread_create error\n");
exit(1);
}
sleep(1); // 在这里添加休眠
return 0;
}
执行
5.3 循环创建多个子线程
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
// 子线程回调函数
void *tfn(void *arg)
{
int i = (int)arg;
sleep(i);
printf("-----I'm %d th thread: pid = %d, tid = %lu\n", i + 1,
getpid(), pthread_self());
return NULL;
}
int main(int argc, char *argv[])
{
int i;
int res;
pthread_t tid; // 线程id
for (i = 0; i < 5; i++)
{
res = pthread_create(&tid, NULL, tfn, (void *)i); // 创建线程
if (res != 0)
{
perror("pthread_create error\n");
exit(1);
}
}
printf("-------main: pid = %d, tid = %lu\n", getpid(), pthread_self());
sleep(i);
return 0;
}
执行
编译时会出现类型强转的警告。
5.4 线程间全局变量
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
int var = 10;
// 子线程回调函数
void *tfn(void *arg)
{
var = 100;
printf("pthread: pid = %d, tid = %lu\n", getpid(), pthread_self());
return NULL;
}
int main(int argc, char *argv[])
{
printf("At first var = %d\n", var);
pthread_t tid; // 子线程id
int res = pthread_create(&tid, NULL, tfn, NULL);
if (res != 0)
{
perror("pthread error\n");
exit(1);
}
sleep(1);
printf("After pthread_create, var = %d\n", var);
return 0;
}
执行
可以看到,子线程里更改全局变量后,主线程里也跟着发生变化。
5.5 pthread_exit退出
作用:将单个线程退出。
void pthread_exit(void *retval);
// 参数:retval 表示线程退出状态,通常传 NULL
比较
exit(); // 退出当前进程。
return: // 返回到调用者那里去。
pthread_exit(): // 退出当前线程。
重点
在多线程的回调函数中加代码
1 exit函数
// 如果在回调函数里加一段代码:
if(i == 2)
{
exit(0);
}
执行
看起来好像是退出了第三个子线程,然而运行时,发现后续的4,5也没了。这是因为,exit是退出进程。
2 return
if(i == 2)
{
return NULL;
}
执行
这样运行一下,发现后续线程不会凉凉,说明return是可以达到退出线程的目的。
然而真正意义上,return是返回到函数调用者那里去,线程并没有退出。
再修改一下,再定义一个函数func,直接返回那种
void *func(void){
return NULL;
}
if(i == 2)
{
func();
}
执行
运行,发现1,2,3,4,5线程都还在,说明没有达到退出目的。
再次修改
void *func(void) {
pthread_exit(NULL);
return NULL;
}
if(i == 2)
{
func();
}
执行
编译运行,发现3没了,看起来很科学的样子。
pthread_exit表示将当前线程退出。放在函数里,还是直接调用,都可以。
5.6 pthread_join 函数(重)
作用:阻塞等待线程退出,获取线程退出状态其作用,对应进程中 waitpid() 函数;
补充:任意线程得到其他线程的pid都可以回收,没有父线程回收子线程的说法。而进程需要父进程回收子进程。
int pthread_join(pthread_t thread, void **retval);
// 阻塞 回收线程。
// thread: 待回收的线程id
// retval:传出参数。 回收的那个线程的退出值。
// 线程异常借助,值为 -1。
// 返回值:成功:0
// 失败:errno
下面这个是回收线程并获取子线程返回值的小例子:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
struct thrd
{
int var;
char str[256];
};
// 回调函数
void *tfn(void *arg)
{
struct thrd *tval;
tval = malloc(sizeof(struct thrd)); // 申请空间
tval->var = 100;
strcpy(tval->str, "hello thread"); // 拷贝数据
return (void *)tval;
}
int main(int argc, char *argv[])
{
pthread_t tid;
struct thrd *retval;
int res = pthread_create(&tid, NULL, tfn, NULL);
if (res != 0)
{
perror("pthread_create error\n");
exit(1);
}
res = pthread_join(tid, (void **)(&retval));
if (res != 0)
{
perror("pthread_join error\n");
exit(1);
}
printf("Child thread exit with var = %d, str = %s\n", retval->var, retval->str);
pthread_exit(NULL);
}
执行
使用pthread_join函数将循环创建的多个子线程回收
这里tid要使用数组来存
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
int var = 100;
void *tfn(void *arg)
{
int i;
i = (int)arg;
if (i == 1)
{
var = 111;
printf("I'm %dth pthread tid = %lu, var = %d\n", i,
pthread_self(), var);
return (void *)var;
}
else if (i == 3)
{
var = 333;
printf("I'm %dth pthread tid = %lu, var = %d\n", i,
pthread_self(), var);
return (void *)var;
}
else
{
printf("I'm %dth pthread tid = %lu, var = %d\n", i,
pthread_self(), var);
return (void *)var;
}
return NULL;
}
int main(int argc, char *argv[])
{
pthread_t tid[5];
int i;
int *res[5];
// 循环创建多个子进程
for (i = 0; i < 5; i++)
{
pthread_create(&tid[i], NULL, tfn, (void *)i);
}
// 循环回收多个子进程
for (i = 0; i < 5; i++)
{
pthread_join(tid[i], (void **)(&res[i]));
printf("--------------%d 's res = %d\n", i, (int)(res[i]));
}
// 输出主线程
printf("I'm main pthread tid = %lu, var = %d\n", pthread_self(), var);
return 0;
}
执行
5.7 pthread_cancel函数
作用:杀死(取消)线程,其作用,对应进程中 kill() 函数。
线程的取消并不是实时的,而有一定的延时。需要等待线程到达某个取消点(检查点)。
取消点:是线程检查是否被取消,并按请求进行动作的一个位置。通常是一些系统调用
creat,open,pause, close,read,write… 执行命令 man 7 pthreads
可以查看具备这些取消点的系统调用列表。也可参阅 APUE.12.7 取消选项小节。可粗略认为一个系统调用(进入内核)即为一个取消点。如线程中没有取消点,可以通过调pthread_testcancel函数自行设置一个取消点。
int pthread_cancel(pthread_t thread);
// 杀死一个线程。 需要到达取消点(保存点)
// thread: 待杀死的线程id
// 返回值:成功:0
// 失败:errno
如果,子线程没有到达取消点, 那么 pthread_cancel 无效。我们可以在程序中,手动添加一个取消点。使用 pthread_testcancel();成功被 pthread_cancel() 杀死的线程,返回 -1.使用pthead_join 回收。
小例子,主线程调用pthread_cancel杀死子线程
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
// 回调函数
void *tfn(void *arg)
{
while (1)
{
printf("thread: pid = %d, tid = %lu\n", getpid(), pthread_self());
sleep(1);
}
}
int main(int argc, char *argv[])
{
pthread_t tid;
int res = pthread_create(&tid, NULL, tfn, NULL); // 创建线程
if (res != 0)
{
fprintf(stderr, "pthread_create error:%s\n", strerror(res));
exit(1);
}
printf("main: pid = %d, tid = %lu\n", getpid(), pthread_self());
sleep(5); // 父线程睡5s
res = pthread_cancel(tid); // 终止线程
if (res != 0)
{
fprintf(stderr, "pthread_cancel error:%s\n", strerror(res));
exit(1);
}
while(1); // 不退出
pthread_exit((void *)0);
}
执行
可以看到,主线程确实kill了子线程。
这里要注意一点,pthread_cancel工作的必要条件是进入内核,如果tfn真的奇葩到没有进入内核,则pthread_cancel不能杀死线程,此时需要手动设置取消点,就是pthread_testcancel()
5.8 pthread_detach 函数
作用:实现线程分离,线程结束后,自动释放资源。无需pthread_join() 回收资源。
int pthread_detach(pthread_t thread);
// 设置线程分离
// thread: 待分离的线程id
// 返回值:成功:0
// 失败:errno
线程分离状态:指定该状态,线程主动与主控线程断开关系。
线程结束后,其退出状态不由其他线程获取,而直接自己自动释放。网络、多线程服务器常用。进程若有该机制,将不会产生僵尸进程。
僵尸进程的产生主要由于进程死后,大部分资源被释放,一点残留资源仍存于系统中,导致内核认为该进程仍存在。也可使用 pthread_create 函数参 2(线程属性)来设置线程分离。
下面这个例子,使用detach分离线程,照理来说,分离后的线程会自动回收:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
// 子线程回调函数
void *tfn(void *arg)
{
printf("pthread: pid = %d, tid = %lu\n", getpid(), pthread_self());
return NULL;
}
int main(int argc, char *argv[])
{
pthread_t tid;
int res = pthread_create(&tid, NULL, tfn, NULL); // 创建一个线程
if (res != 0)
{
// printf("pthread_create error:%s\n", strerror(res));
fprintf(stderr, "pthread_create error:%s\n", strerror(res));
exit(1);
}
res = pthread_detach(tid); // 设置线程分离,分离完的程序可以自动回收
if (res != 0)
{
// printf("pthread_detach error:%s\n", strerror(res));
fprintf(stderr, "pthread_detach error:%s\n", strerror(res));
exit(1);
}
sleep(1);
res = pthread_join(tid, NULL); // 回收子线程
printf("join res = %d\n", res);
if (res != 0)
{
// printf("pthread_join error:%s\n", strerror(res));
fprintf(stderr, "pthread_join error:%s\n", strerror(res));
exit(1);
}
printf("main: pid = %d, tid = %lu\n", getpid(), pthread_self());
pthread_exit((void *)0);
}
这里是最终版,使用fprintf函数和strerror函数。
执行
6 线程进程控制原语比对
进程 | 线程 |
fork | pthread_create |
exit | pthread_exit |
wait | pthread_join |
kill | pthread_cancel |
getpid | pthread_self |
pthread_detach() |
7 线程分离属性设置
线程属性:
设置分离属性。
pthread_attr_t attr;
// 创建一个线程属性结构体变量
pthread_attr_init(&attr);
// 初始化线程属性
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
// 设置线程属性为分离态
pthread_create(&tid, &attr, tfn, NULL);
// 借助修改后的 设置线程属性 创建为分离态的新线程
pthread_attr_destroy(&attr);
// 销毁线程属性
调整线程状态,使线程创建出来就是分离态,代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
// 子线程回调函数
void *tfn(void *arg)
{
printf("pthread: pid = %d, tid = %lu\n", getpid(), pthread_self());
return NULL;
}
int main(int argc, char *argv[])
{
pthread_t tid;
pthread_attr_t attr; // 结构体变量
int res = pthread_attr_init(&attr); // 创建分离
if (res != 0)
{
fprintf(stderr, "pthread_attr_init error :%s\n", strerror(res));
exit(1);
}
if(res != 0)
{
fprintf(stderr, "pthread_attr_init error :%s\n",
strerror(res));
exit(1);
}
res = pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
// 设置线程属性为分离属性
if (res != 0)
{
fprintf(stderr, "pthread_attr_setdetachstate error :%s\n",
strerror(res));
exit(1);
}
res = pthread_create(&tid, &attr, tfn, NULL); // 创建一个线程
if (res != 0)
{
fprintf(stderr, "pthread_create error: %s\n",
strerror(res));
exit(1);
}
res = pthread_attr_destroy(&attr); // 回收分离
if (res != 0)
{
fprintf(stderr, "pthread_attr_destroy error :%s\n",
strerror(res));
exit(1);
}
sleep(1); // 保证子进程结束
res = pthread_join(tid, NULL); // 阻塞回收,分离成功,这里应该回收失败
if (res != 0)
{
fprintf(stderr, "pthread_join error :%s\n", strerror(res));
exit(1);
}
printf("main: pid = %d, tid = %lu\n", getpid(), pthread_self());
pthread_exit((void *)0);
}
执行
如图,pthread_join报错,说明线程已经自动回收,设置分离成功。
8 线程属性注意事项(重)
1 主线程退出其他线程不退出,主线程应调用 pthread_exit
2 避免僵尸线程
pthread_join
pthread_detach
pthread_create 指定分离属性
被 join 线程可能在 join 函数返回前就释放完自己的所有内存资源,所以不应当返回被回收线程栈中的值;
3 malloc 和 mmap 申请的内存可以被其他线程释放
4 应避免在多线程模型中调用 fork 除非,马上 exec,子进程中只有调用 fork 的线程存在,其他线程在子进程中均 pthread_exit
5 信号的复杂语义很难和多线程共存,应避免在多线程引入信号机制。