课堂笔记九之嵌入式C多线程开发

一、信号

1、信号是一种向进程发送通知,告诉其某件事情发生了的一种简单通信机制
2、信号的产生
(1)另一个进程发送信号
(2)内核发送信号底层硬件发送信号
(3)底层硬件发送信号
3、信号列表
(1)常用信号
课堂笔记九之嵌入式C多线程开发
课堂笔记九之嵌入式C多线程开发
4、信号发送
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
kill命令就是调用这个函数来实现。
#include <signal.h>
int raise(int sig);
功能:
1)kill:向PID所指向的进程发送指定的信号。
2)raise:向当前进程发送指定信号。
返回值:
1)kill:成功返回0,失败返回-1,errno被设置。
2)rasie:成功返回0,失败返回非0。
(1)int kill(pid_t pid,int sig);
(2)int raise(int sig);
(3)unsigned int alarm(unsigned int seconds);
(4)void abort();
课堂笔记九之嵌入式C多线程开发
课堂笔记九之嵌入式C多线程开发
课堂笔记九之嵌入式C多线程开发
5、进程挂起pause
pause:只要一直处于休眠状态,表示pause函数一直是调用成功的。
当被信号唤醒后会返回-1,表示失败了,errno的错误号被设置EINTR(表示函数被信号中断)6、信号处理方式
(1)忽略:收到信号等于没收到;
(2)执行用户需要执行的动作(捕获):收到信号,不做默认处理,做想要的处理;
(3)默认处理:信号自生作用(终止)。
7、信号处理API
(1)sighandler_t signal(int signum, sighandler_t handler);
参数:
1)signum:信号编号。
2)handler:信号处理方式。
sighandler_t是被typedef后的类型,原类型 void (*)(int),这是一个函数指针类型。
课堂笔记九之嵌入式C多线程开发
课堂笔记九之嵌入式C多线程开发
课堂笔记九之嵌入式C多线程开发
课堂笔记九之嵌入式C多线程开发
课堂笔记九之嵌入式C多线程开发
课堂笔记九之嵌入式C多线程开发
sighandler_t handler也有直接写成void (*handler)(int);
sighandler_t signal(int signum, void (*handler)(int));

a. 忽略:SIG_IGN
课堂笔记九之嵌入式C多线程开发
课堂笔记九之嵌入式C多线程开发
课堂笔记九之嵌入式C多线程开发
b.默认:SIG_DFL
c.捕获:填写类型为void (*)(int)的捕获函数的地址,当信号发生时,会自动调用捕获函数来进行相应的处理。
当然这个捕获函数需要我们自己来实现,捕获函数的int参数,用于接收信号编号。捕获函数也被称为信号处理函数。
返回值:
成功:返回上一次的处理方式
失败:返回SIG_ERR宏值,并且设置errno。

二、线程

1、线程是任务调度和执行的基本单位
2、为什么会有线程?
(1)进程实现多任务的缺点
1)进程间切换的计算机资源开销很大,切换效率非常低
OS是通过虚拟内存机制来实现进程空间独立的,进程在并发运行时需要相互间的切换,切换时必然需要涉及虚拟内存机制的控制,但是虚拟内存机制比较复杂,所以在进行进程间切换时,会耗费高昂的cpu、缓存(cache)、内存等计算机资源,也非常耗费切换时间。
2)进程间数据共享的开销也很大
当程序涉及多进程时,往往会涉及到进程间的通信,但是由于进程空间的独立性,OS提供了各种各样的通信机制,这些通信机制共同原理就是,通过OS来转发进程间的数据,但是调用OS提供的这些通信机制的函数时,这些OS函数的运行也是需要消耗相当cpu、内存等计算机资源的,同时也很耗费时间。
(2)线程和进程的关系
1)线程是进程的一个执行单元,是进程内的调度实体。比进程更小的独立运行的基本单位。线程也被称为轻量级进程。
2)同一进程的线程共享本进程的地址空间,而进程之间则是独立的地址空间。
3)进程退出,进程中所有线程全部退出。
4)一个进程崩溃后,不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。
5)线程不可能完全替代进程。
6)线程拥有独立的属性。
A、每个线程拥有自己独立的线程ID(TID)
B、每个线程有独立的切换状态
C、调度优先级
D、有自己独立的函数栈
E、自己独立的错误号
F、每一个线程有自己独立的信号屏蔽字和未决信号集
G、每个线程有自己独立的tack_struct结构体
3、线程的特点
(1)线程切换的开销很低(实质是函数的切换)
(2)线程通信机制简单(全局变量)
4、线程操作
(1)线程函数是由谁提供的?
1)非OS,而是线程库libpthread.a/.so
原本线程函数也可以完全由OS来实现,但是后来为了不给OS增加负担,同时也为了提高线程的灵活性,后来的线程就不在由OS提供,而是由单独的线程库来提供,不过线程库在实现时,也是调用了相应的系统API的,也就是说线程的核心实现也是离不开OS支持的。
2)线程控制函数有:pthread_create、pthread_join、 pthread_detach、pthread_cancel、pthread_exit等。
(2)线程库和函数手册的安装
1)sudo apt-get install glibc-doc :安装线程库
2)sudo apt-get install manpages-posix-dev:安装线程库的函数手册
(3)线程创建
1)int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg);

