dotnet 使用 SemaphoreSlim 可能的内存泄露
在使用 SemaphoreSlim 这个锁,能做到的是指定让任务执行几次,同时提供异步方法,减少线程占用。但异步的方法如果没有用对,会因为异步状态机的引用,而存在内存泄露
在 dotnet 的 SemaphoreSlim 的用法基本上是一个线程调用 WaitAsync 等待其他线程调用 Release 释放,在 Release 方法可以设置释放几次,设置之后就能通过几次的 WaitAsync 方法
调用 WaitAsync 方法,如果使用 await
那么将会出让线程执行权,意思是如果是线程池的线程,可以让线程回到线程池,让这个线程去执行其他任务
因此使用 SemaphoreSlim 的 WaitAsync 方法总体性能比较好
但是如果在调用 WaitAsync 方法之后,其他线程调用了 Release 的代码,那么如何让线程从 WaitAsync 方法之后执行?此时就需要用到线程池的调度了,在调用 Release 之后,将会在某个 WaitAsync 方法,通过线程池分配线程继续执行。但是为了让线程池分配的线程知道是从哪里开始执行,就需要用到异步状态机了
在异步状态机记录当前方法上下文信息,而方法上下文信息是强引用的
看到这里,小伙伴也就知道我说的内存泄露的点在哪了
为了让 WaitAsync 方法能在 Release 之后,在线程继续执行,就需要通过异步状态机记录当前方法上下文信息,但是记录上下文信息就会存在强引用,而且这个引用是静态引用,也就是不会释放,因此如下代码会让对象不会释放
class Foo
{
public void F1()
{
_semaphoreSlim = new SemaphoreSlim(0);
F2();
_semaphoreSlim.Dispose();
_semaphoreSlim = null;
}
private async void F2()
{
await _semaphoreSlim.WaitAsync();
Console.WriteLine("F");
}
private SemaphoreSlim _semaphoreSlim;
}
在调用 F1 方法的时候,将会使用 F2 方法等待 SemaphoreSlim 的释放,在 F2 的 WaitAsync 方法将会被异步状态机引用 Foo 对象
而在 F1 方法最后就干掉了 SemaphoreSlim 对象,此时再也不会有时机可以调用 Release 释放,此时异步状态机不会执行,也就是对 Foo 的引用不会释放,此时就存在内存泄露
我创建了两个 Foo 对象,一个调用了 F1 方法,另一个没有调用,然后放在弱引用对象里面,尝试调用垃圾回收之后判断对象是否存在,如下代码
class Program
{
static void Main(string[] args)
{
F3();
GC.Collect();
GC.WaitForFullGCComplete();
GC.Collect();
Console.WriteLine(_weakReference1.TryGetTarget(out _));
Console.WriteLine(_weakReference2.TryGetTarget(out _));
Console.Read();
}
private static void F3()
{
_weakReference1 = new WeakReference<Foo>(new Foo());
var foo = new Foo();
_weakReference2 = new WeakReference<Foo>(foo);
foo.F1();
}
private static WeakReference<Foo> _weakReference1;
private static WeakReference<Foo> _weakReference2;
}
可以看到输出是
False
True
也就是调用了 F1 方法的 Foo 对象不会被释放,而没有创建出来就放在 _weakReference1
的 Foo 对象被释放
因此我对官方的文档的说法有了理解
Always call Dispose before you release your last reference to the SemaphoreSlim. Otherwise, the resources it is using will not be freed until the garbage collector calls the SemaphoreSlim object's Finalize method.
这是官方文档 SemaphoreSlim.Dispose Method (System.Threading)
这句话 Always call Dispose before you release your last reference to the SemaphoreSlim
的本质意思就是在调用 Dispose 之前需要编程开发者确保已经释放完成了所有的任务。同时官方文档也说到,调用 SemaphoreSlim 的 Dispose 方法不是线程安全的
因此安全的方式就是在调用 Dispose 之前先释放,干掉 WaitAsync 的逻辑,就如我在 AsyncQueue 写的代码
/// <summary>
/// 主要用来释放锁,让 DequeueAsync 方法返回,解决因为锁让此对象内存不释放
/// </summary>
public void Dispose()
{
// 当释放的时候,将通过 _queue 的 Clear 清空内容,而通过 _semaphoreSlim 的释放让 DequeueAsync 释放锁
// 此时将会在 DequeueAsync 进入 TryDequeue 方法,也许此时依然有开发者在 _queue.Clear() 之后插入元素,但是没关系,我只是需要保证调用 Dispose 之后会让 DequeueAsync 方法返回而已
_isDisposed = true;
_queue.Clear();
// 释放 DequeueAsync 方法
_semaphoreSlim.Release(int.MaxValue);
_semaphoreSlim.Dispose();
}
本文代码放在github欢迎小伙伴访问
本文会经常更新,请阅读原文: https://blog.lindexi.com/post/dotnet-%E4%BD%BF%E7%94%A8-SemaphoreSlim-%E5%8F%AF%E8%83%BD%E7%9A%84%E5%86%85%E5%AD%98%E6%B3%84%E9%9C%B2.html ,以避免陈旧错误知识的误导,同时有更好的阅读体验。
- MySQL中的统计信息相关参数介绍
- iOS学习——UITableViewCell两种重用方法的区别
- iOS学习——UIPickerView的实现年月选择器
- iOS学习——自动定位
- iOS学习——iOS原生实现二维码扫描
- iOS学习——iOS开发小知识点集合
- iOS学习——@class和#import的区别
- iOS学习——UIView的研究
- iOS学习——布局利器Masonry框架源码深度剖析
- iOS项目——自定义UITabBar与布局
- @FeignClient中的@RequestMapping也被Spring MVC加载的问题解决
- Golang语言中Path包用法
- Golang中container/list包中的坑
- 关于Golang语言数组索引的有趣现象
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- 如何理解直播APP源码开发中的音视频同步
- 在GitHub上发布一个Python项目需要注意哪些
- CS学习笔记 | 21、反向转发通道的建立
- CS学习笔记 | 22、通过SSH开通通道
- 一文搞懂 java -jar 发生了什么
- 树莓派4裸机基础教程:环境搭建
- Oh! Binlog还能这样用之Canal篇
- docker安装elasticsearch和head插件
- redis学习(十六)
- 商业鬼才教你 策略模式
- 为什么StringBuilder是线程不安全的?
- (四)Hive分区、分桶
- Redis的各种数据类型实践-Set
- 快速学习-RocketMQ权限控制
- 快速学习-RocketMQ DefaultMQProducer