前些天驱动搞的有点头大,今天我们来放松放松,我们来讲讲应用编程这边,我们来讲讲多线程:
我们先来讲讲为什么要引入多线程:
在编写代码时,是否会遇到以下的场景会感觉到难以下手?
要做2件事,一件需要阻塞等待,另一件需要实时进行。例如播放器:一边 在屏幕上播放视频,一边在等待用户的按键操作。如果使用单线程的话,程序必 须一会查询有无按键,一会播放视频。查询按键太久,就会导致视频播放卡顿;视频播放太久,就无法及时响应用户的操作。并且查询按键和播放视频的代码混杂在一起,代码丑陋。 如果使用多线程,线程1单独处理按键,线程2单独处理播放,可以完美解决上述问题。
对于进程而言,每一个进程都有一个唯一对应的PID号来表示该进程,而对 于线程而言,也有一个“类似于进程的 PID 号”,名为 tid,其本质是一个 pthread_t 类型的变量。线程号与进程号是表示线程和进程的唯一标识,但是对 于线程号而言,其仅仅在其所属的进程上下文中才有意义。
在程序中,可以通过函数pthread_self,来返回当前线程的线程号,例程 1 给出了打印线程tid号。
测试例程1:(Phtread_txex1.c)
注意:因采用POSIX线程接口,故在要编译的时候包含pthread库,使用gcc 编译应gcc xxx.c -lpthread 方可编译多线程程序。 编译结果:
下面我们来看看线程创建:
怎么创建线程呢?使用pthread_create函数:
⚫ 该函数第一个参数为pthread_t指针,用来保存新建线程的线程号;
⚫ 第二个参数表示了线程的属性,一般传入NULL表示默认属性;
⚫ 第三个参数是一个函数指针,就是线程执行的函数。这个函数返回值为void*, 形参为void*。 ⚫ 第四个参数则表示为向线程处理函数传入的参数,若不传入,可用NULL填充, 有关线程传参后续小节会有详细的说明,接下来通过一个简单例程来使用该函数 创建出一个线程。
测试例程2:(Phtread_txex2.c)
运行结果:
通过pthread_create确实可以创建出来线程,主线程中执行 pthread_create后的tid指向了线程号空间,与子线程通过函数 pthread_self打印出来的线程号一致。 特别说明的是,当主线程伴随进程结束时,所创建出来的线程也会立即结束, 不会继续执行。并且创建出来的线程的执行顺序是随机竞争的,并不能保证哪一 个线程会先运行。可以将上述代码中sleep函数进行注释,观察实验现象。
去掉上述代码25行后运行结果:
上述运行代码3次,其中有2次被进程结束,无法执行到子线程的逻辑,最后一 次则执行到了子线程逻辑后结束的进程。如此可以说明,线程的执行顺序不受控 制,且整个进程结束后所产生的线程也随之被释放,在后续内容中将会描述如何 控制线程执行。
下面我们来试试向线程传入参数:
pthread_create()的最后一个参数的为void*类型的数据,表示可以向线 程传递一个void*数据类型的参数,线程的回调函数中可以获取该参数,例程3 举例了如何向线程传入变量地址与变量值。
运行结果:
本例程展示了如何利用线程创建函数的第四个参数向线程传入数据,举例了 如何以地址的方式传入值、以变量的方式传入值,例程代码的21行,是将变量 a先行取地址后,再次强制类型转化为void*后传入线程,线程处理的回调函数 中,先将万能指针void*转化为int*,再次取地址就可以获得该地址变量的值, 其本质在于地址的传递。例程代码的27行,直接将int类型的变量强制转化为 void*进行传递(针对不同位数机器,指针对其字数不同,需要int转化为long 在转指针,否则可能会发生警告),在线程处理回调函数中,直接将void*数据转化为int类型即可,本质上是在传递变量a的值。 上述两种方法均可得到所要的值,但是要注意其本质,一个为地址传递,一 个为值的传递。当变量发生改变时候,传递地址后,该地址所对应的变量也会发 生改变,但传入变量值的时候,即使地址指针所指的变量发生变化,但传入的为 变量值,不会受到指针的指向的影响,实际项目中切记两者之间的区别。
上述例程讲述了如何向线程传递一个参数,在处理实际项目中,往往会遇到 传递多个参数的问题,我们可以通过结构体来进行传递,解决此问题。
接着来看看线程的退出与回收:
线程的退出情况有三种:第一种是进程结束,进程中所有的线程也会随之结 束。第二种是通过函数pthread_exit来主动的退出线程。第三种被其他线程调 用pthread_cancel 来被动退出。当线程结束后,主线程可以通过函数pthread_join/pthread_tryjoin_np 来回收线程的资源,并且获得线程结束后需要返回的数据。
线程主动退出:
pthread_exit 函数原型如下:
线程被动退出
pthread_cancel 函数原型如下:
该函数传入一个tid号,会强制退出该tid所指向的线程,若成功执行会返回0。
线程资源回收(阻塞方式)
pthread_join 函数原型如下:
该函数为线程回收函数,默认状态为阻塞状态,直到成功回收线程后才返回。第一个参数为要回收线程的tid号,第二个参数为线程回收后接受线程传出的数据。
线程资源回收(非阻塞方式)
pthread_tryjoin_np函数原型如下:
该函数为非阻塞模式回收函数,通过返回值判断是否回收掉线程,成功回 收则返回0,其余参数与pthread_join一致。
前面的话,大概讲了一下线程的一些基础知识,下面我们来看看重点的,我们来看看在使用线程去访问临界资源时需要做的保护:
多线程编临界资源访问:
当线程在运行过程中,去操作公共资源,如全局变量的时候,可能会发生彼 此“矛盾”现象。例如线程1企图想让变量自增,而线程2企图想要变量自减, 两个线程存在互相竞争的关系导致变量永远处于一个“平衡状态”,两个线程互相竞争,线程1得到执行权后将变量自加,当线程2得到执行权后将变量自减, 变量似乎永远在某个范围内浮动,无法到达期望数值,如例程9所示。
运行结果:
为了解决上述对临界资源的竞争问题,pthread线程引出了互斥锁来解决临 界资源访问。通过对临界资源加锁来保护资源只被单个线程操作,待操作结束后 解锁,其余线程才可获得操作权。
因为这里是应用编程来着,只会去讲那些API接口,只需要知道怎么去用就行,如果想知道内部,我们前面在驱动那边也讲过锁和信号量这些(doge.),时不时温故而知新,何不美哉
互斥锁API简述
多个线程都要访问某个临界资源,比如某个全局变量时,需要互斥地访问: 我访问时,你不能访问。 可以使用以下函数进行互斥操作。
初始化互斥量
函数原型如下:
该函数初始化一个互斥量,第一个参数是改互斥量指针,第二个参数为控制 互斥量的属性,一般为NULL。当函数成功后会返回0,代表初始化互斥量成功。 当然初始化互斥量也可以调用宏来快速初始化,代码如下:
互斥量加锁/解锁
函数原型如下:
lock 函数与unlock函数分别为加锁解锁函数,只需要传入已经初始化好的 pthread_mutex_t 互斥量指针。成功后会返回0。
当某一个线程获得了执行权后,执行lock函数,一旦加锁成功后,其余线程遇到lock 函数时候会发生阻塞,直至获取资源的线程执行unlock函数后。 unlock 函数会唤醒其他正在等待互斥量的线程。
特别注意的是,当获取lock之后,必须在逻辑处理结束后执行unlock,否则会发生死锁现象!导致其余线程一直处于阻塞状态,无法执行下去。在使用互斥量的时候,尤其要注意使用pthread_cancel函数,防止发生死锁现象!
互斥量加锁(非阻塞方式)
函数原型如下:
该函数同样也是一个线程加锁函数,但该函数是非阻塞模式通过返回值来 判断是否加锁成功,用法与上述阻塞加锁函数一致。
互斥量销毁(非阻塞方式)
函数原型如下:
该函数是用于销毁互斥量的,传入互斥量的指针,就可以完成互斥量的销 毁,成功返回0。
测试例程10:(Phtread_txex10.c)
上述例程通过加入互斥量,保证了临界变量某一时刻只被某一线程控制, 实现了临界资源的控制。需要说明的是,线程加锁在循环内与循环外的情况。 本历程在进入while循环前进行了加锁操作,在循环结束后进行的解锁操作, 如果将加锁解锁全部放入while循环内,作为单核的机器,执行结果无异,当 有多核机器执行代码时,可能会发生“抢锁”现象,这取决于操作系统底层的实现。
多线程编执行顺序控制
解决了临界资源的访问,但似乎对线程的执行顺序无法得到控制,因线程 都是无序执行,之前采用sleep强行延时的方法勉强可以控制执行顺序,但此 方法在实际项目情况往往是不可取的,其仅仅可解决线程创建的顺序,当创建之后执行的顺序又不会受到控制,于是便引入了信号量的概念,解决线程执行顺序。
文章篇幅有点长了,我们下一篇来讲讲信号量,完结,撒花(doge.)