返回值:
成功:返回0,失败:返回非零错误号。
参数:
thread存放线程的TID。
attr用于设置线程属性,设置线程属性的目的是为了实现某些特殊功能,如果设置为NULL,表示不设置特有的属性,使用线程默认属性所提供的功能即可。正常情况下,线程默认属性所提供的功能就已经够用了,所以这个参数我们都是设置为NULL。
start_routine:要注册为线程的函数地址
arg:传递给线程函数的参数,这个参数会传递给pth_arg,如果参数很多的话,我们做成一个结构体,然后把结构体变量的地址传过去。如果你不想传递参数的话,你可以设置为NULL。
(4)线程退出
1)被动退出:int pthread_cancel(pthread_t thread);
2)主动退出
3)注册线程退出处理函数
A、void pthread_cleanup_push(void (*routine)(void *), void *arg);
void pthread_cleanup_pop(int execute);
B、弹栈线程退出处理函数的几种条件
(a)调用thread_cleanup_pop(!0),主动弹栈
(b)如果线程是被别人调用pthread_cancel取消的,也会弹栈
(c)如果线程是调用pthread_exit函数退出的,也会弹栈
(d)注:return退出的话,是不会自动弹栈的,要弹栈的话,必须主动调动 thread_cleanup_pop(!0)。(4)线程等待
A、等待线程的目的
(a)保证线程的退出顺序:保证一个线程退出并且回收资源后允许下一个进程退出
(b)回收线程退出时的资源情况:保证当前线程退出后,创建的新线程不会复用刚才退出线程的地址空间
(c)获得新线程退出时的结果是否正确的退出返回值,这个有点类似回收僵尸进程的wait,保证不会发生内存泄露等问题
B、int pthread_join(pthread_t thread, void **retval);
(a)线程资源为什么不采用进程退出之后一起回收?有些程序(进程)一旦运行后将会长期运行,不会结束,所以次线程在结束时必须回收资源,如果不回收,每结束一个次线程就导致一部分资源被占用,慢慢累积会使得整个进程资源越用越少,最好导致进程崩溃,所以次线程结束时,必须回收次线程资源。
(2)线程状态
A、可结合态
(a)这种状态下的线程是能够被其他进程回收其资源或杀死的
B、分离态
(a)这种状态下的线程是不能够被其他线程回收或杀死的;它的存储资源在它终止时由系统自动释放
C、默认情况下,线程被创建成可结合的!
D、如何避免多线程退出导致的内存泄漏?
(a)每个可结合线程需要显示的调用pthread_join回收
(b)将其变成分离态的线程线程分离函数—–pthread_detach
5、线程同步
(1)线程 VS 进程
1)进程:进程空间天然是独立的,因此进程间资源的保护是天然的(现成的),需要重点关心的进程间的通信
2)线程:多线程天然的共享进程空间,因此线程数据共享是天然的(现成的),需要重点关心的是资源的保护
(2)线程的资源保护机制
1)互斥锁(只能在线程里用)
A、互斥锁使用的步骤
(a)定义一个互斥锁(变量)pthread_mutex_t mutex;
(b)初始化互斥锁:预设互斥锁的初始值初始化互斥锁的函数a)
函数原型:
#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
功能:初始化定义的互斥锁什么是初始化,就是设置互斥锁所需要的值。
返回值:总是返回0,所以这个函数不需要进行出错处理。
参数:

  • mutex:互斥锁,需要我们自己定义。比如:pthread_mutex_t mutex;pthread_mutex_t是一个结构体类型,所以mutex实际上是一个结构体变量。
  • attr:互斥锁的属性
    置NULL表示使用默认属性,除非我们想要实现一些互斥锁的特殊功能,否则默认属性就够用了。pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER 编译时初始化锁位解锁状态
    (c)加锁解锁
    pthread_mutex_lock(&mutex)(阻塞加锁)访问临界区加锁操作;
    pthread_mutex_trylock( &mutex)(非阻塞加锁);
    pthread_mutex_lock() 类似,不同的是在锁已经被占据时返回 EBUSY 而不是挂起等待;
    pthread_mutex_unlock(&mutex): 访问临界区解锁操。
    (d)进程退出时销毁互斥锁
    pthread_mutex_destroy
    函数原型:
    #include <pthread.h>
    int pthread_mutex_destroy(pthread_mutex_t *mutex);
    功能:销毁互斥锁;所谓销毁,说白了就是删除互斥锁相关的数据,释放互斥锁数据所占用的各种内存资源。
    返回值:成功返回0,失败返回非零错误号
    2)线程信号量
    A、线程信号量使用步骤
    (a)定义信号量集合信号量集合需要我们自己定义,比如:sem_t sem[3],线程信号量集合其实就是一个数组,数组每个元素就是一个信号量。
    sem[0]:第一个信号量
    sem[1]:第二个信号量
    sem[2]:第三个信号量
    (b)初始化集合中的每个信号量
    初始化信号量的函数
    函数原型:#include <semaphore.h>
    int sem_init(sem_t *sem, int pshared, unsigned int value);
    功能:初始化线程信号量集合中的某个信号量,给它设置一个初始值。
    返回值:成功返回0,失败返回-1,errno被设置。
    注意信号量的错误号不是返回的,而是设置到errno中。
    参数:
  • sem:信号量集合中的某个信号量信号量
    集合需要我们自己定义,比如:sem_t sem[3],线程信号量集合其实就是一个数组,数组每个元素就是一个信号量。
    sem[0]:第一个信号量
    sem[1]:第二个信号量
    sem[2]:第三个信号量
    sem_init(&sem[0], int pshared, unsigned int value);线程信号量集合其实就是自定义的一个数组,而进程信号量集合则是通过semget函数创建的。
    我们只要把数组定义为全局变量,所有的线程即可共享使用,不像进程信号量,需要semid才能实现共享操作。
  • pshared:
  • 0:给线程使用
  • !0:可以给进程使用
    不过对于进程来说,我们更多的还是使用进程信号量,因为线程信号量用到进程上时,存在一些不稳定的情况。
  • value:初始化值
    对于二值信号量来说,要么是1,要么是0。
    课堂笔记九之嵌入式C多线程开发
    课堂笔记九之嵌入式C多线程开发
    课堂笔记九之嵌入式C多线程开发

