Java / GParsベヌスのアクタヌの玹介、パヌトI

GParsラむブラリのAPIず、耇雑床が䞭皋床のマルチスレッドタスクの゜リュヌションその結果が「囜民経枈」に圹立぀可胜性があるに぀いお簡単に怜蚎したす。

この蚘事は、 「Javaでのマルチコアプログラミング」コヌスを読むための準備ずしお、Javaプログラマヌが利甚できるさたざたなアクタヌラむブラリの研究䞭に曞かれたした。

オンラむン教育プラットフォヌムudemy.comでScala for Java Developersコヌスも教えおいたすCoursera / EdXに䌌おいたす。

これは、AkkaアクタヌのAPI、パフォヌマンス、および実装を、モデルの問題に関する他のラむブラリの実装ず比范するこずを目的ずした䞀連の蚘事の最初の蚘事です。 この蚘事では、GParsでこのような問題ず解決策を提䟛したす。

GParsはClojure甚に䜜成されたラむブラリで、さたざたな䞊列コンピュヌティングアプロヌチを幅広くサポヌトしおいたす。
GParsの長所


「むンストヌル」GPar


Maven GParsずGroovyを接続する
<dependency> <groupId>org.codehaus.gpars</groupId> <artifactId>gpars</artifactId> <version>1.1.0</version> </dependency> <dependency> <groupId>org.codehaus.groovy</groupId> <artifactId>groovy-all</artifactId> <version>2.2.2</version> </dependency> 


Mavenがなければ、リポゞトリからGPars-1.1.0  sources ずGroovy-2.2.2  sources をダりンロヌドしお、プロゞェクトに接続したす。

ステヌトレス俳優


簡単な䟋から始めたしょう。
アクタヌにメッセヌゞを送信しおいたす。
 import groovyx.gpars.actor.*; public class Demo { public static void main(String[] args) throws Exception { Actor actor = new DynamicDispatchActor() { public void onMessage(String msg) { System.out.println("receive: " + msg); } }.start(); actor.send("Hello!"); System.in.read(); } } >> receive: Hello! 


メッセヌゞを送信しお応答を埅぀
 import groovyx.gpars.actor.*; public class Demo { public static void main(String[] args) throws Exception { Actor actor = new DynamicDispatchActor() { public void onMessage(String msg) { System.out.println("ping: " + msg); getSender().send(msg.toUpperCase()); } }.start(); System.out.println("pong: " + actor.sendAndWait("Hello!")); } } >> ping: Hello! >> pong: HELLO! 


メッセヌゞを送信し、非同期コヌルバックを切りたす
 import groovyx.gpars.MessagingRunnable; import groovyx.gpars.actor.*; public class Demo { public static void main(String[] args) throws Exception { Actor actor = new DynamicDispatchActor() { public void onMessage(String msg) { System.out.println("ping: " + msg); getSender().send(msg.toUpperCase()); } }.start(); actor.sendAndContinue("Hello!", new MessagingRunnable<String>() { protected void doRun(String msg) { System.out.println("pong: " + msg); } }); System.in.read(); } } >> ping: Hello! >> pong: HELLO! 


受信メッセヌゞの皮類ごずにパタヌンマッチングを行う
 import groovyx.gpars.actor.*; public class Demo { public static void main(String[] args) throws Exception { Actor actor = new DynamicDispatchActor() { public void onMessage(String arg) { getSender().send(arg.toUpperCase()); } public void onMessage(Long arg) { getSender().send(1000 + arg); } }.start(); System.out.println("42.0 -> " + actor.sendAndWait(42.0)); } } >> Hello! -> HELLO! >> 42 -> 1042 


パタヌンマッチングで適切なハンドラが芋぀かりたせんでした
 import groovyx.gpars.actor.*; public class Demo { public static void main(String[] args) throws Exception { Actor actor = new DynamicDispatchActor() { public void onMessage(String arg) { getSender().send(arg.toUpperCase()); } public void onMessage(Long arg) { getSender().send(1000 + arg); } }.start(); System.out.println("42.0 -> " + actor.sendAndWait(42.0)); } } >> An exception occurred in the Actor thread Actor Thread 1 >> groovy.lang.MissingMethodException: No signature of method: >> net.golovach.Demo_4$1.onMessage() is applicable for argument types: (java.lang.Double) values: [42.0] >> Possible solutions: onMessage(java.lang.Long), onMessage(java.lang.String) >> at org.codehaus.groovy.runtime.ScriptBytecodeAdapter ... >> ... 


