经过多年对线程、可调用函数和执行器池的研究,终于找到了一种设置并发任务的简单方法
问题是:有一个并发执行的任务,但要保证代码不受可调用和其他线程逻辑的影响
多年来,我一直致力于让代码的某些部分以并发方式执行。向量的成对计算,推文上数千条的文本挖掘操作,网络中节点和边缘的简单检查和操作,等等。
将代码中可以并发执行的部分隔离在一个单独的类中:这就是任务。
让这个类实现可运行接口,或者更可能是可调用接口,因为它允许返回一个对象。
在代码的主要部分,设置Executor服务。实例化所有任务并将其提交给执行者。
我想这没什么错。实际上,这对我不起作用。根本原因在于:
将任务设置为可运行的或可调用的,即使这非常简单和轻量级,也会专门处理此用例的代码。如果我想在另一个上下文中重复使用代码,而这个上下文要求逻辑以不同的接口运行,或者因为代码逻辑的复杂性增加而重构任务,那么我为并发性添加的这些接口将成为样板。我会说:“哦,我添加了一种可调用的方式来运行这个特定项目的代码,但在另一个项目中,这只是我的一种方式。我必须删除它。”所以在实践中,当我开发一些并发代码时,我很快就放弃了它,开始了无障碍的工作。
在代码库的主端设置Executor/Future逻辑比看起来更难。这些API上有太多的变体,因此每次都需要花费大量精力来记住和确定应该如何设置。这是因为多线程和并发是低级的,有人可能会说“有一个库为您打包了这个”。但我真的会避免依赖库来实现并发性,它们可能非常大,而且对于我想要做的事情来说范围太广。
它不涉及聪明或复杂的技巧,所以不要期待一个大“哇”。它具有简单性和健壮性,并且自Java11的HttpClient以来变得非常简单。
解决方案是:将任务放在REST API后面,并使用Java 11的HttpClient异步请求调用它
在主代码库中,使用Java11的HttpClient调用每个任务,使用异步特性。这就是并发性的作用所在!!
用例包括对数千条推文进行情绪分析。与其按顺序进行,还可以同时进行。
输入:每行文本(每条推文)以对象映射的形式存储为一个字符串,带有一个唯一标识符的整数<;整数,字符串>;地图。
输出:每一行文本、其唯一标识符和找到的情绪都存储在一个文档对象中,其本身存储在一个对象ConcurrentHashMap中<;整数,文档>;临时结果。该映射必须是ConcurrentHashMap,因为它将由多个线程写入。
字符串selectedLanguage=";嗯";地图<;整数,字符串>;mapOfLines=newhashmap();地图。这是一个测试。并发性是惊人的!";);地图。put(1,";这是一个测试。并发性很难!";);ConcurrentHashMap<;整数,文档>;tempResults=new ConcurrentHashMap();HttpRequest请求;HttpClient=HttpClient。newHttpClient();设置<;完全未来>;futures=newhashset();//这是我为计时而设计的一门方便的课程,在这里可以买到https://github.com/seinecle/Utils时钟时钟=新时钟(";为并发任务计时";);尝试{for(Map.Entry<;Integer,String>;Entry:mapOfLines.entrySet()){Document doc=new Document();doc.setText(Entry.getValue());doc.setId(Entry.getKey());doc.setsential(Category.10);URI URI=new URI(";http://localhost:45/api/sentimentForAText/" +选择的语言+";?id=";+博士。getId()+"&;text=";+URL编码器。encode(entry.getValue(),StandardCharset)。UTF_8。toString());request=HttpRequest。newBuilder()。uri(uri)。构建();完整的未来<;Void>;未来=客户。sendAsync(request,HttpResponse.BodyHandlers.ofString())。然后accept(resp->;{String body=resp.body();//任务返回一个JSON对象,为了便于处理//如下所示,在每个并发任务JsonReader JsonReader=JSON.createReader返回的体(这里是一个字符串)上定义操作//非常简单方便(新StringReader(正文));JsonObject=jsonReader。readObject();Document docReturn=新文档();if(jsonObject!=null&;!jsonObject.isEmpty()){String key=jsonObject.keySet().iterator().next();docReturn.setId(Integer.valueOf(key));docReturn.setText(mapOfLines.get(Integer.valueOf(key));//Category._11是#34;积极情绪";if的标签(jsonObject.getString(键)。equals(Category.Category.toString()){docReturn.setSenition(Categories.Category._11);}//Category_12是"的标签;负面情绪";if(jsonObject.getString(键)。等于(Category.Category.toString()){docReturn.setSetEntity(Categories.Category.toString)}tempResults。put(Integer.valueOf(key),docReturn); } } );期货添加(未来);}可完成的未来<;Void>;combinedFuture=可完成的未来。allOf(futures.toArray((新的CompletableFuture[0]));结合未来。加入();//tempResults准备好在代码中进一步使用了!}catch(URISyntaxException异常){System.out.println(";URI语法异常";+异常);}catch(UnsupportedEncodingException ex){System.out.println(";编码异常:";+ex)}时钟closeAndPrintClock();
这项任务的背后是一个简单的REST API,使用超轻量级的Javalin框架(但任何其他REST框架都可以)。这段代码驻留在一个单独的Java SE项目中,该项目编译在一个jar中,我可以将其部署在任何地方,并与主代码库分开:
公共类APIController{/***@param args命令行参数*/public static void main(String[]args){Javalin app=Javalin.create().start(45);System.out.println(";运行api";);//用于情绪分析的对象初始化,在部署jar时只需初始化一次。//每次调用API都会发现这些对象可以使用,这会加快速度。//这是一个免费的开源工具:https://github.com/seinecle/umigon-coreUmigonController UmigonController=新的UmigonController();ClassifierMachineDocument classifierOneDocEN=新的ClassifierMachineDocument(umigonController.getSemanticsEN());ClassifierMachineDocument classifierOneDocFR=新的ClassifierMachineDocument(umigonController.getSemanticsFR());应用程序。get(";/api/MONTUREFORATEXT/{lang}";,ctx->;{JsonObjectBuilder objectBuilder=Json.createObjectBuilder();String text=ctx.queryParam(";text";);字符串id=ctx。queryParam(";id";);如果(id==null){id=UUID.randomUUID().toString().substring(0,10);}如果(text==null){objectBuilder.add(id,";文本参数不存在";);JsonObject JsonObject=objectBuilder。构建();ctx。结果(jsonObject.toString())。状态(HttpCode.BAD_请求);}else{String lang=ctx.pathParam(";lang";);Document doc=新文档();医生。setText(文本);switch(lang){case";en";:doc=classifiedocen.call(doc);break;case";fr";:doc=classifiedornedocr.call(doc);break;默认值:objectBuilder.add(";-99";";lang lang-lang的参数错误,不支持";);JsonObject JsonObject=objectBuilder。构建();ctx。结果(jsonObject.toString())。状态(HttpCode.BAD_请求);}objectBuilder。添加(id,doc.gettouction()。toString());JsonObject JsonObject=objectBuilder。构建();ctx。结果(jsonObject.toString())。状态(HttpCode.OK);}}}
看看上面的代码,我意识到反应可能是Whaaat,但这段代码实际上比使用ExecutorService等要复杂得多!好吧,让我们来分析一下利弊:
该任务完全没有并发逻辑。不可调用也不可运行。那会解放你的思想!
因此,从主代码库的角度来看,任务是完全可重用的,并且可以在不影响并发逻辑的情况下进行重构。这是一个巨大的好处,因为在处理代码的逻辑时,您不想因为使代码“并发兼容”的问题而受到干扰。
REST API背后的封装是一个好处:任务的代码库在接口之间是相同的。实际上,情绪分析任务由web应用程序界面和API上的相同代码执行,如下所示:https://nocodefunctions.com/umigon/sentiment_analysis_tool.html
任务从主代码库中卸载,这有助于使事情更有条理
从主代码库调用并发任务相对简单。不需要实例化或摆弄执行器,因为它已经捆绑在HttpClient中。但如果需要的话,它可以是泰勒色的。
使用json将任务的响应带回主代码库会让事情变得脆弱。我可能需要更改REST API端的Json格式,忘记更新在主代码库中读取Json对象的方式。
速度:在多核机器上,执行时间比顺序执行快4到6倍。
限制:本地与远程:当REST API与主代码库位于同一台机器上时,可以获得如上所示的并发性。当我尝试混合时(主代码基于远程服务器上的本地膝上型电脑x REST API),代码的速度非常慢。我没有调查原因。
当REST API在Windows计算机上时,连接有一个最大限制。达到此限制会导致异常。奇怪的因此,请确保在linux上托管REST API。
我将逐步重构nocodefunctions上的所有函数。com来采用这种设计。目前(2022年2月19日),只对网站的测试版本进行了情绪分析。因此,现在分析6000条推文只需10秒钟(每秒600条推文!),一分钟前。
在阅读了专业Java开发人员和StackOverflow Q&;像我感谢所有的贡献者,我希望这篇新的博文能对下一个研究这些话题的人有所帮助。