(c)p、v操作
P操作
函数原型:
#include <semaphore.h>
int sem_wait(sem_t *sem);//阻塞p操作
功能:阻塞p操作集合中某个信号量,值-1
如果能够p操作成功最好,否则就阻塞直到p操作操作成功为止。
返回值:成功返回0,失败返回-1,errno被设置。
参数:p操作的某个信号量。
比如:sem_wait(&sem[0]);
sem_wait的兄弟函数
int sem_trywait(sem_t *sem):不阻塞
如果能够p操作就p操作,如果不能p操作就出错返回,不会阻塞。
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
可以设置阻塞时间,如果能够p操作就p操作,不能就阻塞,如果在设置的时间内好没有p操作成功就是出错返回,不再阻塞。
这两个函数了解即可,不需要掌握,如果你真的用到了,自己举一反三即可搞定。
v操作
函数原型:
#include <semaphore.h>
int sem_post(sem_t *sem);
功能:对某个信号量进行v操作,v操作不存在阻塞问题。
v操作成功后,信号量的值会+1
返回值:成功返回0,失败返回-1,errno被设置。
课堂笔记九之嵌入式C多线程开发
课堂笔记九之嵌入式C多线程开发
课堂笔记九之嵌入式C多线程开发
课堂笔记九之嵌入式C多线程开发
课堂笔记九之嵌入式C多线程开发
课堂笔记九之嵌入式C多线程开发
课堂笔记九之嵌入式C多线程开发
课堂笔记九之嵌入式C多线程开发
课堂笔记九之嵌入式C多线程开发
课堂笔记九之嵌入式C多线程开发
课堂笔记九之嵌入式C多线程开发
课堂笔记九之嵌入式C多线程开发
(d)进程结束时,删除线程信号量集合
删除信号量
函数原型:
#include <semaphore.h>
int sem_destroy(sem_t *sem);
功能:删除某个信号量,把所有信号量都删除后,信号量集合就被销毁。
这与删除进程信号量集合有所不同,对于进程信号量集合来说,只要删除一个信号量,整个集合即被删除,但是对于线程信号量来说,需要一个一个的删除,当所有信号量都删除完后,集合才被删除完毕。
B、代码实例
(a)实现多个线程同步;按照顺序执行
3)条件变量
A、条件变量的作用(线程协同)
多线程配合工作时,当线程检测到某条件不满足时就休眠,直到别的线程将条件准备好,然后通过条件变量将其唤醒。
(a)主线程对va变量循环+1,次线程发现va==5时,打印va的值并将va清0,如果va的值!=5就什么都不做
课堂笔记九之嵌入式C多线程开发
课堂笔记九之嵌入式C多线程开发
课堂笔记九之嵌入式C多线程开发