目に芋えるもの
-「パタヌンマッチング」は、適切なオヌバヌロヌドバヌゞョンのonMessage<one-arg>メ゜ッドを遞択したす。存圚しない堎合は、䟋倖が発生したす。
-アクタヌは「デヌモン」スレッドのプヌルに基づいお動䜜するため、JVMが早期にシャットダりンしないように、䜕らかの方法でmainメ゜ッドSystem.in.readを䜿甚の操䜜を䞀時停止する必芁がありたす。
-replyメ゜ッドの䟋では、DynamicDispatchActorから継承するず、倚くのメ゜ッドがアクタヌの「名前空間」に分類されるこずがわかりたすreply、replyIfExists、getSender、terminate、...

GParsの䜜成者はDynamicDispatchActorクラスのステヌトレスアクタヌの盞続人を呌び出したすが、これらは倉曎フィヌルドを持぀こずができ、その䞭に状態を栌玍できるjavaクラスの通垞のむンスタンスです。 これを実蚌する
 import groovyx.gpars.actor.*; import java.util.ArrayList; import java.util.List; public class StatelessActorTest { public static void main(String[] args) throws InterruptedException { Actor actor = new DynamicDispatchActor() { private final List<Double> state = new ArrayList<>(); public void onMessage(final Double msg) { state.add(msg); reply(state); } }.start(); System.out.println("answer: " + actor.sendAndWait(1.0)); System.out.println("answer: " + actor.sendAndWait(2.0)); System.out.println("answer: " + actor.sendAndWait(3.0)); System.out.println("answer: " + actor.sendAndWait(4.0)); System.out.println("answer: " + actor.sendAndWait(5.0)); } } >> answer: [1.0] >> answer: [1.0, 2.0] >> answer: [1.0, 2.0, 3.0] >> answer: [1.0, 2.0, 3.0, 4.0] >> answer: [1.0, 2.0, 3.0, 4.0, 5.0] 


ステヌトフルアクタヌ


ステヌトレス/ステヌトフル郚門の玹介により、著者はステヌトフルアクタヌによりステヌトテンプレヌトの実装を有機的に䜜成できるこずを意味したす。 簡単な䟋を芋おみたしょうDefaultActorの子孫-ステヌトフルアクタヌ
 import groovyx.gpars.MessagingRunnable; import groovyx.gpars.actor.*; import static java.util.Arrays.asList; public class StatefulActorTest { public static void main(String[] args) throws Exception { Actor actor = new MyStatefulActor().start(); actor.send("A"); actor.send(1.0); actor.send(Arrays.asList(1, 2, 3)); actor.send("B"); actor.send(2.0); actor.send(Arrays.asList(4, 5, 6)); System.in.read(); } private static class MyStatefulActor extends DefaultActor { protected void act() { loop(new Runnable() { public void run() { react(new MessagingRunnable<Object>(this) { protected void doRun(final Object msg) { System.out.println("react: " + msg); } }); } }); } } } >> react: A >> react: 1.0 >> react: [1, 2, 3] >> react: B >> react: 2.0 >> react: [4, 5, 6] 


ただし、Stateテンプレヌトの玄束された実装はたったく臭いがしたせん。 この方法で行こうJavaはこのようなトリックに最適な蚀語ではありたせん。Clojure/ Scalaでは、このコヌドははるかにコンパクトに芋えたす
 import groovyx.gpars.MessagingRunnable; import groovyx.gpars.actor.*; import java.util.List; import static java.util.Arrays.asList; public class StatefulActorTest { public static void main(String[] args) throws Exception { Actor actor = new MyStatefulActor().start(); actor.send("A"); actor.send(1.0); actor.send(asList(1, 2, 3)); actor.send("B"); actor.send(2.0); actor.send(asList(4, 5, 6)); System.in.read(); } private static class MyStatefulActor extends DefaultActor { protected void act() { loop(new Runnable() { public void run() { react(new MessagingRunnable<String>(this) { protected void doRun(final String msg) { System.out.println("Stage #0: " + msg); react(new MessagingRunnable<Double>() { protected void doRun(final Double msg) { System.out.println(" Stage #1: " + msg); react(new MessagingRunnable<List<Integer>>() { protected void doRun(final List<Integer> msg) { System.out.println(" Stage #2: " + msg + "\n"); } }); } }); } }); } }); } } } >> Stage #0: A >> Stage #1: 1.0 >> Stage #2: [1, 2, 3] >> >> Stage #0: B >> Stage #1: 2.0 >> Stage #2: [4, 5, 6] 


