从这一期开始,我会分模块的讲述多线程的知识内容,前期有我对于进程相关文章,对于操作系统的学习中,进程和线程相关知识在工作,面试中都是极为重要的内容,需要掌握。
往前内容:
fork函数详解_草东i的博客-CSDN博客_fork()函数https://blog.csdn.net/weixin_51609435/article/details/124849719
进程之间的通信(管道详解)_草东i的博客-CSDN博客https://blog.csdn.net/weixin_51609435/article/details/125946599
进程间的通信终章之【消息队列,信号量,共享内存】_草东i的博客-CSDN博客https://blog.csdn.net/weixin_51609435/article/details/126164984话不多说,我们开始多线程知识的讲解!
什么是线程?
线程
,又被称为轻量级进程,是操作系统CPU调度执行的最小单位
线程就是进程内部的一条执行路径,或者一个执行序列;一个进程必须至少包含一个线程也可以由很多个线程组成,线程间共享进程的所有资源,每个线程有自己的堆栈和局部变量。线程由CPU独立调度执行,在多CPU环境下就允许多个线程同时运行。同样多线程也可以实现并发操作,每个请求分配一个线程来处理。
一个正在运行的软件就是一个进程,一个进程可以同时运行多个任务( 每个运行任务就是一个线程), 可以简单的认为进程是线程的集合。
一个进程可以包含多个线程,main函数执行的线程称为主线程
,主线程是进程执行的入口。在创建线程时,需要指定线程的执行序列,即函数,故称为函数线程
。主线程创建第一个线程,这个线程也可以创建其他线程,这几个线程会并发执行。
我们在实现函数调用时,只需要传入函数名即可。而函数线程,我们需要给定一个函数地址,告诉创建的线程从哪个函数开始执行,
如下:
void* fun(void* arg)
int main()
{
pthread_create(fun);//此处的fun是函数地址,表示创建出来的线程执行这个函数。
}
多线程与单线程
单线程:只有一条线程在执行任务,这种情况在我们日常的生活学习中很少遇到
多线程:创建多条线程同时执行任务。由于各个线程的控制流彼此独立,使得各个线程之间的代码是乱序执行的。由此要引发线程调度和同步问题,在后续的博文中我们会陆续讲解。
注意:多线程并不是在任何情况下都能提升效率,比如说对于单核CPU计算密集型任务。因为线程本身创建和切换的开销;但是对于频繁IO操作的序列,多线程可以有效的并发,比如生产者、消费者问题。
当增加一个线程的时候,增加的额外开销要小于该线程能够消除的阻塞时间,使用多线程才是有必要的。
进程与线程
进程是一个动态的概念,就是一个程序正在执行的过程
二者的区别:
根本区别:进程是操作系统分配资源的最小单位,线程是任务调动和执行的最小单位。
在开销方面:每个进程都有独立的代码和数据空间,程序切换会有较大的开销。线程之间共享代码和数据,每个线程都有自己独立的栈和调度器,线程之间的切换的开销较小。
所处环境:一个操作系统中可以运行多个进程,一个进程中有多个线程同时执行。
内存分配方面:系统在运行时会为每个进程分配内存,系统不会单独为每个线程分配内存。
包含关系:创建进程时系统会自动创建一个主线程由主线程完成,进程中有多线程时,由多线程共同执行完成。
线程的实现
在操作系统中,线程的实现有以下三种方式:
用户态线程实现:
需要用户自己写一个执行系统作为调度,即线程的实现在用户态完成的,由线程库进行线程的创建,销毁等操作。在用户态线程,每一个线程都是同一权限下的,不会出现夺走CPU控制权的现象,就会出现一个线程占用CPU时间过长,故线程之间必须进行合作,对CPU控制权进行分配
内核感知不到线程,只知道它是一个进程,是一种1-n的关系,这也表示该系统不支持线程,只知道有进程存在。
这种用户态线程的优点和缺点:
优点:
灵活性,内核不用知道线程的存在,所以在任何操作系统上都能应用。
线程切换快,因为切换在用户态进行,无需陷入内核态。
不用修改操作系统,实现简单。
缺点:
编程程序变得诡异,因为用户态线程需要合作才能运转,所以我们需要考虑什么时候让出CPU给别的线程使用,这个时机的选择就很难了。
如果一个线程阻塞,操作系统只会看到进程阻塞,故把CPU控制权交给另一个进程,这样则会造成整个进程阻塞。
用户程序相对复杂。
内核态线程实现:
操作系统管理线程,实现内核态线程,要保持维护线程的各种资料,即将线程控制块存放在操作系统内核空间
,这样操作系统内核就同时有进程控制块,和线程控制块
每个用户线程就是一个内核线程,线程的是实现在内核态,内核知道用户态有几条线程,用户态线程和内核态线程数目对应,一种n-n关系。表示线程是由内核支持的。操作系统可以对线程进行各种类似进程的管理,如线程调度,线程的资源分配,出现阻塞进行处理等措施。
内核态线程的优点和缺点:
优点:
- 用户编程保持简单,因为线程的复杂性由操作系统承担,用户程序员在编程时无需管理线程的调度即无需担心执行,挂起等操作。
- 如果一个线程执行阻塞操作,操作系统可以从容的调度另一个线程执行,因为操作能够监控所有的线程。
- 线程之间无需合作得到CPU控制权,因为操作系统通过周期性的时钟中断把控制权夺过来,重新分配。
缺点:
- 会占用内核稀缺的内存资源,一旦内核空间溢出,操作系统将停止运转。
- 切换效率低,每次线程切换都需要陷入内核态。
- 需要修改操作系统,加入线程管理。
组合级线程实现:
用户态和内核态都存在缺陷,现代操作系统使用的是将两者结合起来。用户态的执行系统负责进程内部线程在非阻塞是的切换;内核态的操作系统负责阻塞线程的切换,将这种方式称作组合级线程。
其中内核态线程数量较少,而用户态线程数量多,每个内核态线程可以服务一个或多个用户态线程,换句话说,用户态线程被多路复用到内核态线程上
共有3条线程,将其分为两组,一组2个,一组1个,每一组线程使用一个内核线程 ,这样,该进程使用两个内核线程,形成n-m的关系,
如果一个线程阻塞,则与其一组的线程皆阻塞,但另外一组可以继续运行。
因此在我们分配线程时:
- 将需要执行阻塞操作的线程设为内核态线程。
- 不会阻塞操作的线程设为用户态线程。
这样我们便可以获得用户态,内核态线程的优点,避免缺点。
注意:在不同的平台中,线程的实现是不一样的;
Linux中线程的实现:
Linux 实现线程的机制非常独特。从内核的角度来说,它并没有线程这个概念。
Linux 把所有的线程都当做进程来实现。内核并没有准备特别的调度算法或是定义特
别的数据结构来表征线程。相反,线程仅仅被视为一个与其他进程共享某些资源的进
程。每个线程都拥有唯一隶属于自己的 task_struct,所以在内核中,它看起来就像
是一个普通的进程(只是线程和其他一些进程共享某些资源,如地址空间)
Linux系统中线程的接口
1)pthread_create创建线程
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
pthread_create()用于创建线程
thread: 接收创建的线程的 ID
attr: 指定线程的属性,一般设置线程属性为NULL;
start_routine: 指定线程函数,这个线程函数的参数为void *,返回值也为void *; 这是一个函数指针;
arg: 给线程函数传递的参数(线程刚启动,线程函数的参数为void*,给它传参就是 void*)
//成功返回 0, 失败返回错误码
2)pthread_join
int pthread_join(pthread_t thread, void **retval);
pthread_join()等待 thread 指定的线程退出,线程未退出时,该方法阻塞//有点像
父进程等待子进程结束的wait,或者说合并线程;
retval:接收 thread 线程退出时,指定的退出信息
3)pthread_exit
int pthread_exit(void *retval);
pthread_exit()退出线程
retval:指定退出信息
4)pthread_detach()线程分离
在某些情况下,程序中的主线程有属于自己的业务处理流程,如果让主线程负责子线程的资源回收,调用 pthread_join() 只要子线程不退出主线程就会一直被阻塞,主要线程的任务也就不能被执行了。
在线程库函数中为我们提供了线程分离函数 pthread_detach(),调用这个函数之后指定的子线程就可以和主线程分离,当子线程退出的时候,其占用的内核资源就被系统的其他进程接管并回收了。线程分离之后在主线程中使用 pthread_join() 就回收不到子线程资源了。
#include <pthread.h> // 参数就子线程的线程ID, 主线程就可以和这个子线程分离了 int pthread_detach(pthread_t thread);
线程的创建使用
示例1:
#include<stdio.h>
#include<stdlib.h>
#include<pthread.h>
#include<unistd.h>
void *thread_fun(void *arg)
{
printf("hello \n");
}
int main()
{
pthread_t id;
pthread_create(&id,NULL,thread_fun,NULL);
printf("hello main\n");
return 0;
}
运行要加-lpthread ,绑定线程库(是一个共享库)
ldd main 查看main的共享库
修改上述代码;示例2:
创建线程:主线程输出10次提示信息,创建的线程输出10次提示信息。
我们只需要在主线程中调用pthread_create函数创建线程,实现函数线程fun,主线程打印信息即可。fun函数线程打印信息即可。那么代码如下:
#include<stdio.h>
#include<stdlib.h>
#include<pthread.h>
#include<unistd.h>
#include<assert.h>
void *thread_fun(void *arg)
{
for(int i=0;i<10;++i)
{
printf("hello \n");
sleep(1);//改为sleep(5),主函数运行完后直接退出,fun中的hello运行不完10次
}
}
int main()
{
pthread_t id;
pthread_create(&id,NULL,thread_fun,NULL);//创建一个新线程 并发运行着
int i=0;
for(;i<10;++i)
{
printf("main run\n");
sleep(1);
}
return 0; //主函数运行结束后,进程直接退出,线程没有运行完也直接退出
}
sleep()会阻塞当前线程),让出CUP的使用
、目的是不让当前线程独自霸占该进程所获的CPU资源,以留一定时间给其他线程执行的机会。所以不会占用cpu。所以sleep的使用是很关键的。
将上面fun函数改为休眠5秒,主函数结束打印。fun函数没有打印完也会结束
如果想要合并或者等待一个线程结束,我们需要引入pthread_join函数
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <assert.h>
#include <pthread.h>
void *fun(void *arg)
{
int i=0;
for(;i<10;i++)
{
printf("fun run\n");
sleep(1);
}
// pthread_exit("fun over");
pthread_exit(NULL);
}
int main()
{
pthread_t id;
pthread_create(&id,NULL,fun,NULL);
int i=0;
for(;i<5;i++)
{
printf("main run\n");
sleep(1);
}
// char *s=NULL;
//pthread_join(id,(void **)&s);
//printf("join:s=%s\n",s);
pthread_join(id,NULL);
exit(0);
}
运行结果我们会发现主函数线程打印结束后,依旧会等待函数线程打印完毕,程序才会结束
总结:
到此Linux多线程第一期结束,主要理解什么是线程以及线程的基本使用创建,感谢观看!后续更新线程相关安全,同步,锁等问题。