线程的概念与创建
1、线程的概念:
线程是进程内部的一条执行序列(执行流),一个进程可以包含多个线程,
将main函数执行的线程称为主线程,其他的线程称之为函数线程。
- main函数: 是进程执行的入口,函数执行的第一个
- 函数线程:创建线程时,需要指定线程的执行序列(一组有序指令--》函数)
//线程是进程内部的一条执行序列或执行的路径,一个进程可以包含多条线程C语言如何组织一条指令 ---》 函数
函数调用和线程函数的区别:
- 一般的函数调用:
int main()
{
fun();
}
// main方法和fun方法是串行执行的
- 线程函数:
void *fun(void *arg)
int main()
{
pthread_create(fun); // pthread_create此处为函数调用
}
//此处的fun只是给定函数地址来指定创建的线程从哪个函数开始执行
(main创建的线程和函数线程是并发关系)
2、线程的实现方式:
一是让进程自己来管理线程;
二是让操作系统来管理线程。
由进程自己管理就是用户态线程实现,由操作系统管理的就是内核态线程实现。
注:进程是在CPU上实现并发(多道线程),而CPU由操作系统管理的,因此,进程的实现只能由操作系统内核来进行,而不存在用户态实现的情况。但线程不同,因为线程是进程内部的东西,存在有进程直接管理线程的可能性,所以就有线程内核态和用户态实现。
//操作系统角度
用户级线程:就是用户自己做线程的切换,自己管理线程的信息,而操作系统无需知道线程的存在(线程的实现和管理都是在用户态完成,所以用户空间有线程库的存在,包含了线程的创建,线程的管理、及线程的调度和销毁。。。
优点:(1)灵活性。因为操作系统不用知道线程的存在,所以在任何操作系统上都能 应用;
(2)线程切换效率快。因为切换在用户态进行,无需陷入到内核态。
(3)不用修改操作系统,实现容易。
缺点:(1)如果一个线程阻塞,则会造成整个进程的阻塞
(2)用户程序就会相对复杂些,(写程序时必须仔细斟酌什么时候让出CPU什么时候占据CPU)
(3)用户态线程实现无法完全达到线程提出所要达到的目的:进程级多道编程。
内核级线程:由操作系统来管理线程,作用是保持维护线程的各种资料,即将线程控制块存放在操作系统内核空间。
优点:(1)用户编程比较简单,因为线程的复杂性由操作系统承担,用户程序员在编程时无需管理线程的调度,即无需担 心线程什么时候会执行和挂起;
(2)如果一个线程执行阻塞操作,操作系统可以从容调度另外一个线程执行。因为操作系统能够监视所有的线程。
缺点:(1)切换效率低(因为线程在内核态实现,每次线程的切换都要陷入到内核,由操作系统进行调度,从用户态切换 到内核态是要花费时间的);
(2)占用内核稀缺的内存资源。
混合级线程:用户态的执行系统负责进程内部线程在非阻塞时的切换;内核态的操作系统负责阻塞线程的切换,即我们同时实现内核态和用户态线程管理。
Linux系统的线程实现方式:(参考Linux内核设计与实现)
Linux实现线程的机制十分独特,从内核角度上说,它并没有线程这个概念,Linux把所有的线程都当做进程来实现。内核并没有准备特别的调度算法或是定义特别的数据结构来表示线程。相反,线程仅仅被视为一个与其他进程共享某些资源的进程。每一个线程都拥有唯一隶属于自己的task_struct,所以在内核中,它看起来就像一个普通的进程(只是线程和其他一些进程共享某些资源,如地址空间)
3、线程的创建
(1)概念: 线程的创建和普通进程的创建类似,只不过再调用clone()的时候需要传递一些参数标志来指明需要共享的资源:
clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, 0);
以上代码产生的结果和调用fork()差不多,只是父子俩共享地址空间、文件系统资源、文件描述符和信号处理程序。(也就是 新建的进程和它的父进程就是流行的所谓的线程)
注:传递给clone()的参数标志决定了新创建进程的行为方式和父子进程之间共享资源的种类。
4、进程与线程的区别:(面试重点内容)
- 进程是执行中的一段程序,即一旦程序被载入到内存中并准备执行,它就是一个进程。进程是表示资源分配的基本概念。
线程:单个进程中执行中的每个任务就是一个线程
- 一个线程只能属于一个进程,但是一个进程可以拥有多个线程。多线程处理就是允许一个进程中在同一时刻执行多个任务。
- 进程是资源分配的最小单位;线程是CPU调度的最小单位
(CPU执行的最小单位是指令)
- 进程有自己的独立地址空间;线程共享进程中的地址空间
- 进程的创建消耗资源大;线程的创建相对较小
- 进程的切换开销大;线程的切换开销相对较小
(同一个进程中的线程切换)
5、Linux上线程库的使用:
int pthread_create(pthread_t *id,pthread_attr_t *attr, void*(*pthread_fun(void*),void *arg);
- id:传递pthread_t类型变量的地址,用于返回创建的线程的ID
- attr:线程属性,一般用默认属性直接传递空NULL
- pthread_fun:传递线程函数的地址(用户自己实现的一个函数)
- arg:传递给线程函数的参数
- 返回值:成功返回0,失败返回错误码
Main.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <assert.h>
#include <pthread.h>
void *fun(void*);
int main()
{
pthread_t id;
//fun并不是函数调用,仅仅是给了一个函数的地址
int res = pthread_create(&id, NULL,fun,NULL);
assert(res == 0);
int i = 0;
for( ; i<5; ++i)
{
printf("main running\n");
sleep(1);
}
exit(0);
}
void *fun(void *arg)
{
int i = 0;
for(; i<3; ++i)
{
printf("fun running\n");
sleep(1);
}
}
编译时指定路径编译(-lpthread)
6、给线程函数传递参数:
void *
(1)传递一个小于等于4字节的值类型
(2)传递一个地址 test.c(将a的地址传递给fun方法)
地址传递中函数线程和传递参数的线程共享传递的变量
(所映射的物理空间相同)
进程 在程序中 分配的是虚拟地址空间
变量 通过页表(一个页表维护一个进程) 映射到物理内存空间
- 要共享地址空间的就用 地址传递; 小于等于四字节的就用 值传递
注:线程之间除了地址空间共享,还有哪些是共享的:
进程: .text(RDONLY) .data .heap .stack 内核(文件描述符(共享))
结论:同一个进程中线程共享.data .heap .text 文件描述符(无论什么时候打开)
(文件描述符传递标志CLONE_FILES:父子进程共享打开的文件);因为共享,所以释放时只free一次即可
其他方法:
//结束一个线程并且设置线程结束的一些信息
int pthread_exit(void *result);
注://exit(0)结束一个进程,
pthread_exit(NULL);//(最好用线程结束的方法结束线程)
//main方法中结束时,需要调用此方法,防止main方法执行完成直接结束掉整个进程
//等待(阻塞)一个ID指定的线程结束,result获取ID线程结束时设置的结束信息
int pthread_join(pthread_t id,void **result);
**result:传递指针变量的地址