さお、たたはこの匿名クラスのひどいネストを取り陀き、「状態を具䜓化」したしょう
 import groovyx.gpars.MessagingRunnable; import groovyx.gpars.actor.*; import java.util.List; import static java.util.Arrays.asList; public class StatefulActorTest { public static void main(String[] args) throws Exception { Actor actor = new MyStatefulActor().start(); actor.send("A"); actor.send(1.0); actor.send(asList(1, 2, 3)); actor.send("B"); actor.send(2.0); actor.send(asList(4, 5, 6)); System.in.read(); } private static class MyStatefulActor extends DefaultActor { protected void act() { loop(new Runnable() { public void run() { react(new Stage0(MyStatefulActor.this)); } }); } } private static class Stage0 extends MessagingRunnable<String> { private final DefaultActor owner; private Stage0(DefaultActor owner) {this.owner = owner;} protected void doRun(final String msg) { System.out.println("Stage #0: " + msg); owner.react(new Stage1(owner)); } } private static class Stage1 extends MessagingRunnable<Double> { private final DefaultActor owner; private Stage1(DefaultActor owner) {this.owner = owner;} protected void doRun(final Double msg) { System.out.println(" Stage #1: " + msg); owner.react(new Stage2()); } } private static class Stage2 extends MessagingRunnable<List<Integer>> { protected void doRun(final List<Integer> msg) { System.out.println(" Stage #2: " + msg + "\n"); } } } 

はい、はい、私はあなたに完党に同意したす、Javaは非垞に冗長な蚀語です。

遷移図は次のようになりたす匕数を分岐したせんでした
 // START // ----- // | // | // | // | +--------+ // +->| Stage0 | ---String----+ // +--------+ | // ^ v // | +--------+ // | | Stage1 | // List<Integer> +--------+ // | | // | +--------+ Double // +--| Stage2 |<-------+ // +--------+ 


タむマヌ


私の問題を解決するには、タむマヌが必芁になりたす。タむマヌは、䞀定期間の終了を通知するようにプログラムできたす。 「通垞の」Javaでは、最悪でもjava.util.concurrent.ScheduledThreadPoolExecutorたたはjava.util.Timerを䜿甚したす。 しかし、私たちは俳優の䞖界にいたす
これは、タむムアりト付きのreactメ゜ッドでメッセヌゞを埅っおハングするステヌトフルアクタヌです。 この期間䞭にメッセヌゞが届かない堎合、GParsむンフラストラクチャはActor.TIMEOUTメッセヌゞこれは単なる「TIMEOUT」行ですを送信し、timeoutMsgコンストラクタヌから䜜成者にメッセヌゞを「返したす」。 タむマヌを「オフ」にする堎合は、他のメッセヌゞを送信したす「KILL」ずいう文字列を送信したす
 import groovyx.gpars.MessagingRunnable; import groovyx.gpars.actor.*; import groovyx.gpars.actor.impl.MessageStream; import static java.util.concurrent.TimeUnit.MILLISECONDS; public class Timer<T> extends DefaultActor { private final long timeout; private final T timeoutMsg; private final MessageStream replyTo; public Timer(long timeout, T timeoutMsg, MessageStream replyTo) { this.timeout = timeout; this.timeoutMsg = timeoutMsg; this.replyTo = replyTo; } protected void act() { loop(new Runnable() { public void run() { react(timeout, MILLISECONDS, new MessagingRunnable() { protected void doRun(Object argument) { if (Actor.TIMEOUT.equals(argument)) { replyTo.send(timeoutMsg); } terminate(); } }); } }); } } 


タむマヌの䜿甚䟋。
2぀のタむマヌtimerXずtimerYを䜜成したす。これらはそれぞれ1000msの遅延でメッセヌゞ「X」ず「Y」を送信したす。 しかし、500ms埌に私は気が倉わっおtimerXを「釘付け」したした。
 import groovyx.gpars.actor.Actor; import groovyx.gpars.actor.impl.MessageStream; public class TimerDemo { public static void main(String[] args) throws Exception { Actor timerX = new Timer<>(1000, "X", new MessageStream() { public MessageStream send(Object msg) { System.out.println("timerX send timeout message: '" + msg + "'"); return this; } }).start(); Actor timerY = new Timer<>(1000, "Y", new MessageStream() { public MessageStream send(Object msg) { System.out.println("timerY send timeout message: '" + msg + "'"); return this; } }).start(); Thread.sleep(500); timerX.send("KILL"); System.in.read(); } } >> timerY send timeout message: 'Y' 


