是控制台程序不是 ASP.NET Core 程序?
如果不是 ASP.NET Core 程序,你要自己管理 Scoped 的生命周期
@dudu: 不是控制台程序,.net core web程序,我之前看到过你也遇到过生命周期的问题,但是我们的情况不太一样,我这个就是因为多线程的问题,有网关通过TCP连接到服务器,上报数据,程序多线程监听,每个线程监听一个连接,问题就是子线程监听到数据后,调用注入的service处理数据,这时候context已经被释放了,尝试了很多注入方法,都失败了。我尝试做了一个webapi,在子线程监听到数据之后通过HttpWebRequest请求这个api来处理数据是没有问题的,但是这不是解决这个问题最有效的方式
@果冻布丁喜之郎: 在 TCPService 的 receive 方法中不要使用成员变量 _ayomarContentService ,用 IServiceProvider.GetRequiredService 重新解析一个 AyomarContentService 实例
@dudu: 我试过,在主线程构造函数注入IServiceProvider,到子线程GetRequiredService,IServiceProvider会被disposed。我又尝试IServiceCollection重新IServiceProvider serviceProvider来BuildServiceProvider(),但是所有的AyomarContentService里面的包括仓储的实例,DBContext,以及它继承的BaseService里面的所有实例,都要重新在IServiceCollection里注入一次。
@果冻布丁喜之郎: 那就是 TCPService 实例的生命周期问题了,难道你把 TCPService 也注册为 Scoped 了?
@dudu: 也不能这么说,TCPService 实例里的都可以的,就是这个实例里的多线程。主线程是没问题的。TcpService是单例
@果冻布丁喜之郎: TCPService 是单例的话,它的构造函数注入了 AyomarContentService ,AyomarContentService 也相当于是单例了
@果冻布丁喜之郎: 推荐阅读一篇英文博文:ASP.NET Core Dependency Injection Best Practices, Tips & Tricks
@果冻布丁喜之郎: 我觉得问题就出在单例的 TCPService 注入了不是单例的 AyomarContentService
@dudu: AyomarContentService 也是单例
@dudu: 这是TCP和AyomarContentService
这是仓储部分
@果冻布丁喜之郎: 这不是单例,单例是 AddSingleton
@果冻布丁喜之郎: 可以试试全部 AddTransient ,不使用 AddScoped
@dudu: 说错了,不是单例,没有用到过单例,所有的回话都用同一个对象,感觉很危险。基本 DBContext和仓储都是Scoped,这是服务都是Transient
@dudu: Context也用Transient吗?我试试
@dudu: 还是被释放了,全换成了Transient
@dudu: 之前也全部是Transient,在linux会引发警告More than twenty 'IServiceProvider' instances have been created for internal use by Entity Framework. This is commonly caused by injection of a new singleton service instance into every DbContext instance. For example, calling UseLoggerFactory passing in a new instance each time--see https://go.microsoft.com/fwlink/?linkid=869049 for more details. Consider reviewing calls on 'DbContextOptionsBuilder' that may require new service providers to be built. 所以改成了Scoped
@果冻布丁喜之郎: 建议提供一下 AppDbContext 的代码以及 Startup 中 services.AddDbContext 的代码
@果冻布丁喜之郎: services.AddDbContext 也可以指定 ServiceLifetime
@dudu: 刚才全部换成Transient的时候指定了。
这是Setup中的:
这是AppDbContext:
这是仓储中的AppDbContext:
@果冻布丁喜之郎: 能否提供重现这个问题的示例代码放到 github 上,我下午找时间看一下
@dudu: 我放网盘里给你吧,包括数据库。
@dudu: 网盘链接私信给你了
@果冻布丁喜之郎: 将 AppDbContext 注册为单例可以避开这个问题
services.AddDbContext<AppDbContext>(..., ServiceLifetime.Singleton);
暂时还没找到更好的解决方法
@果冻布丁喜之郎: 这个问题实际就是在新建的线程中无法访问被依赖注入容器管理的实现 IDispose 接口的类型的实例(单例除外)
@dudu: 单例我怕同一个Context对象会引起问题。我尝试下mq吧
@果冻布丁喜之郎: 不用单例也能实现,只是比较麻烦,等会我提供几个思路
@果冻布丁喜之郎:
基于现有的代码,推荐的解决方法是:从 Ayomar.Core.ServicesImp.AyomarContentService 下手,通过构造函数注入 DbContextOptions (它是单例),用 DbContextOptions 手工 new AppDbContext 重写 SaveAsync 与 UpdateAsync ,实测有效
public class AyomarContentService : Repository<AyomarContents>, IService.IAyomarContentService
{
private readonly DbContextOptions _options;
public AyomarContentService(AppDbContext Context, DbContextOptions options) : base(Context)
{
_options = options;
}
public override async Task<bool> SaveAsync(AyomarContents entity, bool IsCommit = true)
{
using (var context = new AppDbContext(_options))
{
context.Set<AyomarContents>().Add(entity);
if (IsCommit)
return await context.SaveChangesAsync() > 0;
else
return false;
}
}
public override async Task<bool> UpdateAsync(AyomarContents entity, bool IsCommit = true)
{
using (var context = new AppDbContext(_options))
{
context.Set<AyomarContents>().Attach(entity);
context.Entry<AyomarContents>(entity).State = EntityState.Modified;
if (IsCommit)
return await context.SaveChangesAsync() > 0;
else
return false;
}
}
}
@dudu: 谢谢,我试一下。我昨天尝试用HttpClientFactory访问api,也可以,但是由于TCP节点太多,导致mysql超过连接数了,估计还必须走MQ,我先测下你这个方案。
@果冻布丁喜之郎: 有人在我的博客中留言提供了最简单的解决方法 —— 用 Task.Run() 取代 Thread.Start() ,实测有效:
1)用 await Task.Run(() => watchconnecting());
取代 threadwatch = new Thread(watchconnecting);
部分的代码
1)用 Task.Run(() => receive(connection));
取代 Thread thread = new Thread(pts);
部分的代码
@dudu: 这是用异步取代多线程,我之前也有考虑,但是没有尝试,都是防止阻塞,应该也是个很好的方案,我一会弄完mq 试一下.
@果冻布丁喜之郎: 这不是“异步取代多线程”,Task.Run 会使用新的线程执行任务
@dudu: 实测不行,只能有一个tcp 成功
@果冻布丁喜之郎: 这应该是 Task.Run 使用上的问题
@dudu: 用法应该是这样吧
@果冻布丁喜之郎: 我测试时,没有把 watchconnecting 改为 async
@dudu: 用Task.run执行同步方法确实可行,都能发送数据,不过好像在接收的时候有的时候会阻塞一下,导致接收两次甚至更多的数据,如
@dudu: 这是执行了_ayomarConentServices.SaveAsync().Result;换成 Task.Run(async ()=> await _ayomarConentServices.SaveAsync()).就不会出现这样的问题了。只是很容易,mysql的连接池就满了,这个我自己看下是不是没有及时释放。 非常感谢!
@果冻布丁喜之郎: tcp server 的实现推荐2个参考资料:
@果冻布丁喜之郎: 千万不要在同步方法中以 .Result
调用异步方法
@dudu: 看了这两篇文章,基本和我们改成Task之后是一样的,只不过在启动的时候,他是直接执行了listen(),而我们是执行了 Task.Run(()=>listen()),这样可以在点击“启动TCP服务”按钮之后不会阻塞,给用户提示“TCP启动成功”以及返回 当前连接的客户端列表,更友好一些
@dudu: 你测试的时候有数据到数据库吗?数据库没有数据,我刚断点看了下,用Task.Run()一样 Context被释放
@果冻布丁喜之郎: 的确有问题,正在看
@果冻布丁喜之郎: 的确用 Task.Run 也是同样的问题,还是要采用之前的解决方法
吧上下文字段改成线程静态.
然后写一个get属性.给上下文做懒加载.
整个程序的设计是,ef的仓储,然后有个算是DDD的service 来处理,这个service 注入仓储的实例,不管是mvc的控制器还是其它地方,只要处理这个数据都通过这个service,问题在于,子线程调用这个service的时候,context被释放。
@果冻布丁喜之郎: 那你找到他是在什么地方什么时候被释放的吗?
@吴瑞祥: 第三张图
@果冻布丁喜之郎: 我说的是被释放.不是已经被释放的时候抛异常.
@吴瑞祥: 这个怎么看?
@果冻布丁喜之郎: 看了上面的回复.
你这个东西是用了依赖注入.那生命周期管理你是怎么配置的?
@吴瑞祥: 这是TCP和AyomarContentService
仓储部分:
@果冻布丁喜之郎: 那上下文的生命周期你是怎么管理的.
PS:上面这几个截图没意义.
@吴瑞祥: 上下文
@果冻布丁喜之郎: 那你的多线程是在哪里体现的?
@吴瑞祥: 监听客户端消息
@果冻布丁喜之郎: 那就在监听里每次触发时都从容器中取一次服务对象.
@吴瑞祥: 试过了,因为里面牵涉的服务太多,都要重新注册一次。暂时还是接收到数据后,通过创建http post一个api来解决。