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 ,以避免陈旧错误知识的误导,同时有更好的阅读体验。
- C++判断char*的指向
- Linux基础(day18)
- Spring Boot搭建Web应用
- 5.7 vim实践
- Effective Modern C++翻译(7)-条款6:当auto推导出意外的类型时,使用显式的类型初始化语义
- 2.3 ls命令
- Effective Modern C++翻译(6)-条款5:auto比显示的类型声明要更好
- 大白话-prototype属性
- Effective Modern C++翻译(5)-条款4:了解如何观察推导出的类型
- Effective Modern C++翻译(4)-条款3:了解decltype
- 大白话-constructor
- Effective Modern C++翻译(3)-条款2:明白auto类型推导
- React Native在Android平台运行gif的解决方法
- Effective Modern C++翻译(2)-条款1:明白模板类型推导
- 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 数组属性和方法
- 使用 Castle Windsor 实现 Web API 依赖注入
- SparkSQL与Hive metastore Parquet转换
- Spark中广播变量详解以及如何动态更新广播变量
- 按需加载 AngularJS 的 Controller
- Spark SQL中Not in Subquery为何低效以及如何规避
- 踩坑记 | Flutter升级影响了NestedScrollView?
- 使用 RequireJS 加载 AngularJS
- 通过Spark生成HFile,并以BulkLoad方式将数据导入到HBase
- 使用 C 创建 Windows 服务
- AngularJS 中的 controllerAs
- Android-Jetpack笔记-DataBinding
- Android-Jetpack笔记-Lifecycles
- Android-Jetpack笔记-LiveData
- Android-Jetpack笔记-ViewModelSavedState
- Android-Jetpack笔记-ViewModel