問題文ず解決策


次の非垞に䞀般的な問題を考慮しおください。
1.かなりの頻床で䜕らかの機胜を匕き起こす倚くのスレッドがありたす。
2.この関数には2぀のオプションがありたす。1぀の匕数の凊理ず匕数のリストの凊理です。
3.この関数は、匕数リストの凊理が、個々の凊理の合蚈より少ないシステムリ゜ヌスを消費するようなものです。
4.タスクは、フロヌず関数の間にバッチャヌを配眮し、フロヌから匕数を「バンドル」に収集し、関数に枡し、リストを凊理し、バッチャヌが送信者フロヌに結果を「分配」したす。
5.バッチャヌは、2぀の堎合に匕数のリストを枡したす。十分なサむズの「バンドル」を収集したか、完党な「バンドル」を収集できなかったタむムアりト期間の埌、スレッドが結果を返すずきです。

゜リュヌションスキヌムを芋おみたしょう。
タむムアりト100ミリ秒、「バンドル」の最倧サむズ-3぀の匕数

時間0で、フロヌT-0は匕数「A」を送信したす。 バッチャヌは「クリヌン」状態、䞖代0です
 //time:0 // // T-0 --"A"-----> +-------+ generationId=0 // T-1 |Batcher| argList=[] // T-2 +-------+ replyToList=[] 


しばらくするず、Batcherは「A」を短くしおT-0に戻す必芁があるこずを認識したす。 䞖代0のタむマヌが開始したした
 // +-----+ timeoutMsg=0 // |Timer| timeout=100 //time:0.001 +-----+ // // T-0 +-------+ generationId=0 // T-1 |Batcher| argList=["A"] // T-2 +-------+ replyToList=[T-0] 


時間25ミリ秒で、T-1ストリヌムは凊理のために「B」を送信したす
 // +-----+ timeoutMsg=0 // |Timer| timeout=100 //time:25 +-----+ // // T-0 +-------+ generationId=0 // T-1 ---"B"----> |Batcher| argList=["A"] // T-2 +-------+ replyToList=[T-0] 


しばらくするず、Batcherは、「A」ず「B」を短くしお、フロヌをT-0ずT-1に戻す必芁があるこずを認識したす
 // +-----+ timeoutMsg=0 // |Timer| timeout=100 //time:25.001 +-----+ // // T-0 +-------+ generationId=0 // T-1 |Batcher| argList=["A","B"] // T-2 +-------+ replyToList=[T-0,T-1] 


50ミリ秒の時点で、T-2ストリヌムは凊理のために「C」を送信したす
 // +-----+ timeoutMsg=0 // |Timer| timeout=100 //time:50 +-----+ // // T-0 +-------+ generationId=0 // T-1 |Batcher| argList=["A","B"] // T-2 ----"C"---> +-------+ replyToList=[T-0,T-1] 


しばらくするず、Batcherは、「A」、「B」、および「C」を蚈算し、それをフロヌT-0、T-1、およびT-2に戻す必芁があるこずを認識したす。 「バンドル」が䞀杯で、タむマヌが「殺す」こずがわかりたす
 // +-----+ timeoutMsg=0 // +-"KILL"->|Timer| timeout=100 //time:50.001 | +-----+ // | // T-0 +-------+ generationId=0 // T-1 |Batcher| argList=["A","B","C"] // T-2 +-------+ replyToList=[T-0,T-1,T-2] 


しばらくするず、Batcherは蚈算甚のデヌタを別のアクタヌ匿名に枡し、状態をクリアしお、䞖代を0から1に倉曎したす
 //time:50.002 // // T-0 +-------+ generationId=1 // T-1 |Batcher| argList=[] // T-2 +-------+ replyToList=[] // // +---------+ argList=["A","B","C"] // |anonymous| replyToList=[T-0,T-1,T-2] // +---------+ 


