Toggle navigation
首页
问答
文章
积分商城
专家
专区
更多专区...
文档中心
返回主站
搜索
提问
会员
中心
登录
注册
ART-Pi Smart
POSIX
【ART-PI Smart 抛砖引玉 三】基于POSIX的应用开发之互斥锁
发布于 2022-05-12 21:26:02 浏览:771
订阅该版
[tocm] # POSIX 多线程同步之互斥锁 ## 多线程竞争问题 上一篇文章讲到的pthread多线程程序设计,给应用程序的并发执行提供了支持,并且能够尽可能的利用CPU的资源,这要比单线程(进程)的程序要具备很多优越性。 但是多线程应用在执行过程中带来的风险也是必须要考虑的,最明显的问题就是多个线程如果在同一时刻同时访问内存上的某一个全局变量,并且需要对该地址上的内容进行修改时,如果对多线程访问过程中不加以协调,将会出现内容错乱的严重情况。因此,必须采用某种机制,来确保多个线程不会同时修改同一变量,或者某一线程不会读取正由其他线程修改的变量。 我们把多个线程访问某一共享资源的代码片段称为临界区(critical section) ,临界区的代码,在执行过程中必须保证不能被另一个操作打断,否则将无法保证共享资源的正确性。这种操作过程,被应为原子(atomic)操作,即同时访问同一共享资源的其他线程不应中断该片段的执行。 ## 互斥锁的引入 为了解决上述多线程访问共享资源所产生的的竞争问题,pthread标准中加入了互斥锁(mutex mutual exclusion)机制来解决该问题。互斥锁(mutex)存在的本质是保证临界区的原子性。 互斥量有两种状态:已锁定( locked)和未锁定( unlocked)。任何时候,至多只有一个线程可以锁定该互斥量。试图对已经锁定的某一互斥量再次加锁,将可能阻塞线程或者报错失败,具体取决于加锁时使用的方法。一旦线程锁定互斥量,随即成为该互斥量的所有者。只有所有者才能给互斥量解锁。一般情况下,对每一共享资源(可能由多个相关变量组成)会使用不同的互斥量,每一个线程在访问共享资源时将采用下面通用流程: - 针对共享资源锁定互斥量。 - 访问共享资源。 - 对互斥量解锁。  如果多个线程试图执行这一代码块(一个临界区),事实上只有一个线程能够持有该互斥量(其他线程将遭到阻塞),即同时只有一个线程能够进入这段代码区域。 ## MUTEX基本操作API ### MUTEX静态初始化 使用宏 PTHREAD_MUTEX_INITIALIZER可以对互斥锁在创建的同时进行静态初始化。 互斥锁使 用 pthread_mutex_t数据类型 表示 ,pthread_mutex_t其实是一个 结构体类型 , 而宏 PTHREAD_MUTEX_INITIALIZER 其实是一个对结构体赋值操作的封装,并且使用互斥锁的默认属性。 ```c #define PTHREAD_MUTEX_INITIALIZER { { 0, 0, 0, 0, 0, __PTHREAD_SPINS, { 0, 0 } } } ``` 具体初始化互斥锁的操作如下: ```c pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; ``` ### MUTEX动态初始化 除了使用宏进行静态初始化之外,还可以先定义互斥锁,然后在合适位置进行初始化,或者在堆中动态分配的互斥锁,譬如使用 malloc()函数申请分配的互斥锁对象,那么在这些情况下,可以使用初始化函数对互斥锁进行初始化(使用该函数需要包含头文件
),函数原型如下: ```c int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr); ``` 函数参数和返回值含义如下: - mutex: pthread_mutex_t 类型指针, 指向需要进行初始化操作的互斥锁对象。 - attr: pthread_mutexattr_t 类型指针,指向一个 pthread_mutexattr_t 类型对象。 其中pthread_mutexattr_t 类型对象用于定义互斥锁的属性,若将参数 attr 设置为 NULL,则表示将互斥锁的属性设置为默认值,在这种情况下其实就等价于PTHREAD_MUTEX_INITIALIZER 这种方式初始化,而不同之处在于,使用宏不进行错误检查。 返回值: 成功返回 0;失败将返回一个非 0 的错误码。 ### MUTEX上锁与解锁 初始化后的互斥锁处于未锁定状态,在线程临界区起始调用函数 pthread_mutex_lock()可以获取互斥锁的使用权同时实现对互斥锁的加锁,在临界区退出时,调用函数 pthread_mutex_unlock()可以对互斥锁解锁、释放互斥锁。其函数原型如下所示: ```c int pthread_mutex_lock(pthread_mutex_t *mutex); int pthread_mutex_unlock(pthread_mutex_t *mutex); ``` 调用 pthread_mutex_lock()函数对互斥锁进行上锁,如果互斥锁处于未锁定状态,则此次调用会上锁成 功,函数调用将立马返回;如果互斥锁此时已经被其它线程锁定了,那么调用 pthread_mutex_lock()会一直 阻塞,直到该互斥锁被解锁,到那时,调用将锁定互斥锁并返回。 调用 pthread_mutex_unlock()函数将已经处于锁定状态的互斥锁进行解锁。 在使用互斥锁过程中,需要注意不能出现对锁定状态的互斥锁进行解锁操作 ,同时也不能解锁由其它线程锁定的互斥锁 。在多线程抢锁的过程中,无法确定哪个线程能有机会得到互斥锁的所有权,这取决于系统调度等因素。 ### MUTEX超时上锁 互斥锁已经被其它线程锁住时,调用 pthread_mutex_lock()函数会被阻塞,直到互斥锁解锁;如果线程不希望被阻塞,可以使用 pthread_mutex_trylock()函数;调用 pthread_mutex_trylock()函数尝试对互斥锁进行加锁,如果互斥锁处于未锁住状态,那么调用 pthread_mutex_trylock()将会锁住互斥锁并立马返回,如果互斥锁已经被其它线程锁住,调用 pthread_mutex_trylock()加锁失败,但不会阻塞,而是返回错误码 EBUSY。 其函数原型如下所示: ```c int pthread_mutex_trylock(pthread_mutex_t *mutex); ``` 参数 mutex 指向目标互斥锁,成功返回 0,失败返回一个非 0 值的错误码,如果目标互斥锁已经被其它 线程锁住,则调用失败返回 EBUSY。 ### 销毁互斥锁 当不再需要互斥锁时, 应该将其销毁,通过调用 pthread_mutex_destroy()函数来销毁互斥锁,其函数原 型如下所示: ```c int pthread_mutex_destroy(pthread_mutex_t *mutex); ``` 使用该函数需要包含头文件
,参数 mutex 指向目标互斥锁;同样在调用成功情况下返回 0, 失败返回一个非 0 值的错误码。同时还需要注意一下两点: - 不能销毁还没有解锁的互斥锁,否则将会出现错误。 - 没有初始化的互斥锁也不能销毁。 被 pthread_mutex_destroy()销毁之后的互斥锁,就不能再对它进行上锁和解锁了,需要再次调用pthread_mutex_init()对互斥锁进行初始化之后才能使用。 ### MUTEX属性设置 对于互斥锁,还可以定义互斥锁的相关属性,具体属性设置本文暂时不做涉及。 ## 互斥锁在“生产者—消费者”多线程模型中的使用 生产者-消费者模型是操作系统多线程资源同步访问的一个标准情景,在该模型下,可以用来模拟测试各种线程同步机制对多线程对共享资源的竞争访问流程,是测试和研究多线程同步较好的实例。本设计采用mutex来对共享数据资源进行保护,实现生产者线程和消费者线程的同步。 ### 共享资源对象设计 首先设计共享资源对象,具体数据结构如下: ```C struct share_obj { pthread_mutex_t* mutex; int* buf; int len; int pos; int nextval; }; typedef struct share_obj shared_t; ``` 该数据结构包含了共享资源的地址,数据块长度,当前数据存放的位置,已经下一个要存放的值,并且包含了一个互斥锁指针,用来实现对自身共享数据的保护。 ### 共享数据块的创建 带互斥锁保护的共享数据块,采用动态内存分配的方式来创建共享数据块对象句柄,这种方式较为灵活,可以根据需求创建符合需求的数据块大小。该函数入口参数是需要创建的数据块的大小(默认int型数据),返回创建好的共享数据块的句柄指针 ```C shared_t* shared_create(int size) { shared_t* shr = NULL; if(size==0) { printf("[error] init shared buf size invaild!\r\n"); return NULL; } shr = (shared_t*)malloc(sizeof(struct critical_obj)); if(shr == NULL) { printf("[error] malloc shared error!\r\n"); return NULL; } shr->mutex = NULL; shr->buf = NULL; shr->len = 0; shr->pos = 0; shr->nextval = 0; shr->mutex = (pthread_mutex_t*)malloc(sizeof(pthread_mutex_t)); if(pthread_mutex_init(shr->mutex, NULL) != 0) { printf("[error] init shared mutex error!\r\n"); return NULL; } shr->buf = (int*)malloc(sizeof(int)*size); if(shr->buf == NULL) { printf("[error] malloc shared buf error!\r\n"); return NULL; } memset(shr->buf,0,size); shr->len=size; return shr; } ``` ### 共享数据块的销毁 动态分配的数据块对象句柄,在这个应用程序退出时,应该把从系统内配的内存统一释放归还给系统,避免出现不可预测内存问题。所以,在程序结束时,调用该销毁函数,实现对共享资源的释放。 ```C int shared_destroy(shared_t* shr) { if(shr == NULL) { printf("[error] destroy shared error!\r\n"); return -1; } pthread_mutex_destroy(shr->mutex); free(shr->mutex); shr->mutex=NULL; free(shr->buf); shr->buf = NULL; free(shr); shr = NULL; return 0; } ``` ### 生产者线程设计 在该情境下,生产者线程在获取到共享数据块的互斥锁之后,即可以循环往数据块内部填入对应的数据,并且更新当前数据写入位置与下一次要填入的数据。并且,在执行完临界区操作之后,对从主线程传入生产者子线程的一个计数变量进行累加,该变量从主线程传入子线程,用来累计生产者线程在执行完数据写入之后总的写入次数,对于多生产者线程的应用情境下,如果共享数据块的互斥锁机制正常起作用,最终在主线程退出时,各个生产者子线程的所有计数和应该等于共享内存数据长度,及代表各个子线程在访问和操作共享资源的过程中,合理的对互斥锁进行了获取和释放。 每次数据写入后,让线程随机休眠一段时间,来模拟不同生产者线程对共享资源访问的随机性。当数据写入位置到数据块的末尾时,生产者线程则退出。 ```c static void* produce_entry(void* argc) { while(1) { pthread_mutex_lock(shared_handle->mutex); if(shared_handle->pos >= shared_handle->len) { pthread_mutex_unlock(shared_handle->mutex); break; } shared_handle->buf[shared_handle->pos] = shared_handle->nextval; shared_handle->pos++; shared_handle->nextval++; pthread_mutex_unlock(shared_handle->mutex); *((int* )argc)+=1; } sleep(rand()%3); return NULL; } ``` ### 消费者线程设计 消费者线程会按照创建的共享数据块的长度来遍历该共享资源内的数据,在没有访问到数据末尾的时候,利用每次把数据取出进行数据比对校验来模拟消费者线程对共享资源的访问和操作,如果数据比对失败,则意味着生产者填入的某个数据出现了错误。消费者线程用随机睡眠时间,来模拟消费者对共享资源的访问随机效果。 ```c static void* consume_entry(void* argc) { for(int i=0; i
len; i++) { while(1) { pthread_mutex_lock(shared_handle->mutex); if(i < shared_handle->pos) { pthread_mutex_unlock(shared_handle->mutex); break; } if(shared_handle->buf[i] != i) { printf("[error] shared_handle->buf[%d] : %d \r\n", i, shared_handle->buf[i]); } pthread_mutex_unlock(shared_handle->mutex); } sleep(rand()%3); } return NULL; } ``` ### 主程序设计 在主程序之前,首先定义了宏来表示要定义的实际共享数据块的大小以及要创建的生产者线程的数量。 ``` #define MAXITEM 10000 #define MAX_PRODUCE_THREADS 3 ``` 本设计中,设定共享数据块的大小为10000个int数据的容量,生产者线程数量3,消费者线程数量默认为1。创建完各个线程之后,即把各个线程一次设置为可连接状态,此时主线程会阻塞,直到子线程执行完毕,然后统一打印各个子线程的执行情况,已经总的消费者线程的累计计数值,正如前文所述,如果各个子线程累计数据写入计数值等于共享变量的最大容量(int为单位),则说明共享数据块的互斥锁机制正确的起了作用。最后,在主线程退出之前,调用销毁接口来释放共享数据块的资源。 ```C int mutex_demo(void) { int count_tbl[MAX_PRODUCE_THREADS] = {0}; pthread_t consume_thread; pthread_t produce_thread[MAX_PRODUCE_THREADS]; printf("\n [mutex_demo]\r\n"); shared_handle = shared_create(MAXITEM); if(shared_handle == NULL) { printf("[error] create shared error!\r\n"); return -1; } printf("produce thread : %d\r\n",MAX_PRODUCE_THREADS); printf("consume thread : %d\r\n",1); for(int i=0; i
早晨起来 头像炸裂一样疼 这是大机器的额外馈赠 不是钢铁的错 是神经老了 脆弱不堪 >我不大敢看自己的生活 它坚硬 铉黑 有风镐的锐角 石头碰一碰 就会流血 >我在五千米深处打发中年 我把岩层一次次炸裂 借此 把一生重新组合 >我微小的亲人 远在商山脚下 他们有病 身体落满灰尘 我的中年裁下多少 他们的晚年就能延长多少 >我身体里有炸药三吨 他们是引信部分 就在昨夜 在他们床前 我岩石一样 轰地炸裂一地 ————选自 陈年喜(矿山爆破工/诗人)《炸裂志》
0
条评论
默认排序
按发布时间排序
登录
注册新账号
关于作者
RickFlying
something in the way
文章
10
回答
45
被采纳
0
关注TA
发私信
相关文章
1
移植rt-thread2.1.0缺少components/pthreads里的posix_types.h文件中包含的sy
2
[征集自愿者] POSIX相关章节文档编写
3
RT-Thread POSIX支持
4
POSIX标准接口针对文件系统没有导出fsync
5
POSIX接口的select还没实现?
6
pthread 组建bug反馈
7
RTT中的POSIX支持
8
RT-Thread 如何用POSIX接口
9
使用 RT_USING_POSIX finsh能显示,但不能输入
10
请教、讨论POSIX接口、dfs中的pos和size问题
推荐文章
1
RT-Thread应用项目汇总
2
玩转RT-Thread系列教程
3
国产MCU移植系列教程汇总,欢迎查看!
4
机器人操作系统 (ROS2) 和 RT-Thread 通信
5
五分钟玩转RT-Thread新社区
6
【技术三千问】之《玩转ART-Pi》,看这篇就够了!干货汇总
7
关于STM32H7开发板上使用SDIO接口驱动SD卡挂载文件系统的问题总结
8
STM32的“GPU”——DMA2D实例详解
9
RT-Thread隐藏的宝藏之completion
10
【ART-PI】RT-Thread 开启RTC 与 Alarm组件
热门标签
RT-Thread Studio
串口
Env
LWIP
SPI
AT
Bootloader
Hardfault
CAN总线
FinSH
ART-Pi
DMA
USB
文件系统
RT-Thread
SCons
RT-Thread Nano
线程
MQTT
STM32
RTC
rt-smart
FAL
I2C_IIC
UART
ESP8266
cubemx
WIZnet_W5500
ota在线升级
PWM
BSP
flash
freemodbus
packages_软件包
潘多拉开发板_Pandora
定时器
ADC
GD32
flashDB
socket
编译报错
中断
Debug
rt_mq_消息队列_msg_queue
keil_MDK
ulog
SFUD
msh
C++_cpp
MicroPython
本月问答贡献
RTT_逍遥
10
个答案
3
次被采纳
xiaorui
3
个答案
2
次被采纳
winfeng
2
个答案
2
次被采纳
三世执戟
8
个答案
1
次被采纳
KunYi
8
个答案
1
次被采纳
本月文章贡献
catcatbing
3
篇文章
5
次点赞
lizimu
2
篇文章
9
次点赞
swet123
1
篇文章
4
次点赞
Days
1
篇文章
4
次点赞
YZRD
1
篇文章
2
次点赞
回到
顶部
发布
问题
投诉
建议
回到
底部