1、线程与进程
进程:一个正在执行的程序,是资源分配的最小单位
1)进程中的事情需要按照一定的顺序逐个执行,那么如何让一个进程中的一些事情同时执行?
2)进程出现了很多弊端:一是由于进程是资源拥有者,创建、撤销与切换存在较大的时空开销,因此需要引入轻量级进程;二是由于多处理器(SMP)出现,可以满足多个运行单位,而多个进程并行开销过大。
线程:又称为轻量级进程,程序执行的最小单位,系统调度和分配cpu的基本单位,他是进程中的一个实体。一个进程中可以有多个线程,这些线程共享进程的所有资源,线程本身只包含一点必不可少的资源。
进程负责申请资源,从主函数开始就是一个线程了。
进程申请资源,多线程中的各个线程共享线程中的资源
进程申请资源,多进程使用fork()函数来进行创建子进程
1、fork拷贝会消耗资源
2、进程间通信还要经过管道,消息队列,信号量来进行通信比较复杂
进程与线程之间的区别:
1)进程有自己独立的地址空间,多个线程共用同一个地址空间
2)线程更加节省系统资源
3)在一个地址空间中多个线程共享,每个线程都有属于自己的栈区
4)每一个地址空间中多个线程独享,代码区、堆区、数据区、打开的文件(文件描述符)都是线程共享的
5)每个进程对应一个虚拟地址空间,一个进程只能抢一个CPU时间片
6)在一个地址空间中可以划分出多个线程,在有效的资源基础上,能够抢到更多的CPU时间片
CPU调度和切换:线程上下文切换比进程要快的多
上下文切换:进程/线程分时复用CPU时间片,在切换之前会将上一次任务的状态进行保存,下次切换回这个任务的时候,加载这个状态继续执行,任务从保存到再次加载的这个过程就是一次上下文切换。
进程更加廉价,启动速度更快,退出也快,对系统资源的冲击较小
在处理多任务程序的时候使用多线程比使用多进程要更有优势,但是并不是线程并不是越多越好。
2、线程的一些术语
1、并发是指同一时刻,只有一条指令执行,但多个进程指令被快速轮换执行,使得在宏观上具有多个进程同时执行的效果。看起来同时发生,针对单核处理器的
2、并行是指在同一时刻,有多条指令在多个处理器上(cpu)同时执行。真正的同时发生
3、同步:彼此有依赖关系的调用不应该 “同时发生”,而同步就是要阻止那些 “同时发生” 的事情
4、异步:异步的概念与同步相对,任何两个彼此独立的操作是异步的,它表明事情独立的发生
多线程的优势:
1、在多处理器中开发程序的并行性
2、在等待IO操作时,程序可以执行其他操作,提高并发性
3、模块化的编程,能更清晰的表达程序中独立事件的关系,结构清晰
4、占用较少的系统资源,相对于多进程而言
5、多线程不一定要多处理器,多处理器只是提高了并行性
3、线程创建函数
创建出来的是子线程,当单进程程序中创建线程的时候,进程便退化成了主线程。线程与进程的标识符类型、获取id的函数、线程创建函数分别如下图所示:
![](https://i-blog.csdnimg.cn/blog_migrate/0c69d81805097e49d213eb07903ca682.png)
pthread_create函数的使用如下:
#include<pthread.h>
int pthread_create(pthread_t *tidp, const pthread_attr_t *attr,
( void *)(*start_rtn)( void *), void *arg);
参数说明:
thread:传出参数、是无符号长整型数,会将线程id写到这个指针指向的内存中
attr:线程属性,一般为空
start_rtn:是线程运行函数的起始地址
arg:运行函数的参数
返回值:
线程创建成功,则返回0,创建失败返回错误参数
编译方法:
-lpthread //pthread为动态库
例如:
gcc test.c -lpthread -o test
测试demo:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
void print_id(char *s)
{
printf("%s :pid is %u tid is %ld\n", s, getpid(), pthread_self());
}
void* callback(void *arg)
{
print_id(arg);
for (int i = 0; i < 5; i++) {
printf("子线程: i = %d\n", i);
}
printf("子线程: %ld\n", pthread_self());
return (void *)0;
}
int main()
{
pthread_t tid;
pthread_create(&tid, NULL, callback, "new thread");
for (int i = 0; i < 5; i++) {
printf("主线程: i = %d\n", i);
}
printf("pid is %u tid is %ld\n", getpid(), pthread_self());
printf("主线程: %ld\n", pthread_self());
// sleep(2);
return 0;
}
执行结果一
主线程: i = 0
主线程: i = 1
主线程: i = 2
主线程: i = 3
主线程: i = 4
pid is 23801 tid is 139896767649536
主线程: 139896767649536
当我们打开sleep(2)的注释,执行结果二如下:
主线程: i = 0
主线程: i = 1
主线程: i = 2
主线程: i = 3
主线程: i = 4
pid is 23892 tid is 140477647664896
主线程: 140477647664896
new thread :pid is 23892 tid is 140477639358208
子线程: i = 0
子线程: i = 1
子线程: i = 2
子线程: i = 3
子线程: i = 4
子线程: 140477639358208
解释:
程序从main函数开始执行,执行到pthread_create函数时,会创建callback的子线程执行callback函数里面的相关代码,同时main函数里面也继续向下执行,main函数执行完毕后就会释放掉相关虚拟地址空间资源,这时候callback子线程还没有运行完,这时就会执行出现结果一。当我们在主线程中sleep(2);就可以延长虚拟地址空间的生命周期,就可以正常执行完子线程的相关内容了。
4、线程退出函数
在编写多线程程序时,如果想让线程退出,但是不会导致虚拟地址空间资源的释放,我们就可以调用线程库中的线程退出函数,只要调用该函数当前线程就立马退出了,并且不会影响到其他线程的正常运行,不管是子线程还是主线程中都适用。
#include <pthread.h>
void pthread_exit(void *retval);
参数说明:
retval:void*类型的指针,指向的数据将作为线程退出时的返回值。如果线程不需要返回任何数据,直接设为NULL即可
测试demo:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
void print_id(char *s)
{
printf("%s :pid is %u tid is %ld\n", s, getpid(), pthread_self());
}
void* callback(void *arg)
{
print_id(arg);
for (int i = 0; i < 5; i++) {
printf("子线程: i = %d\n", i);
}
printf("子线程: %ld\n", pthread_self());
return (void *)0;
}
int main()
{
pthread_t tid;
pthread_create(&tid, NULL, callback, "new thread");
printf("pid is %u tid is %ld\n", getpid(), pthread_self());
printf("主线程: %ld\n", pthread_self());
pthread_exit(NULL);
return 0;
}
执行结果:
pid is 25055 tid is 140031551674112
主线程: 140031551674112
new thread :pid is 25055 tid is 140031543367424
子线程: i = 0
子线程: i = 1
子线程: i = 2
子线程: i = 3
子线程: i = 4
子线程: 140031543367424
将第三点创建线程中的例子对比可以看出,pthread_exit函数将主线程退出了,但是并没有释放虚拟地址空间资源。
5、线程回收函数
线程和进程一样,子线程退出的时候其内核资源主要由主线程回收,线程库中提供的线程回收函数是pthread_join(),这个函数是一个阻塞函数,如果还有子线程运行,调用该函数就会阻塞,子线程退出函数解除阻塞进行资源的回收,函数被调用一次,只能回收一个子线程,如果有多个子线程则需要循环进行回收操作。
另外通过线程回收函数还可以获取到子线程退出时传递出来的数据,如下:
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
参数说明:
thread:等待退出线程的线程号
retval:退出线程的返回值,二级指针,是一个传出函数,这个地址中存储了pthread_exit()传递出的数据,如果不需要,可以为NULL
返回值:线程回收成功返回0,回收失败返回错误号
测试demo1:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
struct test
{
int num;
int age;
};
struct test t; // 全局变量多线程共享
void print_id(char *s)
{
printf("%s :pid is %u tid is %ld\n", s, getpid(), pthread_self());
}
void* callback(void *arg)
{
print_id(arg);
for (int i = 0; i < 5; i++) {
printf("子线程: i = %d\n", i);
}
// struct test t; 栈区被释放了,所以test不能是局部变量,定义1
t.num = 100;
t.age = 50;
pthread_exit(&t);
printf("子线程: %ld\n", pthread_self());
return (void *)0;
}
int main()
{
pthread_t tid;
pthread_create(&tid, NULL, callback, "new thread");
printf("pid is %u tid is %ld\n", getpid(), pthread_self());
printf("主线程: %ld\n", pthread_self());
void *ptr; // ptr指向pthread_exit退出时t的地址
pthread_join(tid, &ptr);
struct test *pt = (struct test*)ptr;
printf("num: %d, age = %d\n", pt->num, pt->age);
return 0;
}
执行结果:
pid is 26531 tid is 139762155865856
主线程: 139762155865856
new thread :pid is 26531 tid is 139762147559168
子线程: i = 0
子线程: i = 1
子线程: i = 2
子线程: i = 3
子线程: i = 4
num: 100, age = 50
分析:test t不能定义在1处,因为test t是子线程里面的栈内存,在一块虚拟内存空间中,只有一个栈,这个栈被多个子线程均分了,一个子线程退出,子线程所使用的栈就被释放了,我们取出来的其实是随机数。如果保证数据正确呢?要保证这块地址不被释放就可以了,可以使用堆内存,也可以是全局变量,因为多线程共享全局数据区和堆区,只要保证多个线程能够访问这块内存就可以了。
测试demo2:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
struct test
{
int num;
int age;
};
// struct test t; // 全局变量多线程共享
void print_id(char *s)
{
printf("pid is %u tid is %ld\n", getpid(), pthread_self());
}
void* callback(void *arg)
{
print_id(arg);
for (int i = 0; i < 5; i++) {
printf("子线程: i = %d\n", i);
}
struct test*t = (struct test*)arg;
// struct test t; 栈区被释放了,所以test不能是局部变量
t->num = 100;
t->age = 50;
pthread_exit(t);
printf("子线程: %ld\n", pthread_self());
return (void *)0;
}
int main()
{
struct test t;
pthread_t tid;
pthread_create(&tid, NULL, callback, &t);
printf("pid is %u tid is %ld\n", getpid(), pthread_self());
printf("主线程: %ld\n", pthread_self());
void *ptr; // ptr指向pthread_exit退出时t的地址
pthread_join(tid, &ptr);
struct test *pt = (struct test*)ptr;
printf("num: %d, age = %d\n", pt->num, pt->age);
return 0;
}
执行结果:
pid is 27333 tid is 140069782001408
主线程: 140069782001408
pid is 27333 tid is 140069773694720
子线程: i = 0
子线程: i = 1
子线程: i = 2
子线程: i = 3
子线程: i = 4
num: 100, age = 50
分析:多个线程把栈空间给平分了,多线程中不能访问相应的栈空间的,但是主动将主线程的栈空间传递给子线程,那么就可以访问了,主线程和子线程都是同一个虚拟地址空间的,所以可以访问到。当子线程释放掉,主线程栈空间还是存在的,调用的时候是可以正常执行的。
6、线程分离函数
一般情况下,程序中的主线程有属于自己的处理流程,如果让主线程负责子线程的资源回收,调用pthread_join()只要子线程不退出就会被一直阻塞,那么主线程的任务也不能被执行了
在线程库函数中为我们提供了线程分离函数pthread_detach(),调用这个函数之后指定的子线程可以跟主线程分离,当子线程退出的时候,其占用的内核资源就被系统的其他进程接管并回收了。线程分离之后主线程中使用pthread_join()就回收不到子线程资源了。
#include <pthread.h>
int pthread_detach(pthread_t thread);
参数解释:
thread:线程id
测试demo:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
struct test
{
int num;
int age;
};
void print_id(char *s)
{
printf("pid is %u tid is %ld\n", getpid(), pthread_self());
}
void* callback(void *arg)
{
print_id(arg);
for (int i = 0; i < 5; i++) {
printf("子线程: i = %d\n", i);
}
struct test*t = (struct test*)arg;
t->num = 100;
t->age = 50;
pthread_exit(t);
printf("子线程: %ld\n", pthread_self());
return (void *)0;
}
int main()
{
struct test t;
pthread_t tid;
pthread_create(&tid, NULL, callback, &t);
printf("pid is %u tid is %ld\n", getpid(), pthread_self());
printf("主线程: %ld\n", pthread_self());
pthread_detach(tid);
pthread_exit(NULL);
return 0;
}
执行结果:
pid is 28021 tid is 140366746593024
主线程: 140366746593024
pid is 28021 tid is 140366738286336
子线程: i = 0
子线程: i = 1
子线程: i = 2
子线程: i = 3
子线程: i = 4
分析:使用pthread_detach函数就可以直接将子线程与主线程的分离,子线程函数执行完后退出,由内核的函数自动回收,主线程也不会被阻塞。
7、线程取消函数
线程取消函数的意思就是在某些特定的情况下在一个线程中杀死另外一个线程,使用这个函数杀死另外一个线程需要分两步:
1)在线程A中调用线程取消函数pthread_cancel,指定取消线程B,这是B是被取消不了的
2)线程B进行一次系统调用(用用户态切换到内核态),否则线程B可以一直运行
#include <pthread.h>
int pthread_cancel(pthread_t pid);
参数:线程号
返回值:成功返回0,失败返回错误码
第二点比如执行printf进行打印,printf最终会写终端,因此在底层会调用read方法。
8、线程ID比较函数
在linux中线程ID本质就是一个无符号长整形,因此可以直接使用比较操作符比较两个线程的ID:
#include <pthread.h>
int pthread_equal(pthread_t t1, pthread_t t2);
返回值:ID相等返回值不等于0,不相等返回值等于0