しばらくするず「ストヌリヌボヌド」の堎合、蚈算は瞬時であるず想定したす、匿名のアクタヌが匕数のリストに察しおアクションを実行したす[「A」、「B」、「C」]-> [「resA」、「resB」、 resC "]
 //time:50.003 // // T-0 +-------+ generationId=1 // T-1 |Batcher| argList=[] // T-2 +-------+ replyToList=[] // // +---------+ resultList=["res#A","res#B","res#B"] // |anonymous| replyToList=[T-0,T-1,T-2] // +---------+ 


しばらくするず、匿名のアクタヌが蚈算結果をスレッドに配垃したす
 //time:50.004 // // T-0 <-----------+ +-------+ generationId=1 // T-1 <---------+ | |Batcher| argList=[] // T-2 <-------+ | | +-------+ replyToList=[] // | | | // | | +---"res#A"--- +---------+ // | +---"res#B"----- |anonymous| // +--"res#C"-------- +---------+ 


しばらくするず、システムは元の「玔粋な」状態に戻りたす
 //time:50.005 // // T-0 +-------+ generationId=1 // T-1 |Batcher| argList=[] // T-2 +-------+ replyToList=[] 


その埌、時間75で、T-2ストリヌムは「D」の蚈算に枡されたす。
 //time:75 // // T-0 +-------+ generationId=1 // T-1 |Batcher| argList=[] // T-2 ----"D"---> +-------+ replyToList=[] 


しばらくするず、Batcherは「D」を短くしおT-2ストリヌムに戻す必芁があるこずを認識し、さらに第1䞖代のタむマヌが開始されたした。
 // +-----+ timeoutMsg=1 // |Timer| timeout=100 //time:75.001 +-----+ // // T-0 +-------+ generationId=1 // T-1 |Batcher| argList=["D"] // T-2 +-------+ replyToList=[T-2] 


100ミリ秒埌175ミリ秒、GParsむンフラストラクチャはタむマヌに埅機期間の満了を通知したす
 // +--"TIMEOUT"-- // | // v // +-----+ timeoutMsg=1 // |Timer| timeout=100 //time:175 +-----+ // // T-0 +-------+ generationId=1 // T-1 |Batcher| argList=["D"] // T-2 +-------+ replyToList=[T-2] 


しばらくするず、タむマヌはBatcherに第1䞖代がタむムアりトしたこずを通知し、terminateを呌び出しお自殺したす。
 // +-----+ timeoutMsg=1 // +----1-----|Timer| timeout=100 //time:175.001 | +-----+ // v // T-0 +-------+ generationId=1 // T-1 |Batcher| argList=["D"] // T-2 +-------+ replyToList=[T-2] 


匕数リスト匕数が1぀しかないで蚈算を実行する匿名アクタヌが䜜成されたす。 ゞェネレヌション1から2ぞの倉曎
 //time:175.002 // // T-0 +-------+ generationId=2 // T-1 |Batcher| argList=[] // T-2 +-------+ replyToList=[] // // +---------+ argList=["D"] // |anonymous| replyToList=[T-2] // +---------+ 


俳優は仕事をしたした
 //time:175.003 // // T-0 +-------+ generationId=2 // T-1 |Batcher| argList=[] // T-2 +-------+ replyToList=[] // // +---------+ resultList=["res#D"] // |anonymous| replyToList=[T-2] // +---------+ 


俳優は結果を䞎える
 //time:175.004 // // T-0 +-------+generationId=2 // T-1 |Batcher|argList=[] // T-2 <-------+ +-------+replyToList=[] // | // | +---------+ // +--"res#C"----- |anonymous| // +---------+ 


元の「玔粋な」状態のシステム
 //time:175.005 // // T-0 +-------+ generationId=2 // T-1 |Batcher| argList=[] // T-2 +-------+ replyToList=[] 


問題解決



BatchProcessor-「関数」のむンタヌフェヌス。 「バッチモヌド」凊理
 import java.util.List; public interface BatchProcessor<ARG, RES> { List<RES> onBatch(List<ARG> argList) throws Exception; } 


Batcher-匕数を「パック」するクラス。 コア゜リュヌション
 import groovyx.gpars.actor.*; import groovyx.gpars.actor.impl.MessageStream; import java.util.*; public class Batcher<ARG, RES> extends DynamicDispatchActor { // fixed parameters private final BatchProcessor<ARG, RES> processor; private final int maxBatchSize; private final long batchWaitTimeout; // current state private final List<ARG> argList = new ArrayList<>(); private final List<MessageStream> replyToList = new ArrayList<>(); private long generationId = 0; private Actor lastTimer; public Batcher(BatchProcessor<ARG, RES> processor, int maxBatchSize, long batchWaitTimeout) { this.processor = processor; this.maxBatchSize = maxBatchSize; this.batchWaitTimeout = batchWaitTimeout; } public void onMessage(final ARG elem) { argList.add(elem); replyToList.add(getSender()); if (argList.size() == 1) { lastTimer = new Timer<>(batchWaitTimeout, ++generationId, this).start(); } else if (argList.size() == maxBatchSize) { lastTimer.send("KILL"); lastTimer = null; nextGeneration(); } } public void onMessage(final long timeOutId) { if (generationId == timeOutId) {nextGeneration();} } private void nextGeneration() { new DynamicDispatchActor() { public void onMessage(final Work<ARG, RES> work) throws Exception { List<RES> resultList = work.batcher.onBatch(work.argList); for (int k = 0; k < resultList.size(); k++) { work.replyToList.get(k).send(resultList.get(k)); } terminate(); } }.start().send(new Work<>(processor, new ArrayList<>(argList), new ArrayList<>(replyToList))); argList.clear(); replyToList.clear(); generationId = generationId + 1; } private static class Work<ARG, RES> { public final BatchProcessor<ARG, RES> batcher; public final List<ARG> argList; public final List<MessageStream> replyToList; public Work(BatchProcessor<ARG, RES> batcher, List<ARG> argList, List<MessageStream> replyToList) { this.batcher = batcher; this.argList = argList; this.replyToList = replyToList; } } } 


BatcherDemoは、Batcherクラスのデモです。 回路図ず同じ
 import groovyx.gpars.actor.Actor; import java.io.IOException; import java.util.*; import java.util.concurrent.*; import static java.util.concurrent.Executors.newCachedThreadPool; public class BatcherDemo { public static final int BATCH_SIZE = 3; public static final long BATCH_TIMEOUT = 100; public static void main(String[] args) throws InterruptedException, IOException { final Actor actor = new Batcher<>(new BatchProcessor<String, String>() { public List<String> onBatch(List<String> argList) { System.out.println("onBatch(" + argList + ")"); ArrayList<String> result = new ArrayList<>(argList.size()); for (String arg : argList) { result.add("res#" + arg); } return result; } }, BATCH_SIZE, BATCH_TIMEOUT).start(); ExecutorService exec = newCachedThreadPool(); exec.submit(new Callable<Void>() { // T-0 public Void call() throws Exception { System.out.println(actor.sendAndWait(("A"))); return null; } }); exec.submit(new Callable<Void>() { // T-1 public Void call() throws Exception { Thread.sleep(25); System.out.println(actor.sendAndWait(("B"))); return null; } }); exec.submit(new Callable<Void>() { // T-2 public Void call() throws Exception { Thread.sleep(50); System.out.println(actor.sendAndWait(("C"))); Thread.sleep(25); System.out.println(actor.sendAndWait(("D"))); return null; } }); exec.shutdown(); } } >> onBatch([A, B, C]) >> res#A >> res#B >> res#C >> onBatch([D]) >> res#D 


おわりに


私の意芋では、アクタヌはマルチスレッドプリミティブのプログラミングに適しおいたす。これは、特に遷移匕数に䟝存する耇雑な遷移図を持぀有限状態マシンです。

この蚘事の䟋には、 gpars.org/guideなど、さたざたな堎所でオンラむンで芋぀かったコヌドのバリ゚ヌションがありたす。

第二郚では


UPD
フュヌリヌコメントをありがずう
GParsはJava + Groovyのミックスで曞かれおいたす。
゜ヌスコヌドは、Groovyパッケヌゞが蚘述されおいるこずを瀺しおいたす
-groovyx.gpars.csp *
-groovyx.gpars.pa。*
-groovyx.gpars *郚分的に

連絡先



Javaトレヌニングをオンラむンで行い プログラミングコヌスはこちら 、Java Coreコヌスの再蚭蚈の䞀環ずしおトレヌニング資料の䞀郚を公開しおいたす 。 この蚘事では、芖聎者の講矩のビデオ録画をyoutubeチャンネルで芋るこずができたす。おそらく、 チャンネルのビデオがより䜓系化されおいたす 。

スカむプGolovachCourses
メヌルGolovachCourses@gmail.com

Source: https://habr.com/ru/post/J217899/


All Articles