线程的基础概念与线程创建
我们在之前了解了什么是进程,那么什么是线程呢?
- 进程:执行中的程序,程序是存储在磁盘上的可执行文件,是静态的概念,而进程是动态的概念,进程可以看作一组有序指令+数据+资源的集合。
- 线程:我们将线程看作轻量级的进程,线程是进程内部的一条执行序列,一个进程中至少有一条线程,也就是main主函数所代表的执行序列,称之为主线程,通过可以通过线程库创建线程(函数线程)。
线程的实现方式分为三种:
①:用户级线程:线程的实现在用户态实现,对于内核,只能识别其是一个进程。
线程的管理由用户级实现,内核实现简单,不需要实现线程。
优点:切换速度快
缺点:1.用户代码复杂 2.当一条线程阻塞,会造成整个进程阻塞
②:内核级线程:(Linux目前支持)线程的实现在内核态实现,线程创建,调度,销毁等管理是由操作系统支持的。
优点:1.用户代码简单 2.一条线程阻塞,操作系统会立马切换到另一条线程,整个进程不会阻塞。
缺点:1.内核实现复杂,需要支持多线程。 2.线程切换效率低,线程每次切换必须陷入内核
③:混合级线程:结合上述两个,集齐优点
我们用图来直观表示其三者的区别:
接下来我们看一看线程的创建(也就是线程库的使用):
①:线程创建函数:pthread_create()
函数原型:int pthread_create(pthread_t *id, pthread_attr_t *attr, \
void *(*pthread_fun)(void *), void *arg);
我们来看一看其中的参数的意义:
- id:create函数返回创建的线程的id,通过第一个参数“id”进行接收
- attr:传入线程的属性,默认属性传NULL
- pthread_fun:线程库创建的函数线程(相当于创建的线程需要执行的函数)
- arg:传递给函数线程所执行的函数的实参
- 返回值:成功返回0,失败返回错误码
②:线程的结束函数:pthread_exit
函数原型:int pthread_exit(void *reval);
这个函数会结束当前线程
- reval:可以通过这个参数将线程结束时像传递的信息传递出来,可以通过pthread_join()函数中的val参数进行接收。
- 返回值:成功返回0,失败返回错误码
③:等待线程结束的函数:pthread_join
int pthread_join(phtread_t tid, void **val);
调用方会阻塞,直到等待的tid号线程结束,获取结束的线程的退出信息
- tid:线程的id
- val:将函数线程传递出的信息,可用这个输出参数进行接收
- 返回值:成功返回0,失败返回错误码
④:主动结束函数:pthread_cancel
int pthread_cancel(pthread_t tid);
线程可以通过调用pthread_cancel函数来请求取消同一进程中的其他线程
这个函数会根据传入的tid,直接发送取消信号给这个线程。
- tid:线程的id
- 返回值:成功返回0,失败返回错误码
最后我们这里需要注意一点,pthread_create()线程创建的时候,第四个参数arg的传参方式:
分为两种:
①:值传递方式:
- 只能应用于小于等于4字节的数据,因为强转前其本质是个指针,只有4字节大小空间
- 我们传参时需要先将变量数值强转为void *,进行传值
②:地址传递方式:
- 将变量地址转化为void * 很危险
- 函数线程中通过传递对变量的修改,会造成主线程值也随之修改
- 而主线程对变量的修改,也会创造函数线程中值的修改
③:为了安全,函数线程一开始,就将传进来的值自己拷贝一份,之后修改都修改自己拷贝的值
最后,我们来看一看线程的数据共享并与进程做对比:
我们首先要知道进程哪些数据是共享的,哪些是不共享的:
我们在之前对fork的底层实现过程进行过总结,可以知道fork出的子进程对父进程的已打开文件的控制结构file_struct会通过copy_files()函数进行拷贝,所以父子进程会共享文件描述符,以及文件偏移量,而全局数据,栈区数据,堆区数据是不共享的。
我们用下面的这张图来直观表示:
而线程的哪些数据是共享的,哪些是不共享的:
我们知道同一个进程中每个线程都共享进程的所有资源,除了栈区。
所以结论就是:同一个进程中每个线程都有自己独立的栈区,而其他资源,比如全局数据,堆区数据,PCB(文件表数组)都是共享的。所以线程间通讯可以很简单的通过全局,堆区,文件来通讯。
我们用下面的这张图来直观表示:
至此,线程基础知识了解完毕。