目录
引言
近期需要使用GAMS(General Algebraic Modeling System)这一软件,在网上查找资料时发现除了官方的文档外讨论较少,故将使用过程中的疑问与发现记录下来。
使用的GAMS版本为33,IDE为IDEA Community 2022.3,需求为使用java语言从文件中加载一个模型,并调整模型的参数,获得输出的结果,并且希望使用多线程来提高效率。官网上的API文档很详细,参见GAMS官网Java API文档,但是在实现上述需求的过程中仍然遇到了一些困难,下面记录一些印象深刻的问题。
问题
1. 工作目录和Include文件的位置
此问题相关代码如下。
GAMSWorkspaceInfo wsInfo = new GAMSWorkspaceInfo();
wsInfo.setWorkingDirectory(workingdir);
报错找不到Include文件的位置,解决方法是需要将Include文件复制到工作目录中。
2. GAMSModelInstance实例化
此问题相关代码如下。发现Transport7.java和想要实现的需求非常像,于是仿照Transport7.java进行开发。关键是用到了GAMSModelInstance这个类,这个类的作用在于可以在一次模型初始化的基础上多次改变某个参数并求解,即“一次初始化多次求解”,有助于提高运行效率。
GAMSModelInstance mi = cp.addModelInstance();
GAMSParameter gama = mi.SyncDB().addParameter("gama", "Capital elasticity in production function");
GAMSParameter dk = mi.SyncDB().addParameter("dk", "Depreciation rate on capital (per year)");
// instantiate the ModelInstance and pass a model definition and Modifier to declare gama&dk changeable
// change the parameters according the sampling, here is a demo.
mi.instantiate("CO2 us nlp max UTILITY", new GAMSModifier(gama), new GAMSModifier(dk));
gama.addRecord().setValue( 0.3 );
dk.addRecord().setValue( 0.1 );
报错“Could not set value for locked database”(大致是这样的表达),直接的报错在addRecord().setValue()的语句,然而实际的原因在instantiate()这一句,与Transport7.java逐句对比发现下面这一句
mi.instantiate("transport use lp min z", opt, new GAMSModifier(bmult));
,其中第一个字符串参数不是没有实际意义的,官网文档中这个方法的介绍如下图所示。
第一个参数含义是Model definition,我一直以为这是一个介绍模型的、可以随便填写的、提升代码可读性的一个参数,但实际上这个参数和模型(.gms文件)的代码密切相关,必须根据模型代码进行相应的设置,设置的规律参看Transport7.java可以得出。
3. 检测到致命错误(A fatal error has been detected by the Java Runtime Environment)
此问题的出现十分玄学,在反复调试中,我碰到过以下几种情况:一运行立即报错、运行一段时间后报错,并且报错的内容也存在两种情况。
一种Problematic frame是C 0x0000000000000000,如下图。
#
# A fatal error has been detected by the Java Runtime Environment:
#
# EXCEPTION_ACCESS_VIOLATION (0xc0000005) at pc=0x0000000000000000, pid=388, tid=0x0000000000008234
#
# JRE version: Java(TM) SE Runtime Environment (8.0_341-b10) (build 1.8.0_341-b10)
# Java VM: Java HotSpot(TM) 64-Bit Server VM (25.341-b10 mixed mode windows-amd64 compressed oops)
# Problematic frame:
# C 0x0000000000000000
#
# Failed to write core dump. Minidumps are not enabled by default on client versions of Windows
另一种Problematic frame是C [joatdclib64.dll+0x8c7a3],如下图。
# A fatal error has been detected by the Java Runtime Environment:
#
# EXCEPTION_ACCESS_VIOLATION (0xc0000005) at pc=0x00007ffb1d0fc7a3, pid=24148, tid=0x0000000000006cd8
#
# JRE version: Java(TM) SE Runtime Environment (8.0_341-b10) (build 1.8.0_341-b10)
# Java VM: Java HotSpot(TM) 64-Bit Server VM (25.341-b10 mixed mode windows-amd64 compressed oops)
# Problematic frame:
# C [joatdclib64.dll+0x8c7a3]
#
# Failed to write core dump. Minidumps are not enabled by default on client versions of Windows
首先注意到了最后一句,尝试允许写出Minidump,改变IDEA的配置文件,但问题并未解决。后续在查找资料过程中意识到dump文件是内核崩溃时的转储文件,用于分析崩溃的原因,也就是说问题的根源并不在此。
接着注意到针对第二种情况网上有说是对应的dll文件的版本与jdk的版本不合适的缘故,有类似问题的解决方案是更换dll文件或者降低jdk版本。我发现joatdclib64.dll这个文件是在GAMS的安装目录里,想要更换不太可能。于是尝试了降低jdk版本,从8_341降到8_241,再到8_101,还是会出现相同的问题。
之后针对第一种情况注意到网上大多说是由于“用Executors去创建线程导致内存泄漏”,这个项目中使用了guava中的ListenableFuture对象,在ListeningExecutorService的创建中确实使用了Executors去创建线程,于是尝试调整为使用ThreadPoolExecutor进行创建。本以为就是这里的问题,但还是没有解决。。。
随后继续查资料,也看到关于调大项目运行内存的说法,但感觉还是治标不治本。之后看到了堆内存、栈内存的一些内容,看到了一处“如果不及时释放不用的内存,内存会逐渐占满并溢出”,突然觉得这个描述很符合“运行一段时间后报错”的现象,于是按着这个思路重新看官方的示例与我的代码有什么不同,在官方文档的搜索栏搜索“multi thread”,在Transport8.java中发现有个“mi.dispose()”语句,方法说明如下图。
大致是该对象不再使用,释放资源的意思。加上这一句后不再报错。后来注意到Transport7.java里没有这一句,之前一直以Transport7.java为参考,所以没有注意到这里。
随后一直不太确定多线程是否运行了,便尝试进行检查,最初的思路是为每个线程指定了一个工作目录,但运行后观察输出发现两个工作目录中都有初始化(构造函数)生成的文件,但之后的文件都出现在其中一个工作目录中。通过IDEA中的断点调试发现确实有两个线程,但初始化过后两个线程的GAMSConn对象(与GAMS进行连接的类)会变成一样的(初始化的时候是不一样的),开始以为这意味着直接用同一个工作目录就可以,但修改后再次出现了“A fatal error has been detected by the Java Runtime Environment” (Problematic frame是C 0x0000000000000000),并且是有时出现有时不出现。
再次碰到这个问题是有点崩溃的,冷静下来后首先换回不会报错的版本,即使用两个工作目录,随后意识到或许是后创建的GAMSConn对象把先创建的覆盖了,然后就注意到了静态(static关键词)这个问题,查了一下果然静态成员是属于类的,所有对象都共用,去掉static关键词后两个线程各自在自己的工作目录计算,拥有不同的GAMSConn对象,逻辑总算是理顺了。回想了一下由于很久没有使用java,在static关键词的使用上比较模糊,编程时遇到报错就根据IDEA的提示修改了,导致将一些变量、方法都变成了static的,从而引发了错误。
总结一下这个问题,“Problematic frame是C [joatdclib64.dll+0x8c7a3]”应该是和没有及时dipose GAMSModelInstance对象有关,“Problematic frame是C 0x0000000000000000”应该和工作目录冲突或者两个线程访问同一个对象有关。
感想
- 编程语言非常严谨,有时多一个词就不对了。
- 断点调试非常有用,尤其在程序比较复杂的情况下。
- 自己的感觉、思考不一定不对,对网上的信息应有所判断。