一、线程介绍
- 线程是轻量级的进程;因为它的资源创建轻巧,调度效率快
- 线程是进程内部的一条执行序列(一组有序指令),或者说是执行流。
- 一个进程至少有一条线程,即就是main函数所代表的执行序列。称之为主线程,通过线程库可以创建线程----函数线程
- 主线程仅仅代表进程执行的第一条线程而已。当主线程通过线程库创建出函数线程以后,所有线程就没有任何区别。
- 主线程默认结束,结束是整个进程。
二、线程和进程的区别
- 进程是资源分配的单位,线程是CPU调度执行的单位;
- 多进程:进程间资源都是独立的,同一进程中的多线程,资源是共享的,除了独立的栈区空间。
- 线程更加轻便,更加小巧,从而造成线程调度,创建、调度、切换效率都比进程高。
三、线程库的使用
#include <pthread.h>
创建:
int pthread_create(pthread_t *id,pthread_attr_t *attr,void * (pthread_fun)(void *), void *arg);
- id: 线程ID,线程创建时分配的线程ID,传递一个变量的地址
- attr: 线程属性,默认为NULL;
- pthread_fun: 线程函数 新创建的线程的执行体;函数地址:指定新线程从那块开始执行。
- arg: 传递给线程函数的参数。
返回值(res == 0);成功返回0,失败返回错误码
创建时给函数线程传参的两种方式:
1、值传递 --最多传递四个字节的数据,---》指针
将传递的值直接强转为void*
arg: 类型是void* 记录的传递的值
2、地址传递 要进行判断,防止后面的函数线程对其值修改
将要传递的值的地址转化为void*
arg: 类型是void* 记录的是传递的地址
四、线程代码
并行--硬件环境 并发---软件执行
我们要加线程库加上,执行代相关码,gcc -o pthread pthread.c -lpthread
代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
void *fun(void * arg)
{
int data = * (int*)arg; //地址传递
//int data = (int)arg; //值传递
int i = 0;
for(; i < 3; ++i)
{
sleep(1);
printf("fun running\n");
}
}
int main()
{
pthread_t id;
int res = pthread_create(&id,NULL,fun,(void*)&data); //不会阻塞
//(void*)data 值传递
assert(res == 0);
int i = 0;
for(; i < 5; ++i)
{
sleep(1);
printf("main running\n");
}
}
结果如下:
由此可看,线程的执行顺序并不确定,谁先执行,谁后执行,取决于系统。
我们结合另一个fork函数,使用strace ./XXX(可执行文件)跟踪查看程序执行中系统的调用函数。来分析下两种的底层调用:
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <string.h>
#include <unistd.h>
int main()
{
pid_t n = fork();
assert(n!= -1);
if(n == 0)
{
sleep(1);
}
else
{
sleep(1);
}
}
结合图:
linux下调用都是一样的,只是传的参数不一样对应做该做的事情,但都是克隆函数,clone()函数。两个函数都是有自己的pid,和栈空间:fork是全新的函数为0,而线程不会直接分配空间,是共享的空间。
将代码改动后,通过地址传递的方法,来看谁先传递;
代码改动为:
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
void *fun(void * arg)
{
int data = *(int*)arg;
printf("%d\n",data);
int i = 0;
for(; i < 3; ++i)
{
sleep(1);
printf("fun running\n");
}
}
int main()
{
int data = 10;
pthread_t id;
int res = pthread_create(&id,NULL,fun,(void*)&data);
assert(res == 0);
data = 20;
int i = 0;
for(; i < 5; ++i)
{
sleep(1);
printf("main running\n");
}
pthread_exit(NULL);
}
运行结果如下:
凭上图,是否就可以证明先是函数线程执行呢?其实不是,当执行多次后,就不一定是谁先执行了。
由此可看出来:
主线程后期对值的修改可能影响函数线程中获取值,函数线程中通过地址对变量修改,也会影响主线程中变量的值。
main函数结束,进程结束会调用exit(0)函数, 线程依赖着进程,进程结束,所有线程随之结束。
例如:在函数线程执行的次数大于主线程时,会发现主线程结束后,函数线程是执行不到它相应的次数的。
接下来我们就需要用线程库中的线程的函数,我给大家介绍几个函数:
1)int pthread_exit(void* reval); 就可以使进程不结束,将函数线程执行完。
2)函数等待线程结束,获取线程退出信息:int(pthread_t id,void **retval); 相当于waitpid()这个函数。
3) 取消一个线程(只是发出取消请求,并不会阻塞):int pthread_cancel(pthread_t id);
五、线程的实现方式
1.用户级线程---》 内核并不支持多线程,多线程是用户态实现,用户代码就必须实现线程的创建,调度,销毁等工作。
优点:内核简单,线程切换速度快(每次切换不需要陷入内核)
缺点:用户代码复杂,如果一条线程阻塞,整个进程都会阻塞。
2.内核级线程
优点:用户简单,线程切换速度慢(每次切换需要陷入内核),一条线程阻塞,可以立刻切换。
缺点:内核代码复杂。
3.混合级线程 :一部分实现在内核,一部分实现在用户,结合了两者的优点,但是比较复杂。
linux中使用的是内核级线程。