B、注:条件变量需要在互斥锁的配合下才能工作。
C、条件变量的使用步骤:
(a)定义一个条件变量(全局变量)由于条件变量需要互斥锁的配合,所以还需要定义一个线程互斥锁。
条件变量的类型:pthread_cond_t
(b)初始化条件变量
函数原型:
#include <pthread.h>
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
功能:初始化条件变量,与互斥锁的初始化类似。
pthread_cond_t cond; //定义条件变量pthread_cond_init(&cond, NULL); //第二个参数为NULL,表示不设置条件变量的属性。
也可以直接初始化:pthread_cond_t cond = PTHREAD_COND_INITIALIZER;//与互斥锁的初始化的原理是一样的
返回值:成功返回0,失败返回非零错误号
参数:

  • cond:条件变量
  • attr:用于设置条件变量的属性,设置为NULL,表示使用默认属性
    (c)使用条件变量
    等待条件的函数
    函数原型:
    #include <pthread.h>
    int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
    功能:检测条件变量cond,如果cond没有被设置,表示条件还不满足,别人还没有对cond进行设置,此时pthread_cond_wait会休眠(阻塞),直到别的线程设置cond表示条件准备好后,才会被唤醒。
    返回值:成功返回0,失败返回非零错误号
    参数:
  • cond:条件变量
  • mutex:和条件变量配合使用的互斥锁
    pthread_cond_wait的兄弟函数
    int pthread_cond_timedwait(pthread_cond_t *restrict cond, \pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);
    多了第三个参数,用于设置阻塞时间,如果条件不满足时休眠(阻塞),但是不会一直休眠,当时间超时后,如果cond还没有被设置,函数不再休眠。
    设置条件变量的函数
    函数原型:
    #include <pthread.h>
    int pthread_cond_signal(pthread_cond_t *cond);
    功能:当线程将某个数据准备好时,就可以调用该函数去设置cond,表示条件准备好了,pthread_cond_wait检测到cond被设置后就不再休眠(被唤醒),线程继续运行,使用别的线程准备好的数据来做事。
    当调用pthread_cond_wait函数等待条件满足的线程只有一个时,就是用pthread_cond_signal来唤醒,如果说有好多线程都调用pthread_cond_wait在等待时,使用int pthread_cond_broadcast(pthread_cond_t *cond);它可以将所有调用pthread_cond_wait而休眠的线程都唤醒。
    代码演示
    调用pthread_cond_signal去设置条件变量,相当是给pthread_cond_wait发送了一个线程间专用的信号,通知调用pthread_cond_wait的线程,某某条件满足了不要再睡了,赶紧做事吧。
    课堂笔记九之嵌入式C多线程开发
    课堂笔记九之嵌入式C多线程开发
    课堂笔记九之嵌入式C多线程开发
    课堂笔记九之嵌入式C多线程开发

(d)删除条件变量,也需要把互斥锁删除。
删除条件变量
函数原型:
#include <pthread.h>
int pthread_cond_destroy(pthread_cond_t *cond);
6、进程 VS 线程
(1)多线程比多进程成本低,但性能更低
1)多进程是立体交通系统,虽然造价高,上坡下坡多耗点油,但是不堵车。
2)多线程是平面交通系统,造价低,但红绿灯太多,老堵车。
(2)区别
1)进程是资源分配的最小单位,线程是任务调度的最小单位;
2)每个进程拥有独立的地址空间,多个线程共享进程地址空间
A、线程之间切换比进程之间切换开销少;
B、线程的调度必须通过频繁加锁来保持同步,影响了线程并发性能;
C、进程比线程更健壮,多进程之间相互独立,进程的异常对其他进程无影响,一个线程的崩溃可能影响其他线程或者整个程序;
D、线程之间的通信更方便(小数据量),同一进程下的线程共享全局变量、静态变量等数据,而进程之间的通信需要以通信的方式(IPC)进行。不过如何处理好同步与互斥是编写多线程程序的难点。E、多线程的代码结构比多进程代码结构易读;
(3)如何选择?
1)需要频繁创建销毁的优先用线程
2)高性能交易服务器中间件,如TUXEDO,都是主张多进程的
3)需要进行大量计算的优先使用线程
4)强相关的处理用线程,弱相关的处理用进程
5)多机分布的用进程,多核分布的用线程