【嵌入式Linux】嵌入式Linux应用开发基础知识之多线程编程
韦东山嵌入式Linux应用开发基础知识学习笔记嵌入式Linux应用开发基础知识之多线程编程
文章目录
前言
韦东山嵌入式Linux应用开发基础知识学习笔记
文章中大多内容来自韦东山老师的文档,还有部分个人根据自己需求补充的内容
视频教程地址: https://www.bilibili.com/video/BV1kk4y117Tu
1、多线程基础编程–创建线程和使用等待函数休眠线程
这一章韦东山老师的文档里写的很多也很好,所以有需要请直接参阅文档,这里仅仅记录一些简单的函数用法和示例程序分析
1.1、程序分析–使用信号量PV操作sem_wait
初始化信号量
#include <semaphore.h>
int sem_init(sem_t *sem,int pshared,unsigned int value);
该函数可以初始化一个信号量,第一个参数*sem
传入sem_t类型指针
第二个参数pshared
传入0代表线程控制,否则为进程控制
第三个参数value
表示信号量的初始值,0代表阻塞,1代表运行
待初始化结束信号量后,若执行成功会返回0
信号量PV操作(阻塞)
#include <pthread.h>
int sem_wait(sem_t *sem);
int sem_post(sem_t *sem);
sem_wait函数作用为检测指定信号量是否有资源可用,若无资源可用会阻塞等待,若有资源可用会自动的执行“sem-1”的操作。所谓的“sem-1”是与上述初始化函数中第三个参数值一致,成功执行会返回0。
sem_post函数会释放指定信号量的资源,执行“sem+1”操作。
通过以上2个函数可以完成所谓的PV操作,即信号量的申请与释放,完成对线程执行顺序的控制。
初始化互斥量
#include <pthread.h>
int pthread_mutex_init(phtread_mutex_t *mutex, const pthread_mutexattr_t *restrict attr);
该函数初始化一个互斥量,第一个参数 *mutex
是改互斥量指针,第二个参数 *restrict attr
为控制互斥量的属性,一般为NULL。当函数成功后会返回0,代表初始化互斥量成功。
初始化互斥量也可以调用宏来快速初始化,代码如下:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITALIZER;
互斥量加锁/解锁
#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
成功:返回0
lock函数与unlock函数分别为加锁解锁函数,只需要传入已经初始化好的pthread_mutex_t互斥量指针。成功后会返回0。
当某一个线程获得了执行权后,执行lock函数,一旦加锁成功后,其余线程遇到lock函数时候会发生阻塞,直至获取资源的线程执行unlock函数后。unlock函数会唤醒其他正在等待互斥量的线程。
特别注意的是,当获取lock之后,必须在逻辑处理结束后执行unlock,否则会发生死锁现象!导致其余线程一直处于阻塞状态,无法执行下去。在使用互斥量的时候,尤其要注意使用pthread_cancel函数,防止发生死锁现象!
int pthread_cancel(pthread_t thread);
函数用于线程被动退出,该函数传入一个tid号,会强制退出该tid所指向的线程,若成功执行会返回0
线程的创建
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg);
*thread
:用来保存新建线程的线程号
*attr
:表示线程的属性,一般传入NULL表示默认属性
*(*start_routine) (void *)
:一个函数指针,就是线程执行的函数。这个函数返回值为void*,形参为void*
*arg
:表示为向线程处理函数传入的参数,若不传入,可用NULL填充
程序功能:从主线程中接收键盘输入内容保存在全局变量中,在子线程中将变量中的内容输出
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <semaphore.h>
#include <string.h>
static char g_buf[1000];//用来保存键盘输入内容的全局变量
static sem_t g_sem;
//也可以使用 pthread_mutex_init(&g_tMutex, NULL); 来初始化互斥量
static pthread_mutex_t g_tMutex = PTHREAD_MUTEX_INITIALIZER;
static void *my_thread_func (void *data)
{
while (1)
{
/* 等待通知 */
sem_wait(&g_sem);
/* 打印 */
pthread_mutex_lock(&g_tMutex);
printf("recv: %s\n", g_buf);
pthread_mutex_unlock(&g_tMutex);
}
return NULL;
}
int main(int argc, char **argv)
{
pthread_t tid;
int ret;
char buf[1000];
//g_sem:sem_t类型变量 0:线程控制 0:信号量的初始值,0代表阻塞
sem_init(&g_sem, 0, 0);
/* 1. 创建"接收线程" */
//tid:保存新建的线程号 NULL:默认属性 my_thread_func:函数指针 NULL:无参数
ret = pthread_create(&tid, NULL, my_thread_func, NULL);
if (ret)
{
printf("pthread_create err!\n");
return -1;
}
/* 2. 主线程读取标准输入, 发给"接收线程" */
while (1)
{
//获取标准输入存入buf
fgets(buf, 1000, stdin);
//加锁成功会返回0,一旦加锁成功其他线程想给同一个互斥量加锁会发生阻塞
pthread_mutex_lock(&g_tMutex);
memcpy(g_buf, buf, 1000);
//解锁成功会返回0,一旦解锁成功,函数会唤醒其他正在等待互斥量的线程
pthread_mutex_unlock(&g_tMutex);
/* 通知接收线程 */
sem_post(&g_sem);
}
return 0;
}
1.2、程序分析–使用条件变量pthread_cond_wait
创建和销毁条件变量
#include <pthread.h>
// 初始化条件变量
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr);//cond_attr通常为NULL
// 销毁条件变量
int pthread_cond_destroy(pthread_cond_t *cond);
这些函数成功时都返回0
等待条件变量
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
这需要结合互斥量一起使用,示例代码如下:
pthread_mutex_lock(&g_tMutex);
pthread_cond_wait(&g_tConVar, &g_tMutex); // 如果条件不满足则,会unlock g_tMutex
// 条件满足后被唤醒,会lock g_tMutex
/* 操作临界资源 */
pthread_mutex_unlock(&g_tMutex);
pthread_cond_wait
总和一个互斥锁结合使用。在调用pthread_cond_wait
前要先获取锁,因为pthread_cond_wait
函数执行时先自动释放指定的锁,然后等待条件变量的变化,在函数调用返回之前,自动将指定的互斥量重新锁住。
通知条件变量
int pthread_cond_signal(pthread_cond_t *cond);
pthread_cond_signal函数只会唤醒一个等待cond条件变量的线程,示例代码如下:
pthread_cond_signal(&g_tConVar);
pthread_cond_signal
通过条件变量cond发送消息,若多个消息在等待,它只唤醒一个
调用pthread_cond_signal
后要立刻释放互斥锁,因为pthread_cond_wait
的最后一步是要将指定的互斥量重新锁住,如果pthread_cond_signal
之后没有释放互斥锁,pthread_cond_wait
仍然要阻塞。
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <semaphore.h>
#include <string.h>
static char g_buf[1000];
static pthread_mutex_t g_tMutex = PTHREAD_MUTEX_INITIALIZER;
//也可以使用pthread_cond_init(g_tConVar,NULL);来初始化条件变量
static pthread_cond_t g_tConVar = PTHREAD_COND_INITIALIZER;
static void *my_thread_func (void *data)
{
while (1)
{
pthread_mutex_lock(&g_tMutex);
/*
pthread_cond_wait函数先释放g_tMutex
等待条件变量g_tConVar变化后在函数返回之前重新锁定g_tMutex
*/
pthread_cond_wait(&g_tConVar, &g_tMutex);
/* 打印 */
printf("recv: %s\n", g_buf);
pthread_mutex_unlock(&g_tMutex);
}
return NULL;
}
int main(int argc, char **argv)
{
pthread_t tid;
int ret;
char buf[1000];
/* 1. 创建"接收线程" */
ret = pthread_create(&tid, NULL, my_thread_func, NULL);
if (ret)
{
printf("pthread_create err!\n");
return -1;
}
/* 2. 主线程读取标准输入, 发给"接收线程" */
while (1)
{
fgets(buf, 1000, stdin);
pthread_mutex_lock(&g_tMutex);
memcpy(g_buf, buf, 1000);
pthread_cond_signal(&g_tConVar); /* 通知接收线程 */
//调用pthread_cond_signal后要立刻释放互斥锁
pthread_mutex_unlock(&g_tMutex);
}
return 0;
}
2、一些多线程编程实例
2.1、线程的执行顺序是随机竞争的
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
void *fun(void *arg)
{
printf("pthread_New = %lu\n",(unsigned long)pthread_self());
}
int main()
{
pthread_t tid1;
int ret = pthread_create(&tid1,NULL,fun,NULL);
if(ret != 0){
perror("pthread_create");
return -1;
}
/*tid_main 为通过pthread_self获取的线程ID,tid_new通过执行pthread_create成功后tid指向的空间*/
printf("tid_main = %lu tid_new = %lu \n",(unsigned long)pthread_self(),(unsigned long)tid1);
/*因线程执行顺序随机,不加sleep可能导致猪线程先执行,导致进程结束,无法执行到子线程*/
//sleep(1);
return 0;
}
下图中的两次执行之所以会产生不同的结果就是因为线程的执行顺序是随机竞争的,第一次执行时创建子线程后没有输出子线程中的内容因为在子线程输出执行之前主线程就已经结束进而导致子线程结束,而第二次执行子线程顺利执行则说明了线程的执行顺序不受控制
▲线程的执行顺序是随机竞争的
2.2、向线程传入参数
pthread_create()的最后一个参数的为void类型的数据,表示可以向线程传递一个void数据类型的参数,线程的回调函数中可以获取该参数,下面的例子举例了如何向线程传入变量地址与变量值。
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
void *fun1(void *arg)
{
//(int *)arg: a的值
//arg :a的地址
printf("%s:arg = %d Addr = %p\n",__FUNCTION__,*(int *)arg,arg);
}
void *fun2(void *arg)
{
//(int)(long)arg:a的值
//arg :输出是16进制a的值
printf("%s:arg = %d Addr = %p\n",__FUNCTION__,(int)(long)arg,arg);
}
int main()
{
pthread_t tid1,tid2;
int a = 50;
//传递变量a的地址
int ret = pthread_create(&tid1,NULL,fun1,(void *)&a);
if(ret != 0){
perror("pthread_create");
return -1;
}
//传递变量a的值,针对不同位数机器,指针对其字数不同,需要int转化为long再转指针,否则可能会发生警告
ret = pthread_create(&tid2,NULL,fun2,(void *)(long)a);
if(ret != 0){
perror("pthread_create");
return -1;
}
sleep(1);
printf("%s:a = %d Add = %p \n",__FUNCTION__,a,&a);
return 0;
}
2.3、向线程传入结构体地址
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
struct Stu{
int Id;
char Name[32];
float Mark;
};
void *fun1(void *arg)
{
struct Stu *tmp = (struct Stu *)arg;
printf("%s:Id = %d Name = %s Mark = %.2f\n",__FUNCTION__,tmp->Id,tmp->Name,tmp->Mark);
}
int main()
{
pthread_t tid1,tid2;
struct Stu stu;
stu.Id = 10000;
strcpy(stu.Name,"ZhangSan");
stu.Mark = 94.6;
int ret = pthread_create(&tid1,NULL,fun1,(void *)&stu);
if(ret != 0){
perror("pthread_create");
return -1;
}
printf("%s:Id = %d Name = %s Mark = %.2f\n",__FUNCTION__,stu.Id,stu.Name,stu.Mark);
sleep(1);
return 0;
}
2.4、线程的退出与回收
线程的退出情况有三种:第一种是进程结束,进程中所有的线程也会随之结束。第二种是通过函数pthread_exit来主动的退出线程。第三种被其他线程调用pthread_cancel来被动退出。
线程主动退出
#include <pthread.h>
void pthread_exit(void *retval);
pthread_exit函数为线程退出函数,在退出时候可以传递一个void*类型的数据带给主线程,若选择不传出数据,可将参数填充为NULL。
线程被动退出
线程被动退出,其他线程使用该函数让另一个线程退出
#include <pthread.h>
int pthread_cancel(pthread_t thread);
成功:返回0
该函数传入一个tid号,会强制退出该tid所指向的线程,若成功执行会返回0。
线程资源回收(阻塞方式)
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
该函数为线程回收函数,默认状态为阻塞状态,直到成功回收线程后才返回。第一个参数为要回收线程的tid号,第二个参数为线程回收后接受线程传出的数据。
线程资源回收(非阻塞方式)
#define _GNU_SOURCE
#include <pthread.h>
int pthread_tryjoin_np(pthread_t thread, void **retval);
该函数为非阻塞模式回收函数,通过返回值判断是否回收掉线程,成功回收则返回0,其余参数与pthread_join一致。
程序实例
程序功能:向子线程中传入变量在子线程中自加后子线程主动退出将自加后变量返回给主线程,主线程以阻塞方式回收子线程资源
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
void *fun1(void *arg)
{
static int tmp = 0;//必须要static修饰,否则pthread_join无法获取到正确值
tmp = *(int *)arg;
tmp+=100;
printf("%s:Addr = %p tmp = %d\n",__FUNCTION__,&tmp,tmp);
//线程主动退出
pthread_exit((void *)&tmp);
}
int main()
{
pthread_t tid1;
int a = 50;
void *Tmp = NULL;//用来接收线程回收后传递出来的数据
int ret = pthread_create(&tid1,NULL,fun1,(void *)&a);
if(ret != 0){
perror("pthread_create");
return -1;
}
//以阻塞方式完成线程资源回收
pthread_join(tid1,&Tmp);
printf("%s:Addr = %p Val = %d\n",__FUNCTION__,Tmp,*(int *)Tmp);
return 0;
}
程序功能:使用非阻塞方式回收线程
#define _GNU_SOURCE
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
void *fun(void *arg)
{
printf("Pthread:%d Come !\n",(int )(long)arg+1);
pthread_exit(arg);
}
int main()
{
int ret,i,flag = 0;
void *Tmp = NULL;
pthread_t tid[3];
for(i = 0;i < 3;i++){//创建3个线程
ret = pthread_create(&tid[i],NULL,fun,(void *)(long)i);
if(ret != 0){
perror("pthread_create");
return -1;
}
}
while(1){//通过非阻塞方式收回线程,每次成功回收一个线程变量自增,直至3个线程全数回收
for(i = 0;i <3;i++){
if(pthread_tryjoin_np(tid[i],&Tmp) == 0){
printf("Pthread : %d exit !\n",(int )(long )Tmp+1);
flag++;
}
}
if(flag >= 3) break;
}
return 0;
}
▲程序运行
程序功能:使用被动退出方式在线程1中kill线程2
#define _GNU_SOURCE
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
void *fun1(void *arg)
{
printf("Pthread:1 come!\n");
while(1){
sleep(1);
}
}
void *fun2(void *arg)
{
printf("Pthread:2 come!\n");
pthread_cancel((pthread_t )(long)arg);//杀死线程1,使之强制退出
pthread_exit(NULL);
}
int main()
{
int ret,i,flag = 0;
void *Tmp = NULL;
pthread_t tid[2];
ret = pthread_create(&tid[0],NULL,fun1,NULL);
if(ret != 0){
perror("pthread_create");
return -1;
}
sleep(1);
ret = pthread_create(&tid[1],NULL,fun2,(void *)tid[0]);//传输线程1的线程号
if(ret != 0){
perror("pthread_create");
return -1;
}
while(1){//通过非阻塞方式收回线程,每次成功回收一个线程变量自增,直至2个线程全数回收
for(i = 0;i <2;i++){
if(pthread_tryjoin_np(tid[i],NULL) == 0){
printf("Pthread : %d exit !\n",i+1);
flag++;
}
}
if(flag >= 2) break;
}
return 0;
}
2.5、线程的控制
2.5.1、多线程编临界资源访问
当线程在运行过程中,去操作公共资源,如全局变量的时候,可能会发生彼此“矛盾”现象。例如线程1企图想让变量自增,而线程2企图想要变量自减,两个线程存在互相竞争的关系导致变量永远处于一个“平衡状态”,两个线程互相竞争,线程1得到执行权后将变量自加,当线程2得到执行权后将变量自减,变量似乎永远在某个范围内浮动,无法到达期望数值
程序功能:程序试图在主线程创建一个子线程让传入参数自加到3后退出和另一个子线程让传入参数自减到-3后退出,但是以下程序无法实现此功能
#define _GNU_SOURCE
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
int Num = 0;
void *fun1(void *arg)
{
while(Num < 3){
Num++;
printf("%s:Num = %d\n",__FUNCTION__,Num);
sleep(1);
}
pthread_exit(NULL);
}
void *fun2(void *arg)
{
while(Num > -3){
Num--;
printf("%s:Num = %d\n",__FUNCTION__,Num);
sleep(1);
}
pthread_exit(NULL);
}
int main()
{
int ret;
pthread_t tid1,tid2;
ret = pthread_create(&tid1,NULL,fun1,NULL);
if(ret != 0){
perror("pthread_create");
return -1;
}
ret = pthread_create(&tid2,NULL,fun2,NULL);
if(ret != 0){
perror("pthread_create");
return -1;
}
pthread_join(tid1,NULL);
pthread_join(tid2,NULL);
return 0;
}
▲程序运行截图
为了解决上述对临界资源的竞争问题,pthread线程引出了互斥锁来解决临界资源访问。通过对临界资源加锁来保护资源只被单个线程操作,待操作结束后解锁,其余线程才可获得操作权。
2.5.2、互斥锁API简述
在1.1中介绍了初始化互斥量和互斥量加锁/解锁,这里补充介绍互斥量加锁(非阻塞方式)和互斥量销毁
互斥量加锁(非阻塞方式)
互斥量加锁(非阻塞)
#include <pthread.h>
int pthread_mutex_trylock(pthread_mutex_t *mutex);
该函数同样也是一个线程加锁函数,但该函数是非阻塞模式通过返回值来判断是否加锁成功,用法与上述阻塞加锁函数一致。
互斥量销毁
#include <pthread.h>
int pthread_mutex_destory(pthread_mutex_t *mutex);
成功:返回0
该函数是用于销毁互斥量的,传入互斥量的指针,就可以完成互斥量的销毁,成功返回0。
程序功能:在主线程创建一个子线程让传入参数自加到3后退出和另一个子线程让传入参数自减到-3后退出
#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);
}
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);
}
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);
if(ret != 0){
perror("pthread_create");
return -1;
}
ret = pthread_create(&tid2,NULL,fun2,NULL);
if(ret != 0){
perror("pthread_create");
return -1;
}
pthread_join(tid1,NULL);
pthread_join(tid2,NULL);
pthread_mutex_destroy(&mutex);
return 0;
}
2.5.2、多线程编执行顺序控制
从2.1中可以知道线程的执行顺序是随机竞争的,如果想要顺序执行就要借助信号量来执行
2.5.2.1、信号量API简述
在1.1中已经介绍了初始化信号量和信号量P/V操作,下面将介绍信号量申请(非阻塞方式)和信号量销毁
信号量申请(非阻塞方式)
#include <pthread.h>
int sem_trywait(sem_t *sem);
成功:返回0
此函数是信号量申请资源的非阻塞函数,功能与sem_wait一致,唯一区别在于此函数为非阻塞。
信号量销毁
#include <pthread.h>
int sem_destory(sem_t *sem);
成功:返回0
该函数为信号量销毁函数,执行过后可将信号量进行销毁。
程序功能:在主线程创建三个子线程并使用信号量P/V操作让子线程顺序执行
#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;
}
2.5.2.2、条件变量
见1.2
2.6、使用流程图
▲线程使用流程图
▲互斥量使用流程图
▲信号量使用流程图
总结
▲多线程编程函数使用脑图
参考资料
更多推荐
所有评论(0)