本文章当作学习笔记,也欢迎交流。
为什么使用多线程?
在嵌入式软件的编程中,我们常常将核心代码放置在一个while(1)循环中反复执行,此时难免会遇到这种情况:
除了大循环的执行代码外,还需要添加一些其他功能来满足项目要求,比如一个视频播放器,我们不仅需要它能播放视频,还需要它能检测按键是否按下等。
如果我们没有使用多线程编程,那么我们需要将视频播放置于while中反复执行,来实现视频的流畅播放;另外,也需要将按键检测放于while循环中反复检测按键是否被按下,但这时就容易遇到一个问题:当按键检测时间过于长时,视频的播放将会卡顿不流畅,同样当视频播放时,按键检测不灵敏,因为两者是互斥的,视频播放时按键无法检测。
如果我们使用多线程,可以在main中创建一个子线程来检测按键,然后在主线程中播放视频,这样就可以一边播放视频一边检测按键两不误了。
在执行线程时必然需要占用CPU资源,而对于多线程来说,线程的执行取决于系统的调度,单核CPU时,多线程的执行是以时间片执行的方式进行;多核CPU相当于多个单核CPU,能同时执行多个线程。
如何多线程编程
多线程编程主要使用以下几个函数:
- 线程创建
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
头文件:#include <pthread.h> 编译时链接库 -lpthread
参数1:pthread_t *thread 用于表示线程id
thread是一个pthread_t的结构体,需要在创建函数前提前定义一个pthread_t 类型的变量:
例如:pthread_t tid
然后再是创建函数pthread_create(&tid, .......)
参数2:const pthread_attr_t *attr 用于设置新建线程的属性,一般写NULL
参数3:void *(*start_routine) (void *) 函数指针,其是线程创建后所执行的函数,需要另外实现
参数4:void *arg 作为参数3线程函数的输入参数,无输入参数则写NULL
对于参数4,当需要传输多个参数时,需要将多个参数打包作为一个结构体来传入。
像大多数函数一样,此函数成功返回0;第四个参数看着是个指针类型,但还是能传入数值的,使用强制类型转化即可。
线程的有序执行——信号量
关于信号量
信号量类似一个标志量,本质是一个数值,用于协调各个线程有序的占用资源。信号量起到通知的作用,目的是让线程可以按照我们所想的顺序来执行。
- 信号量初始化
int sem_init(sem_t *sem, int pshared, unsigned int value);
头文件:#include <semaphore.h>
功能:初始化信号量
参数1:sem_t *sem,信号量的结构体,需提前定义一个信号量sem_t *
参数2:int pshared, 用于设置线程间共享还是进程间共享,写0表示线程共享,1表示进程共享,这里写0
参数3:unsigned int value,设置信号量的初始值,一般写0或1,0表示为其分配资源,1表示不分配
- 信号量等待 ,也叫P操作
int sem_wait(sem_t *sem);
头文件:#include <semaphore.h>
功能:让线程挂起等待直到信号量的值合适, sem值为0时阻塞(等待),sem值为非0时执行操作并返回。阻塞的好处就是在阻塞时不会占用cpu资源。
参数:sem_t *sem,信号量
//具体实现类似这样
while(!sem)
{
if(sem != 0)
{
sem--;
break;
}
}
- 信号量释放 , 也叫V操作
int sem_post(sem_t *sem);
头文件:#include <semaphore.h>
功能:释放信号量守护的资源,使得其他线程的sem_wait()可以正常访问资源。
参数:sem_t *sem,信号量
//具体实现就是让信号量加一,类似 sem++;
sem_wait和sem_post函数经常搭配使用,来协调线程对资源的访问。
- 获取当前线程tid号
pthread_t tid = pthread_self();//获取当前线程的 tid 号
信号量的摧毁
#include <pthread.h>
int sem_destroy(sem_t *sem);
成功:返回 0
使用完信号量需要对其摧毁。
线程的退出与回收
线程的退出有三种方式:
- 进程结束时所有线程都会结束。
- 线程自身调用pthread_exit函数来主动退出。
void pthread_exit(void *retval);
pthread_exit 函数为线程退出函数,在退出时候可以传递一个 void*类
型的数据带给主线程,若选择不传出数据,可将参数填充为 NULL。
注意:线程是个函数,如果要返回内容则返回的变量不能是临时变量否则线程结束则临时变量自动回收,应该用static变量
或者全局变量等。
- 线程调用pthread_cancel函数来退出其他线程。
int pthread_cancel(pthread_t thread);
输入参数为线程tid号,函数执行成功返回0
那么信号量如何控制线程的执行顺序呢?参考以下代码:
#define _GNU_SOURCE
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <semaphore.h>
sem_t sem1,sem2,sem3;//申请的三个信号量变量
void *fun1(void *arg)
{
sem_wait(&sem1);//因 sem1 本身有资源,所以不被阻塞 获取后 sem1-1 下次会会阻塞
printf("%s:Pthread Come!\n",__FUNCTION__);
sem_post(&sem2);// 使得 sem2 获取到资源
pthread_exit(NULL);
}
void *fun2(void *arg)
{
sem_wait(&sem2);//因 sem2 在初始化时无资源会被阻塞,直至 14 行代码执行 不被阻塞 sem2-1 下次会阻塞
printf("%s:Pthread Come!\n",__FUNCTION__);
sem_post(&sem3);// 使得 sem3 获取到资源
pthread_exit(NULL);
}
void *fun3(void *arg)
{
sem_wait(&sem3);//因 sem3 在初始化时无资源会被阻塞,直至 22 行代码执行 不被阻塞 sem3-1 下次会阻塞
printf("%s:Pthread Come!\n",__FUNCTION__);
sem_post(&sem1);// 使得 sem1 获取到资源
pthread_exit(NULL);
}
int main()
{
int ret;
pthread_t tid1,tid2,tid3;
ret = sem_init(&sem1,0,1); //初始化信号量 1 并且赋予其资源
if(ret < 0){
perror("sem_init");
return -1;
}
ret = sem_init(&sem2,0,0); //初始化信号量 2 让其阻塞
if(ret < 0){
perror("sem_init");
return -1;
}
ret = sem_init(&sem3,0,0); //初始化信号 3 让其阻塞
if(ret < 0){
perror("sem_init");
return -1;
}
ret = pthread_create(&tid1,NULL,fun1,NULL);//创建线程 1
if(ret != 0){
perror("pthread_create");
return -1;
}
ret = pthread_create(&tid2,NULL,fun2,NULL);//创建线程 2
if(ret != 0){
perror("pthread_create");
return -1;
}
ret = pthread_create(&tid3,NULL,fun3,NULL);//创建线程 3
if(ret != 0){
perror("pthread_create");
return -1;
}
/*回收线程资源*/
pthread_join(tid1,NULL);
pthread_join(tid2,NULL);
pthread_join(tid3,NULL);
/*销毁信号量*/
sem_destroy(&sem1);
sem_destroy(&sem2);
sem_destroy(&sem3);
return 0;
}
//代码源自百问网教程
线程的回收
阻塞方式
int pthread_join(pthread_t thread, void **retval); //阻塞方式
参数1是,被回收的线程pid号
参数2是,被回收线程返回的数据,也就是exit函数的参数返回
非阻塞方式
int pthread_tryjoin_np(pthread_t thread, void **retval); //非阻塞方式
参数1是,被回收的线程pid号
参数2是,被回收线程返回的数据,也就是exit函数的参数返回
通过返回值判断是否回收掉线程,成功回收则返回 0。
比较于阻塞方式,通过函数 pthread_tryjoin_np,使用非阻塞回收,线程可以根据退出先
后顺序自由的进行资源的回收。
互斥量(互斥锁)
线程的执行是无序的,很可能多个线程就同时对一个共享资源进行了操作,为了防止这种事情,有了互斥量,互斥量的作用是防止多个线程同时访问共享资源。在线程里,我们要对资源操作时,先上一把锁,表示此时已经有线程在操作了,让其他想要对此资源动手的线程先等一等(阻塞),等我操作完了就解锁,此时其他线程就可以动手了。
- 互斥量的初始化,互斥量一般申请全局变量
int pthread_mutex_init(phtread_mutex_t *mutex, const pthread_mutexattr_t *restrict attr);
类似信号量的初始化,第一个参数是互斥量,第二个参数是互斥量属性一般写NULL,函数成功返回0
也可以用宏初始化:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITALIZER;
-
互斥量上锁与解锁
int pthread_mutex_lock(pthread_mutex_t *mutex);//(阻塞方式)
int pthread_mutex_unlock(pthread_mutex_t *mutex);
当某一个线程成功执行 lock 函数获取到执行权时,其余线程遇到 lock 函数时候会发生阻塞,直至获取资源的线程执行 unlock 函数后。unlock 函数会唤醒其他正在等待互斥量的线程。
需要注意的是:上锁用完必须解锁,不然会导致其他线程一直阻塞在lock处,造成“死锁”现象。 -
互斥量的销毁
int pthread_mutex_destory(pthread_mutex_t *mutex);
成功返回0
互斥量的使用,参考以下代码:
#define _GNU_SOURCE
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
pthread_mutex_t mutex;//互斥量变量 一般申请全局变量
int Num = 0;//公共临界变量
void *fun1(void *arg)
{
pthread_mutex_lock(&mutex);//加锁 若有线程获得锁,则会阻塞
while(Num < 3){
Num++;
printf("%s:Num = %d\n",__FUNCTION__,Num);
sleep(1);
}
pthread_mutex_unlock(&mutex);//解锁
pthread_exit(NULL);//线程退出 pthread_join 会回收资源
}
void *fun2(void *arg)
{
pthread_mutex_lock(&mutex);//加锁 若有线程获得锁,则会阻塞
while(Num > -3){
Num--;
printf("%s:Num = %d\n",__FUNCTION__,Num);
sleep(1);
}
pthread_mutex_unlock(&mutex);//解锁
pthread_exit(NULL);//线程退出 pthread_join 会回收资源
}
int main()
{
int ret;
pthread_t tid1,tid2;
ret = pthread_mutex_init(&mutex,NULL);//初始化互斥量
if(ret != 0){
perror("pthread_mutex_init");
return -1;
}
ret = pthread_create(&tid1,NULL,fun1,NULL);//创建线程 1
if(ret != 0){
perror("pthread_create");
return -1;
}
ret = pthread_create(&tid2,NULL,fun2,NULL);//创建线程 2
if(ret != 0){
perror("pthread_create");
return -1;
}
pthread_join(tid1,NULL);//阻塞回收线程 1
pthread_join(tid2,NULL);//阻塞回收线程 2
pthread_mutex_destroy(&mutex);//销毁互斥量
return 0;
}
//代码源于百问网
条件变量
作用:使线程的执行具有某种条件性。线程满足条件前阻塞在原地等待条件满足,待条件满足后再从阻塞的地方继续执行。
条件变量的使用要与互斥量一起使用。
使用的函数 (函数成功返回0)
-
条件变量的初始化
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
或者
int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr);//cond_at tr 通常为 NULL
-
条件变量的销毁
int pthread_cond_destroy(pthread_cond_t *cond);
-
条件变量的等待函数:
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
功能:其实就是用于阻塞等待的,等待条件满足时会继续执行
参数:条件变量和互斥量
等待函数执行时,自动解锁并进入阻塞状态,等待条件满足;
等到条件满足时,会自动再上锁,然后从阻塞地方继续执行。
此函数需要搭配互斥量一起使用,使用方式如下:
void* consumer(void* arg)
{
// 消费者逻辑
// ...
// 获取互斥量
pthread_mutex_lock(&mutex);
// 检查条件并等待条件满足 使用方式一般是放入while循环中:
while (shared_resource == 0) {
pthread_cond_wait(&condition, &mutex);
}
// 修改共享资源
//...
// 释放互斥量
pthread_mutex_unlock(&mutex);
pthread_exit(NULL);
}
- 通知条件变量
int pthread_cond_signal(pthread_cond_t *cond);
功能:随机唤醒一个等待条件的线程,唤醒哪一个不得而知,具体要看系统实现
参数:条件变量
int pthread_cond_broadcast(pthread_cond_t *cond);
功能:唤醒所有等待条件的线程,然后被唤醒的线程会竞争互斥锁,只会有一个竞争成功上锁
参数:条件变量
条件变量的使用有一类经典问题:生产者—消费者问题,应用代码如下:
#include <stdio.h>
#include <pthread.h>
#define BUFFER_SIZE 10
int buffer[BUFFER_SIZE];
int count = 0;
pthread_mutex_t mutex;
pthread_cond_t cond;
void* producer(void* arg) {
for (int i = 0; i < 20; i++) {
pthread_mutex_lock(&mutex);
// 等待缓冲区有空位
while (count == BUFFER_SIZE) {
pthread_cond_wait(&cond, &mutex);
}
buffer[count] = i;
count++;
printf("Producer produced: %d\n", i);
// 通知消费者有数据可用
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mutex);
}
pthread_exit(NULL);
}
void* consumer(void* arg) {
for (int i = 0; i < 20; i++) {
pthread_mutex_lock(&mutex);
// 等待缓冲区有数据
while (count == 0) {
pthread_cond_wait(&cond, &mutex);
}
int data = buffer[count - 1];
count--;
printf("Consumer consumed: %d\n", data);
// 通知生产者有空位可用
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mutex);
}
pthread_exit(NULL);
}
int main() {
pthread_t producerThread, consumerThread;
// 初始化互斥锁和条件变量
pthread_mutex_init(&mutex, NULL);
pthread_cond_init(&cond, NULL);
// 创建生产者和消费者线程
pthread_create(&producerThread, NULL, producer, NULL);
pthread_create(&consumerThread, NULL, consumer, NULL);
// 等待线程完成
pthread_join(producerThread, NULL);
pthread_join(consumerThread, NULL);
// 销毁互斥锁和条件变量
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cond);
return 0;
}
代码解释:
上述程序创建了一个生产者线程和一个消费者线程。生产者线程不断产生数据并将其放入缓冲区,而消费者线程从缓冲区中取出数据进行消费。使用条件变量来保证缓冲区在满或空时的同步。
在生产者线程中,当缓冲区已满时,它会调用pthread_cond_wait()等待条件满足,此时它会释放互斥锁并等待消费者线程的通知。当有空位可用时,生产者线程会将数据放入缓冲区,并通过pthread_cond_signal()通知消费者线程有数据可用。
在消费者线程中,当缓冲区为空时,它会调用pthread_cond_wait()等待条件满足,此时它会释放互斥锁并等待生产者线程的通知。当有数据可用时,消费者线程会从缓冲区取出数据进行消费,并通过pthread_cond_signal()通知生产者线程有空位可用。
这样,通过条件变量和互斥锁的组合,生产者和消费者线程能够在缓冲区满或空的情况下进行正确的同步,避免竞态条件和数据不一致的问题。
需要注意的是,条件变量的等待和通知操作必须在持有互斥锁的情况下进行,以确保线程在等待和唤醒时能够正确地操作共享数据。同时,生产者和消费者线程必须在互斥锁的保护下对共享数据进行修改,以避免并发访问导致的数据损坏。