c线程
1. c线程初识
thread1.c#include <pthread.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
void *pth_fun(void *pth_arg){
while(1){
printf("11111\n");
sleep(1);
}
return NULL;
}
int main(int argc, char const *argv[])
{
pthread_t tid=0;
pthread_create(&tid,NULL,pth_fun,NULL);
printf("tid=%u\n",tid); //typedef unsigned long int pthread_t;
while(1){
printf("22222\n");
sleep(3);
printf("-----------------\n");
}
return 0;
} gcc thread1.c -pthread
2. 为什么会有线程
在上世纪60年代,也就是操作系统刚问世不久时,那个时候的OS只有进程没有线程,直到到了80年代才开始
有了线程这个东西
为什么有线程
主要是人们开始发现,进程有缺点,为了弥补进程的缺点,就发明了线程这个东西,需要注意的是,是
弥补而不是替代,也就是说线程不会把进程给干掉,线程也是基于进程而实现的,是没办法干掉进程的
3. 进程的缺点
让每个进程有一个安全的独立进程空间,OS使用了虚拟内存机制,通过虚拟内
存机制,能够让每一个进程都有完全独立的进程空间
这种独立的进程空间最大的优点就是,可以很好地保证每一个进程的安全,不被其它进程所***或者干扰,
但是突出的优点往往又会导致另外的缺点。
拥有独立进程空间的进程有两大明显的缺点,
(1)进程间切换的计算机资源开销很大,切换效率非常低
(2)进程间数据共享的开销也很大
3.1 进程间切换的计算机资源开销很大,切换效率非常低
OS是通过虚拟内存机制来实现进程空间独立的,进程在并发运行时需要相互间的切换,切换时必
然需要涉及虚拟内存机制的控制,但是虚拟内存机制比较复杂,所以在进行进程间切换时,
会耗费高昂的cpu、缓存(cache)、内存等计算机资源,也非常耗费切换时间
总之,进程切换的开销很大,有关进程间切换的开销问题,目前因为课程定位问题,我们目前只能以这种笼
统的方式来解释,不过也确实没有深入介绍的必要,对于大家来说,你只需要清楚进程切换时的计算机资源
开销是很大就行
3.2 进程间数据共享的开销也很大
当程序涉及多进程时,往往会涉及到进程间的通信,但是由于进程空间的独立性,OS提供了各种各样的通信
机制,这些通信机制共同原理就是,通过OS来转发进程间的数据,但是调用OS提供的这些通信机制的函数时,
这些OS函数的运行也是需要消耗相当cpu、内存等计算机资源的,同时也很耗费时间。
因此,对于我们有OS的计算机来说,虽然进程是必不可少的,但是进程确又不能太多,进程太多会导致计算
机资源被剧烈消耗,此时你会发现你的计算机非常的卡
4. 因为进程的缺点,使得早期只有进程的OS存在着非常大的问题
在早期,当OS只有进程时,应用程序通过创建子进程来得到多进程的目的大致有两个:
(1)目的1:创建子进程,执行新程序
(2)目的2:创建子进程得到多进程,通过多进程并发实现多线任务
1)同时阻塞的读鼠标和键盘时,如果单线的话会想互影响,需要两线任务来实现。
2)读写管道时,读操作是阻塞的,为了避免读操作影响写操作,也需要两线任务同时进行。
3)等等:多线任务的例子很多
对于第一种目的,执行新程序时必须创建子进程,这个无法逃避的
但是对于第二种目的来说,如果使用多进程来实现就存在巨大的问题,因为几乎所有的程序都涉及多线任务的
操作,而且好些程序往往都是十几个任务以上,如果此时使用多进程来实现多线任务的,这就大致大量进程的产生。
比如计算机运行了100个程序,假设每个程序平均10多个任务,如果全部采用多进程来实现,计算机最终要运行
的进程就多达上100个。
所以在早期使用多进程来实现程序的多线任务时,往往导致计算机进程数量暴增,而进程切换和进程间通信
的计算机资源开销又很大,因此往往导致计算机非常卡顿,程序的运行效率非常低
当人们认识到进程缺点所带来的巨大问题后,大家就开始思考,能不能使用另一种方式来实现程序的“多线任务”,
而不是使用多进程来实现,到了上世纪80年代人们就发明了线程这个东西,以弥补多进程实现多线任务的缺点
5. 线程为什么能弥补进程的缺点
线程与进程一样,线程和进程会被OS统一调度,所以所有的线程和进程都是一起并发运行的,
如果线程不是并发的,是不可能实现程序的多线任务的。
有了线程以后,凡是程序涉及到多线任务时,都使用多线程来实现,使用多线程来实现时,线程间的切换和数据
通信的开销非常低,正因为开销非常低,因此线程还有另一个名称,叫“轻量级的进程”。
总结的讲,说白了线程就是为了多线任务而生的,多线程的多线二字,不就是多线任务的多线二字吗
疑问:使用线程来实现时,线程也需要切换和通信,为什么线程的切换和通信开销就很低呢
5.1 为什么线程切换的开销很低
使用多进程来实现程序的多线任务,多线并发运行时,涉及到的是进程间的切换,我们前面就说过,进程间
切换时开销非常大
但是使用多线程来实现多线任务时,由于线程本质上它只是程序(进程)的一个函数,只不过线程函数
与普通函数的区别是,普通函数时单线的运行关系,而线程函数被注册为线程后,是多线并发运行
对于普通函数来说,只有当相互调动时才会涉及函数间的切换,但是对于线程函数来说,只要运行的时间片
到了就会切换,但是不管是那种函数间的切换,进程自己函数的切换只是进程内部的事情,不涉及进程间切换,
就省去了进程间切换的巨大开销。
如果是不同进程的线程之间需要切换的话,还是会涉及到进程间的切换了,但是不管怎们说,线程的出
现,至少为程序内部多线任务之间的切换,省去了大笔的进程切换所导致“资源开销”。
线程的切换其实就是函数间的切换,函数切换当然也需要开销,但是这些开销相比进程间切换的开销来说,已经非常小了。
5.2 为什么线程间数据通信的开销很低
线程的本质就是函数,请问大家函数之间如果想要数据共享(通信)的话,应该怎么办?
函数间通信有两种方式:
(1)具有相互调用关系函数来说
使用函数传参来通信
(2)对于没有调用关系的函数来说
使用全局变量来通信。
A函数 ————> 全局变量 ————> B函数
所以说全局变量的作用就是用来实现无调用关系的函数间通信的。
进程中所有的线程函数除了相互并发运行外,没有调用关系,所以线程函数间想要数据共享的话,就使用全局变量来通信。
从这里可以看出,进程内部的线程间进行数据共享非常容易,使用全局变量即可,根本不需要调用什么
OS提供的通信机制,所以线程间通信的开销自然就非常的低。
5.3 是不是有了线程后,进程就不需要了
线程是不可能完全替代掉进程的,只有在多线任务时会替代进程,
但是运行新程序的时,还是必须创建子进程。
线程的本质是函数,函数运行需要内存空间,这个内存空间怎么来,
事实上线程运行的内存空间就是进程的内存空间,因此线程运行时必须依赖于进程的存在,如果没有进程所
提供的内存空间这个资源,线程根本无法运行
换句话说,线程作为函数,只是进程的一个部分而已,线程是不可能脱离进程而独立存在。
所以同一个进程中的所有线程,都是运行在相同的进程空间中的,换句话说同一个进程中所有线程共相同的进程空间。
程序(进程)的函数能够通过全局变量来通信,就是因为全局变量、函数
等全部都在同一个进程空间中,既然进程空间是大家共享的,那么所有的函数,自然就能共享访问在共享空间中所开辟出来的全局变量。
对于进程中的所有函数来说(包括线程函数),进程中几乎所有的资源都是共享的,比如打开的文件描述,
所有可以被调用的子函数,进程的当前工作目录,进程uid、gid,进程PID等等
5.4 线程自己独立的属性
进程中的所有线程会共享进程提供资源(全局变量、工作目录、打开的文件描述符、子函数等等),但是每
个线程作为一个单独的执行体,也有属于自己的独立的东西。
(1)每个线程拥有自己独立的线程ID(TID)
(2)每个线程有独立的切换状态
1)在切换时,当前线程被中断的那条指令的地址
2)线程切换时相关的运行状态
当线程切换时,必须保存以上信息,以便切换回来后还原现场,从中断处继续运行这就好比我正在工作,突然有人来找我,此时我么就被中断了,我就需要保存好我的工作现场,等我回来时再还原工作现场,以便接着做。
(3)有自己独立的函数栈
其实每一个函数都有自己的函数栈,所有的函数栈都开辟于进程空间的进程栈
函数栈的作用就是用来保存函数局部变量的,既然所有的线程函数都有自己的独立的函数栈,自然就有自
己独立的局部变量
线程函数的函数栈,我们往往也称为线程栈
(4)自己独立的错误号
线程函数的错误号是独立的,所以线程函数出错时,错误号并不是通过设置errno实现的,而是直接将错误号返回
(5)每一个线程有自己独立的信号屏蔽字和未决信号集
(6)每个线程有自己独立的tack_struct结构体
我们说进程在运行的过程中,OS会为每个进程开辟一个task_struct结构体变量用于存放进程所涉及
到的各种管理信息,同样的为了管理线程,也会为线程开辟一个task_struct变量,只不过适用于存放
线程的管理信息的
6. 什么时候使用多线程和多进程
6.1 线程
程序涉及多线任务时,使用线程
6.2 进程
程序涉及到运行新程序时,必须使用多进程,不过一般来说,如果不是大型软件和框架的话,我们的程序并不
需要执行新程序
创建子进程执行新程序,大多都是OS操心的事,比如通过命令行或者图形界面启动程序,此时就涉及到要
运行一个新的程序,根据我们进程控制章节的学习,父进程(命令行、图形界面)就会创建子进程并加
载执行新程序
7. 线程控制相关的函数
实现多进程的时候有进程控制,进程控制涉及到的函数有fork、exec、wait、exit等函数,实现多线程的时候,
同样有线程控制函数,这些线程控制函数有:
pthread_create、pthread_join、 pthread_detach、pthread_cancel、pthread_exit等。
7.1 线程函数是由谁提供的
进程控制的fork、exec等函数都是由os系统提供的,那线程函数是由谁提供的呢?
原本线程函数也可以完全由OS来实现,但是后来为了不给OS增加负担,同时也为了提高线程的灵活性,
后来的线程就不在由OS提供,而是由单独的线程库来提供,不过线程库在实现时,也是调用了相应的系统API的,也就是说线程的核心实现也是离不开OS支持的
7.1.1 线程库
(1)c线程函数
由c线程库提供,注意这个c线程库并不是C标准库,而是POSIX C库的一部分,有关标准C库和POSIX库
是什么关系,我们在《C深度解析》这门课中讲C库时,有详细的介绍,不清楚的请看这部分内容。
我们通过man手册就可以查看这个函数是属于什么库的。
CONFORMING TO
POSIX.1-2001.
这里的c线程库,其实windows也是支持的
这里所讲的posix的c线程库,是由美国一些标准组织制定,并有相应c语言和unix、Linux维护团队
开发的。
(2)java、c++、c#的线程函数
由他们自己的线程库来实现的。
这些语言的线程库,是由这些语言的维护团队来开发的
7.1.2 线程库和OS系统API的关系
线程库函数实际上也是封住OS的相应API来实现的,如果线程库运行在Linux这边的话,线程库其实就是
通过调用Linux的clone()等系统函数实现的
将线程函数注册为线程时,其实就是通过调用这类系统API,然后去模拟我们的进程来实现的,正是因为是
模拟进程来实现的,所以线程函数才能进程一样,一起被并发运行
7.1.3 可不可以自己实现线程库
如果你有能力,你完全可以自己调用系统API,然后封装做出自己的c/c++/java线程库,不过就个人来说,
很少人会这么做,一个是因为个人很难有这样的能力,就算勉强实现也是一堆bug,另一个是已经有前辈做
好的、完善的、免费的线程库,个人做线程库基本没有任何经济价值,换句话不挣钱做它干什么
虽然个人很少自己做,但是不少大公司因为自己的独特的需求,往往会开发出自己公司的线程库,专供自己
公司使用,不过一般来说这样的情况很少,我们很难遇到。
不过就算遇到了也没关系,不管谁实现的,线程的原理都是一样的,学会了一种线程库函数的使用,使用
其它线程库时,很快就能上手。
7.1.4 学习本章的意义
(1)c程序经常涉及到多线任务的问题,所以c线程在实际开发中会被经常用到,所以本章必须学
当然要注意,你要使用多线程的话,必须要OS支持,如果没有OS支持,在裸机上是不能使用多线程库的。
如果你在裸机上想要实现多线任务,必须想其它办法
(2)所有的线程库的原理和使用方式都是类似,学习本章有助于学习其它语言的线程库
不管是c的线程函数,还是c++、java、c#的线程函数,它们的实现原理都是类似的,顶多就是所用语言和
具体的细节不太一样,大体上都是一样的,你理解了C线程函数,在学习其它语言的线程库函数就会容易很多
当然java、c++的线程函数会比c的线程复杂一些
|