安全函数不安全-多线程慎用List.h
linux 开发应该多少都听过大名鼎鼎的 list.h ,其简洁优雅的设计,一个头文件完成了一个高可用的链表。 |
前言
linux 开发应该多少都听过大名鼎鼎的 list.h ,其简洁优雅的设计,一个头文件完成了一个高可用的链表。
但是 list.h 并不是线程安全的,在多线程的情况下使用,必须考虑多线程数据同步的问题。
然而。。。。
我在使用互斥锁对链表的操作进行保护之后,还是被坑了!
下面是把我坑了的 list_for_each_entry 和 list_for_each_entry_safe 两个函数的详细分析。
list.h 单线程使用
在 list.h 这个文件中有非常多值得学习的地方,比如其最经典的 container_of 的实现。
在这里只介绍几个常用的函数,然后重点分析在多线程使用时的碰到的坑。
链表初始化及添加节点
首先定义一个链表和链表节点,定义一个产品,其属性为产品重量(weight)。
typedef struct product_s { struct list_head product_node; uint32_t index; uint32_t weight; }product_t;//初始化链表头 LIST_HEAD(product_list);
生产者在生产完产品后,将产品加入链表,等待消费者使用。
void producer(void) { product_t *product = malloc(sizeof(product_t));// 产品重量为 300 ± 10 product->weight = 290 + rand() % 20;printf("product :%p, weight %d\n", product, product->weight); list_add_tail(&product->product_node, &product_list); }
遍历链表
使用 list_for_each_entry 可以将链表进行遍历:
// 遍历打印链表信息 void print_produce_list(void) { product_t *product; list_for_each_entry(product, &product_list, product_node) { printf("manufacture product :%p, weight %d\n", product, product->weight); } }
其具体实现是使用宏将 for 循环的初始条件和完成条件进行了替换:
#define list_for_each_entry(pos, head, member) \ for (pos = list_first_entry(head, typeof(*pos), member); \ &pos->member != (head); \ pos = list_next_entry(pos, member))
其中for循环的第一个参数将 pos = list_first_entry(head, typeof(*pos), member); 初始化为链表头指向的第一个实体链表成员。
第二个参数 &pos->member != (head) 为跳出条件,当pos->member再次指向链表头时跳出for循环。
for的第三个参数通过pos->member.next指针遍历整个实体链表,当pos->member.next再次指向我们的链表头的时候跳出for循环。
但是 list_for_each_entry 不能在遍历的循环体中删除节点,因为在循环体中删除链表节点后,当前节点的前驱结点和后继结点指针会被置空。
在for循环的第三个参数中,获取下一个节点时,会发生非法指针访问
“安全遍历链表”
为了解决在遍历链表过程中,无法删除结点的问题,在 list.h 中提供了一个安全删除节点的函数
// 删除重量小于300的节点 void remove_unqualified_produce(void) { product_t *product, *temp; list_for_each_entry_safe(product, temp, &product_list, product_node) { // 移除重量小于300的产品 if (product->weight < 300) { printf("remove product :%p, weight %d\n", product, product->weight); list_del(&product->product_node); free(product); } } }
其实现是使用一个中间变量,在开始每次开始执行循环体前,将当前节点的下一个节点保存到中间变量,从而实现"安全"遍历
#define list_for_each_entry_safe(pos, n, head, member) \ for (pos = list_first_entry(head, typeof(*pos), member), \ n = list_next_entry(pos, member); \ &pos->member != (head); \ pos = n, n = list_next_entry(n, member))
多线程中使用list.h
上面我们在主线程里面创建了产品,并放入到链表中并,并过滤了重量小于300的产品。
后面我们在多线程中对产品进行消费(两个线程同时消费链表的数据,使用完成后删除并释放结点)。
这里的逻辑和单线程中的差不多,同样是遍历链表,然后从链表中删除节点。不同的是,由于list.h自身没有带锁,所以需要使用互斥锁将链表的操作进行保护。
于是很自然的有了下面的代码
void * consumer(void *arg) { product_t *product, *temp;// 使用互斥锁对链表进行保护 pthread_mutex_lock(&producer_mutex); list_for_each_entry_safe(product, temp, &product_list, product_node) { list_del(&product->product_node); printf("consume product :%p, weight %d, consumer :%p\n", product, product->weight, (void *)pthread_self()); pthread_mutex_unlock(&producer_mutex);// 睡一会,防止太快了 usleep(10*1000); free(product); pthread_mutex_lock(&producer_mutex); } pthread_mutex_unlock(&producer_mutex);return NULL; }
在上面的这段代码中,在对链表操作时,使用互斥锁对链表进行了保护,使同时只能有一个线程访问链表。
不过你以为这样就好了嘛,如果时这样,这篇文章就没存在的必要了。。。
在两个线程同时遍历时,即便是加了锁之后,数据访问也不安全。
在遍历使用的 list_for_each_entry_safe 宏中,使用了一个零时变量对保存着当前链表的下一个节点。
但是多线程访问链表时,有可能零时变量保存的节点,被另一个线程删除了,所以访问的时候又是 Segmentation fault
后记
原因找到了,也就好办了。以至于解决方法嘛,我是使用一个全局的零时变量,将需要删除节点的下一个节点保存起来,手动实现多线程的"安全删除"。
// 消费者 void * consumer(void *arg) { product_t *product;// 使用互斥锁对链表进行保护 pthread_mutex_lock(&producer_mutex); list_for_each_entry(product, &product_list, product_node) { temp = list_next_entry(product, product_node); list_del(&product->product_node); printf("consume product :%p, weight %d, consumer :%p\n", product, product->weight, (void *)pthread_self()); pthread_mutex_unlock(&producer_mutex);// 睡一会,防止太快 usleep(10*1000); free(product);pthread_mutex_lock(&producer_mutex); if(temp != NULL){ product = list_prev_entry(temp, product_node); } } pthread_mutex_unlock(&producer_mutex);return NULL; }
一个晚上找到了这个bug,然后又花了一个晚上记录下来这个bug。
据说 klist.h 是 list.h 的线程安全版本,后面花时间在研究一下去,今天就先睡了。。。
安全函数不安全-多线程慎用List.h相关推荐
- C语言试题二十五之编写一个函数float function(double h),函数的功能使对变量h中的值保留2位小数,并对第三位进行四舍五入(规定h中的值位正数)。
1. 题目 编写一个函数float function(double h),函数的功能使对变量h中的值保留2位小数,并对第三位进行四舍五入(规定h中的值位正数). 2 .温馨提示 C语言试题汇总里可用于 ...
- python线程池回调函数_python回调函数中使用多线程的方法
下面的demo是根据需求写的简单测试脚本 #!/usr/bin/env python # coding: utf-8 # 第一个列表为依赖组件和版本号,后面紧跟负责人名称 # 接着出现第二个以来组建列 ...
- 添加槽函数出现no documents matching “ui_xxx.h“
添加槽函数出现no documents matching "ui_xxx.h" 在qt creator下添加槽函数出现no documents matching "ui_ ...
- Vue3官网-可复用组合式API(十四)实例 property(\$slots,\$attrs)、渲染函数render(虚拟节点VNode,h() 参数,使用JavaScript代替模板功能),插件
Vue3官网-可复用&组合式API(十四)实例 property($slots,$attrs).渲染函数render(虚拟节点VNode,h() 参数,使用JavaScript代替模板功能), ...
- 线程模型、pthread 系列函数 和 简单多线程服务器端程序
一.线程有3种模型,分别是N:1用户线程模型,1:1核心线程模型和N:M混合线程模型,posix thread属于1:1模型. (一).N:1用户线程模型 "线程实现"建立在&qu ...
- python中run函数作用_python3多线程中如何改写run()函数?
我们对于函数的使用一般是直接根据其作用进行举例讲解,最近偶然的一次多线程的代码练习中,让小编发现在构建多线程的时候,我们也可以对函数进行重写.小编马上进行了这个函数内容的整理,为了让大家能理解前后的内 ...
- vue2.x的h函数(createElement)与vue3中的h函数
1. vue2.x的 h 函数(createElement) 使用方法及介绍:(参考官网提取) h函数第一个是标签名字 或者是组件名字,第二个参数是配置项,第三个参数是 innerText ,不会帮你 ...
- c语言中延时函数delay1,delay什么意思_delay.h这一段是什么意思
delay中文是什么意思 delay 英[dɪˈleɪ] 美[dɪˈle] n. 耽搁; 延迟,拖延; 被耽搁或推迟的时间; vt. 耽搁; 延期,推迟; vi. 延缓,延期; [例句]For sen ...
- c语言createthread函数用法,C++多线程函数CreateThread如何使用?
C++ CreateThread函数如何传递this指针作为参数 C++多线程函数CreateThread如何使用? #include #include using namespace std; /* ...
最新文章
- Python | 用PrettyPrinter,让Python输出更漂亮,你值得拥有
- Tungsten Fabric SDN — Service Chain — Basic Service Chain and Policy-Based Steering
- wxWidgets随笔(9)-utf8~wxString存储二进制数据(4)
- 【收藏】Docker安装微信
- ajax接口一直在重复调用请求是什么原因_为什么RPC超时设置非常重要
- python 爬虫可视化编程_Python爬虫爬取博客实现可视化过程解析
- 转:linux中fork()函数详解
- JS window对象 返回前一个浏览的页面 back()方法,加载 history 列表中的前一个 URL。 语法: window.history.back();...
- Python+tkinter打造类似于IDLE的代码编辑器(83行源码)
- 基于Chrome浏览器的前端调试
- 女儿还有三个月就高考了,最近压力大,特别敏感,容易发火怎么办?
- android滑动fragment,android中ViewPager结合Fragment进行无限滑动
- python 一个包中的文件调用另外一个包文件 实例
- 小组成员的github地址
- VUE前端+Node后台模拟打印机Web即时打印
- 有哪些值得长期坚持下去的好习惯?
- 微服务模式笔记:服务分解策略
- 5000字、12字 连环炮、一张图快速解决线程池
- 【网安神器篇】——enum4linux枚举工具
- 汽车变速器测试系统ETest
热门文章
- 自动微分到底是什么?这里有一份自我简述
- 收下这份来自GitHub的神器,一图搞定Matplotlib!
- 字节跳动的面试题.pdf
- Python标准库queue模块原理浅析
- E: Could not get lock /var/lib/dpkg/lock-frontend - open (11: Resource temporarily unavailable)E: U
- 创业基础(第8章 新企业的创办与管理) 来自高校:全国大学生创新创业实践联盟 分类:创新创业 学习规则:按序学习
- 实验四 JSP数据库编程基础
- Python学习笔记.OS学习笔记 OS操作系统(operating system)(三) 日期和时间
- 通过对比对象掩码建议的无监督语义分割
- 模拟PLC 的圆弧插补方式在VC中绘制圆弧