img

همروندی پیشرفته و هوش مصنوعی در جاوا

/
/
/

ساختن یک روبات خودکار برای بازی، به ما کمک ‌می‌کند راجع به ویژگی‌های جدید همروندی (Concurrency) در جاوا ۸ اطلاعات بیشتری کسب کنیم.

در این مقاله به ایجاد یک بازی در جاوا می‌پردازیم، اما تمرکز ما همیشه بر یادگیری ویژگی‌های متعدد جاوا بوده است. در این مقاله با ساختن روبات بازی، با ویژگی‌های جدید همروندی در جاوا ۸ آشنا ‌می‌شویم. چندین همروندی ساده را با استفاده از رشته ‌ها یاد گرفته‌ایم، اما همانطور که ‌می‌بینید، رشته‌ها به تنهایی قادر به حل مشکلات همروندی ما نیستند. ما کلاس CompletableFuture<E> را از کتابخانه همروندی معرفی ‌می‌کنیم و نشان ‌می‌دهیم که چگونه با استفاده از آن ‌می‌توان یک لایه سرویس بین استراتژی روبات و معماری اساسی سرور ساخت.
یک نکته مهم برای کسانی که از آخرین قسمت سری به ما پیوسته اند(قسمت ۵ام): موضوع بخش چهارم شامل بخشی راجع به اصلاح مشکلات همروندی در سرور بود، اما کدهای استفاده شده در بخش پنجم در اصلاحات کنونی دخیل نیستند. اگر برنامه شما بر مبنای کدهای بخش پنجم است، لطفا کلاس Player، StandardGameModel و StreamInputController را با نسخه مهیا شده در دیسک جایگزین کنید.
پروژه این ماه کمی‌ متفاوت خواهد بود: این بار، به جای گسترش بازی، ‌می‌خواهیم یک روبات مخرب بسازیم که دور تا دور نقشه ‌می‌رود و تخم مرغ‌ها را جمع ‌می‌کند. الگوریتم اصلی که روبات استفاده ‌می‌کند به شما بستگی دارد: چگونگی ساختن یک نسخه بسیار ساده را در این مقاله آموزش ‌می‌دهیم اما این به شما بستگی دارد که نسخه ای موثرتر بسازید.
در حالت ایده‌آل چیزی که در انتها انتظار داریم نوشتن کدی مشابه کد زیر است:

move(‘N’);
MapTile[ ][ ] mapGrid = search();
List<Point2D> eggPositions =
lookForEggs(mapGrid);
for (Point2D eggPosition : eggPositions) {
pickUpEgg(eggPosition);
}

در جایی روش‌های move، search و امثال آن‌ها را داریم که ‌می‌توانند با سرور ارتباط برقرار کنند. با اینحال این روش یک مشکل دارد: تنها کاری که کلاینت ‌می‌تواند انجام دهد فرستادن دستور به سرور و –به صورت جداگانه- خواندن ناهمگون پیام‌ها از سرور است.

 

همروندی پیشرفته و هوش مصنوعی در جاوا

هیچ شکی نیست که راه‌هایی برای حل این مشکل با استفاده از رشته‌ها وجود دارد: مثلا، یکی از راه حل‌ها مشابه کدی ست که در شکل ۱ ‌می‌بینید، جایی که یک رشته جدا مسئول خواندن پیام‌ها از سرور و تنظیم متغیر بولین hasServerResponded وقتی پیام ‌می‌رسد، است. اشکالات متعددی به این روش وارد است: نوشتن آن سخت است، اجازه نمی‌دهد کدی ثابت نوشته شود و باید برای دریافت پاسخ از سرور یا مدت زیادی صبرکرد (در این مثال ۵۰۰ میلی ثانیه) یا با استفاده از busy wait، (حذف دستور Thread.sleep()) باقی رشته‌ها را از منبع تغذیه جدا کرد. مهم ترین دلیلی که نباید از این سازه به خصوص استفاده کنید، غیرضروری بودن آن است: جاوا ابسترکت‌های بسیار بهتری را ارائه ‌می‌دهد که بدون اجرای دستی، قابل استفاده هستند.
با اینکه در این مقاله از آن استفاده نمی‌کنیم، بررسی جاوا کلاسیک که حول این موضوع از مانیتورها استفاده ‌می‌کند، ارزش دارد. هر آبجکتی در جاوا، لحظه‌ای از سوپرکلاس Object است، که روش‌های wait() و notify() را فراهم ‌می‌کند. اگر چند آبجکت monitor در کد داشته باشم، ‌می‌توانیم monitor.wait(); را فراخوانی کنیم که در بعضی نقاط رشته درحال اجرا را متوقف ‌می‌کند. اگر در رشته‌ای دیگر object.monitor(); را فراخوانی کنیم، رشته اول ادامه پیدا ‌می‌کند.
مانیتورها، راه‌های قابل‌قبولی برای رویایی با مسئله انتظار برای ورودی هستند اما در این مقاله، به ابسترکت متفاوتی از CompletableFuture نگاه ‌می‌اندازیم. اگر کلاس Result داشته باشیم، کلاس CompletableFuture<Result> ابسترکتی است که آبجکتی از نوع Result ارائه ‌می‌دهد که در future قابل دسترس است. بزرگترین مزیتی که بر wait()/notify() دارد، تماس‌ بلاک‌نشدنی آن است.

 

همروندی پیشرفته و هوش مصنوعی در جاوا

دو روش استفاده از search() را در شکل۲ باهم مقایسه کنید. اگر اولی را از رشته فراخوانی کنم، آن رشته تا زمانی که پاسخ از سرور بیاید متوقف ‌می‌شود. اگر دو‌می‌را فراخوانی کنم، روش بلافاصله بر ‌می‌گردد اما به جای نقشه چهارچوب بندی شده،

CompletableFuture<MapTile[][]>

را بر ‌می‌گرداند.
این روش قابل انعطاف تری است: نیازی نیست فورا رشته خود را بلاک کنیم، و ‌می‌توانیم پردازش‌های آتی را حین اینکه منتظر پاسخ از سرور هستیم، انجام دهیم. زمانی که پاسخ سرور را نیاز داریم، ‌می‌توانیم روش get() را از کلاس CompletableFuture فراخوانی کنیم که اگر مقدار مهیا باشد، آن را بازمی‌گرداند و اگر نباشد برای آن صبر کند:

CompletableFuture<MapTile[][]> mapGridFuture =
search();
// When we need it…
MapTile[][] mapGrid = mapGridFuture.get();

در عین حال در جای دیگری از کد، با ورودی سرور سروکار داریم که برای فراخوانی روش complete() از کلاس CompletableFuture استفاده ‌می‌شود، که در آینده مقادیر کامل شده را نشانه گذاری ‌می‌کند:

public void dealWithServerMapTiles(MapTile[]
[] grid)
{
currentSearchRequest.complete(grid);
}

اگر رشته ای برای روش get() در آبجکت خاص CompletableFuture منتظر بازگردانی ست، فراخوانی روش complete() باعث ‌می‌شود get() با یک مقدار، بازگردد. اگر CompletableFuture را با مقداری از complete() روبه رو کردیم، هر درخواست متعاقب برای get()، مقدار را بلافاصله باز ‌می‌گرداند.
کلاس CompletableFuture بیش از ۵۰ روش عمو‌می‌دارد و درحال حاضر فقط قطره ای از دریا را به شما نشان دادیم. در ادامه از کارهایی که قادر به انجام آن‌هاست، بیشتر ‌می‌بینیم.

 

آغاز کار
همانطور که در مسائل قبلی دیدیم، دسته کاملی از کدها را برای آغاز کار شما فراهم کرده ایم. برای استفاده از آن فایل eggs.tar را از دیسک پیدا کرده و در سیستم خود کپی کنید. Eclipse را راه اندازی کرده و به File>Import بروید. گزینه Existing Projects into Workspace را انتخاب و Browse را زده و سپس فایل eggs.tar را انتخاب کنید. Finish را برای اتمام مراحل وارد کردن، فشار دهید.

 

همروندی پیشرفته و هوش مصنوعی در جاوا

اگر با کدهای خود کار ‌می‌کنید، پکیج دیگری فراهم کرده ایم که امکان دارد برای راه یابی بین نقشه‌ها استفاده از آن را مفید بدانید. برای استفاده از آن بازهم eggs.tar را در سیستم خود دانلود کنید. سپس در Eclipse، یک پکیج جدید در پروژه خود با نام luad.eggs.network.botClient.pathFinder بسازید.
سپس از مسیر File>Import، فایل آرشیو را انتخاب کنید. با متنی که در شکل ۳ ‌می‌بینید، روبه رو ‌می‌شوید. Browse را فشار دهید و eggs.tar را پیدا کنید. درخت دایرکتوری در بالا ظاهر ‌می‌شود، تیک دایرکتوری بالا را بردارید و به

src/main/java/luad/eggs/network/botClient/ pathFinder

بروید.

 

همروندی پیشرفته و هوش مصنوعی در جاوا

تیک باکس‌های PathFinder.java و MoveInstruction.java را بزنید و در پایین تر Browse را فشار دهید تا متنی مشابه شکل۴ برایتان ظاهر شود. بعد از انتخاب پکیجی که ساخته اید، اول OK و سپس Finish را برای وارد کردن کلاس‌ها بزنید.
این باید کلاس‌های جدید را به برنامه شما وارد کند. با اینحال، کلاس‌های جدید برمبنای Maven هستند، که باید قبل از استفاده از آن‌ها، نصبش کنید. از راهی که برای نصب Spring در موضوع آخری استفاده کرده ایم، استفاده کنید: در اکسپلورر پروژه pom.xml را بزنید، به سربرگ Dependencies رفته و با زدن Add متنی که در عکس۵ هست را ‌می‌بینید. dependency دیگری که ‌می‌خواهیم آن را اضافه کنیم یک کتابخانه الگوریتم گراف با نام JGraphT است. زیر Group Id عبارت org.jgrapht، زیر Artifact Id عبارت jgrapht-core و زیر نسخه، ۱٫۱٫۰ تایپ کنید و سپس OK کرده، eggs install را که در مبحث قبل ساختیم، برای پیکربندی اجرا کنید. اگر آن را گم کرده اید، ‌می‌توانید از طریق مسیر Run > Run configurations و ایجاد یک سازه جدید Maven با هدف نصب، آن را دوباره بسازید.
در انتها، همانطور که گفته شد برای ان‌هایی که به پروژه از ماه پیش ملحق شده اند، باید چند نسخه از کلاس‌های بازی‌های پایه را دانلود کنید. براساس دستورالعمال‌های ذکر شده در بالا برای وارد کردن پکیج pathFinder، فایل‌های StandardGameModel.java، Player.java و StreamInputController.java را در پکیج luad.eggs از حالت فشرده خارج و با نسخه موجود در آن جا، جایگزین کنید. احتمال دارد برای اطمینان بیشتر بخواهید قبل از جایگزینی از فایل‌ها پشتیبان تهیه کنید.
یک پکیج جدید به نام luad.eggs.network.botClient بسازید. اولین کلاسی که ‌می‌خواهیم آن را بسازیم، کلاس لایه سرویس است که دو نقش ایفا ‌می‌کند. ابتدا، GUI را از کلاینت اصلی جایگزین ‌می‌کند: به جای نمایش اطلاعات از سرور به صورت گرافیکی، مستقیما آن را به مغز روبات وارد ‌می‌کند. دوم، مسئول بازفرستادن پیام‌ها به سرور با استفاده از الگوی شاهد است. کلاسی در پکیج جدید با نام BotServiceLayer ایجاد کنید و مطمئن شوید به کلاس Observable گسترش و در رابط OutputViewer قرار داده ‌می‌شود. این کلاس حاوی روش‌هایی ست که روبات با استفاده از آن‌ها ‌می‌تواند اطلاعات را از سرور دریافت کند. شما را در راه ایجاد یکی از این‌ها همراهی ‌می‌کنیم و ‌می‌گذاریم باقی را خودتان مشابه با این روش بسازید.
در داخل کلاس BotServiceLayer روشی با عنوان requestMapTiles() ایجاد کنید که هیچ پارامتری نخواهد و آبجکتی از نوع

CompletableFuture<MapTile[][]>

بازگرداند. این روش اینگونه عمل ‌می‌کند: یک آبجکت جدید و خالی از نوع

CompletableFuture<MapTile[][]>

ارائه ‌می‌دهد و بلافاصله آن را باز‌می‌گرداند. در همین حین، کلاس مرجعی را در CompletableFuture که آن را ساخته ایم، حفظ ‌می‌کند؛ وقتی سرور بعضی از آن‌ها را در نقشه پراکنده ‌می‌کند، از آن‌ها برای کامل کردن future استفاده ‌می‌کند. از آن جا که باید مرجعی به future را حفظ کنیم، در کلاس BotServiceLayer نوع

CompletableFuture<MapTile[][]>

زمینه ای با نام currentMapGridRequest بسازید. در داخل روش equestMapTiles()، دسته دستوری به زمینه موردنظر به وسیله ایجاد آبجکت جدید

CompletableFuture<MapTile[][]>

بفرستید و دوباره به زمینه بازگرید:

currentMapGridRequest =
new CompletableFuture<>();
return currentMapGridRequest;

این روش همچنین باید دستور search را به سرور بفرستد. به منظور فرستادن دستورات، از روش‌های setChanged() و notifyObservers() از سوپرکلاس Observer استفاده کنید؛ بعدها کلاسی به عنوان شاهد به آن اضافه ‌می‌کنیم، کلاسی که این مقادیر را ‌می‌خواند و آن‌ها را از به سرور منتقل ‌می‌کند.

setChanged();
notifyObservers(«search»);

حالا زمانی که خبری از سرور به دست ما ‌می‌رسد باید با completing the future سروکار داشته باشیم. اگر تاکنون آن را نداشته‌اید، از روش displayMapTiles() از رابط OutputViewer آن را ایجاد کنید. این روش زمانی فراخوانی ‌می‌شود که سرور انتقالات را در سطح نقشه انجام ‌می‌دهد. درون این روش complete the future از این داده‌های آمده از سرور استفاده ‌می‌کند:

@Override
public synchronized void
displayMapTiles(MapTile[][] grid)
{
currentMapGridRequest.complete(grid);
}

یک پیچیدگی جزئی این است که معمولا سرور اطلاعاتی ‌می‌فرستد که در پاسخ به دستور کلاینت نیست. مثلا، اگر یک بازیکن در همان اطراف حرکتی انجام دهد، سرور داده‌های جدیدی در سطح نقشه ‌می‌فرستد. CompletableFuture راه حل مناسبی پیرامون این موضوع دارد: زمانی که CompletableFuture کامل شود، هر فراخوانی متعاقب بر complete() تاثیری ندارد. این بدان معناست که اولین دسته داده‌های نقشه که بعد از فرستادن دستور، دریافتشان کرده ایم برای کامل کردن complete() آینده استفاده ‌می‌شود و هر داده متعاقب دیگری در نظر گرفته ‌می‌شود.
تنها مشکل ممکن این است که سرور احتمال دارد داده‌ها را قبل از اینکه از آن بخواهیم، بفرستد. در آن صورت، اگر بخش currentMapGridRequest آغاز نشده باشد، به ارور ‌می‌خوریم. برای اطمینان از اینکه چنین اتفاقی نمی‌افتد، این قسمت را وقتی ساختید، آغاز کنید:

private CompletableFuture<MapGrid[][]>
currentMapGridRequest =
new CompletableFuture<>();

حالا کارهای مشابهی برای درخواست‌های inventory و pick up انجام دهید. روش‌های requestInventory() و requestPickUp() را بسازید که در صورت requestInventory() بودن عبارت

CompletableFutures a CompletableFuture<Player>

و درصورت requestPickUp() عبارت

CompletableFuture<Boolean>

را باز‌می‌گرداند، در حالیکه مقدار بولین اگر موفق به برداشت تخم مرغ شویم، TRUE و اگر نشویم، FALSE بر‌می‌گرداند. این روش‌ها را دقیقا مشابه آنچه برای requestMapTiles() اجرا کردیم، انجام دهید، با ایجاد یک CompletableFuture جدید، قسمت‌های currentInventoryRequest و currentPickUpRequest را برای نگهداری مقادیر بازگشتی بسازید. باید دستورات inventory و pick up را در این روش‌ها فراخوانی کنید، همانطور که ما قبلا search را فراخوانی کردیم.
برای کامل کردن Future، از رابط OutputViewer، روش‌های displayInventory() و displayMessage() را استفاده ‌می‌کنیم. روش displayInventory() ‌می‌تواند از پارامترهای خودش برای کامل کردن مستقیم future استفاده کند، مشابه روش displayMapTiles(). برای کامل کردن currentPickUpRequest، باید بر پیام‌های دریافتی به عنوان پارامتری از روش displayMessage() شرطی اعمال کنیم. وقتی کلاینت دستور pick up را ‌می‌فرستد، سرور بسته به اینکه برداشت موفق بوده یا نبوده، با پیا‌می ‌پاسخ ‌می‌دهد. بسته به پیا‌می ‌که به عنوان پارامتر به روش displayMessage ‌می‌رسد، currentPickUpRequest به TRUEو FALSE کامل ‌می‌شود. اگر پیام حاصل از سرور به موفقیت یا شکست در برداشت مربوط نباشد، به آن کاری نداشته باشید.
همچنین باید روش move() را در کلاس BotServiceLayer بسازید که کاراکترهای

N›, ‹S›, ‹E› or ‹W›

را دریافت و دستور move N را به سرور ‌می‌فرستد. اگر دوست دارید ‌می‌توانید کاری کنید که این روش CompletableFutureای وابسته به اینکه حرکت موفق بوده یا نبوده، برگرداند، اما حرکت آسان‌تر تبدیل آن به روش معمول void است. همانطور که در روش‌های دیگر وجود دارد، از setChanged() و notifyObservers() برای فرستادن دستور استفاده کنید.

 

ایجاد استراتژی برای روبات
حالا که لایه سرویس را ایجاد کردیم، برای نوشتن استراتژی روبات آماده ایم. کلاسی با نام BotStrategy حاوی بخش serviceLayer از نوع ServiceLayer بسازید. تنظیم این بخش با استفاده از انتقال مقادیر در سازنده کلاس و به عنوان پارامتر، عملی ‌می‌شود، بنابراین ‌می‌توانیم لحظه ای از کلاس را به صورت زیر ایجاد کنیم:

BotServiceLayer serviceLayer = new
BotServiceLayer();
BotStrategy strategy = new
BotStrategy(serviceLayer);

مهم ترین روش در این کلاس run() است که از روش‌های void محسوب ‌می‌شود و ‌می‌تواند استارتژی را اجرا کند. این همانجایی ست که احتیاج دارید قدری خلاق باشید تا هوش مصنوعی روبات خود را طراحی کنید. در این مقاله، نسخه ساده ای از چگونگی استفاده از CompletableFutures را توضیح ‌می‌دهیم که توسط لایه سرویس به ما بازگشته است.

 

همروندی پیشرفته و هوش مصنوعی در جاوا

همروندی پیشرفته و هوش مصنوعی در جاوا

نسخه ابتدایی روش run() در شکل ۶ نشان داده شده است. دو روش جدید وجود دارد که برای به کار افتادن این روش آن را نوشته ایم: findandpicktheegg() و moveInRandomDirection(). دو‌می‌به راحتی اجرا ‌می‌شود: به صورت تصادفی یکی از پارامترهای ‘N’، ‘S’، ‘E’ و ‘W’ را انتخاب کرده و به روش serviceLayer.move() ‌می‌دهیم.
اگر از پکیج pathfinder استفاده کنید روش اول نیز تاحدودی آسان است، که در کدهای این ماه آورده شده است. ابتدا، اگر آرایه دوبعدی mapGrid را از آبجکت‌های MapTile داشته باشیم، ‌می‌توانیم یک آبکت جدید PathFinder با کد زیر بسازیم:

PathFinder pathFinder = new
PathFinder(mapGrid);

سپس کلاس PathFinder دو روش مفید را فراهم ‌می‌کند. اگر آبجکت‌های Point2D را داشته باشیم، ‌می‌توانیم توالی حرکاتی که برای رفتن از یک نقطه به نقطه دیگر انجام دادیم را با کد زیر داشته باشیم:

Point2D playerPosition = new Point2D(3, 3);
Point2D eggPosition = new Point2D(4, 5);
List<Character> moves =
pathfinder.findPath(PlayPosition,
eggPosition);

این روش لیستی از کاراکترها را (‹N›, ‹N›, ‹E›, ‹S›, ‹S) باز ‌می‌گرداند؛ اگر دستورهای متناظر move را به ترتیب فراخوانی کنیم، باعث ‌می‌شود بازیکن از مکان کنونی اش به مسیری که eggPosition فراهم ‌می‌کند، ‌می‌رود. مسیر به صورت خودکار از مسیرهای غیرقابل دسترس مثل دریا، همانطور که در شکل ۷ ‌می‌بینید، اجتناب ‌می‌کند. اگر راهی برای رفتن از میان دریا نباشد، روش findPath()، null بر‌می‌گرداند.

 

همروندی پیشرفته و هوش مصنوعی در جاوا

توجه کنید که نقاط playerPosition و eggPosition به آرایه mapGrid مربوطند: اگر چهارچوب چیزی باشد که سرور در پاسخ به دستور search باز ‌می‌گرداند، مکان بازیکن همیشه نقاط (۳ و ۳) خواهد بود.
از آن جا که چهارچوب نقشه محتوی تخم مرغ‌های متعددی است، ‌می‌توانیم روش findPath را با عبور درمیان Collection یا List از آبجکت‌های MapTile به عنوان پارامترهای دوم فراخوانی کنیم. مثلا اگر درستور زیر را فراخوانی کنیم:

List<Point2D> eggPositions = …
List<Character> pathToNearestEgg =
pathfinder.findPath(PlayPosition,
eggPositions);

آبجکت PathFinder به صورت خودکار مسیر را به نزدیکترین تخم مرغ باز‌می‌گرداند. اگر هیچ راهی به مکان‌های هدف نباشد، روش findPath() به راحتی null بر ‌می‌گرداند.
روش findandpickupegg() که به مراتب استفاده کردیم، دستور move() را فراخوانی ‌می‌کند تا در مسیری که PathFinder آن را بازگردانده، حرکت انجام شود، و سپس requestPickUp() را فراخوانی ‌می‌کند و آبجکت CompletableFuture<> را که روش بازگرداننده آن است، باز ‌می‌گرداند.
با ایجاد نسخه شخصی استراتژی برای روبات خود، از فرایند لذت ببرید. نسخه ما بسیار آسان است اما مطمئنیم که شما ‌می‌توانید نسخه بهتر آن را بسازید.
فقط یک چیز باقی مانده که باید قبل از اتصال همه چیز به یکدیگر، انجام دهیم: وقتی سرور خود را ساختیم، مکانیسمی ‌‌می‌سازیم که کلاینت ورودی را وقتی به صورت تکراری اتفاق بیفتد، نادیده ‌می‌گیرد. به صورت مخصوص، سرور پیام‌هایی که زیر ۱۰۰میلی ثانیه بعد از پیام قبلی فرستاده ‌می‌شود را نادیده ‌می‌گیرد. نمی‌خواهیم پیام‌های روبات ما نادیده گرفته شود، بنابراین مطمئن شوید تاخیری (با استفاده از Thread. sleep()) به اندازه ۱۰۰ میلی ثانیه هربار که روبات دستوری به سرور ‌می‌فرستد، اضافه کنید.
البته، همیشه این امکان وجود دارد که پیام اولیه دیر برسد و سرور با وجود اعمال تاخیر، باز هم پیام دوم را نادیده بگیرد. در آن صورت، CompletableFuture هیچ گاه کامل نمی‌شود. برای جلوگیری از این قضیه، ‌می‌توانیم از ویژگی مهلت در روش get() استفاده کنیم. مثلا:

MapTile[][] mapGrid = serviceLayer
.requestMapTiles()
.get(1000, TimeUnit.MILLISECONDS);

این نسخه در حد یک لحظه برای کامل شدن future صبر ‌می‌کند. اگر در زمان مقرر شده کامل نشود، get() نوعی TimeoutException به میان ‌می‌آورد، که ‌می‌توان آن را برای گرفتن شانسی دوباره، اگر سرور پیام ما را باز پس نداد، استفاده کنیم.

 

اجرا کردن روبات
آخرین قدم، اتصال همه چیز به یکدیگر است. کلاس BotClient با روش main() ‌می‌سازیم که برنامه را تنظیم و اجرا ‌می‌کند. روش main() مبتنی بر روش start() در کلاس EggsClient است، با این تفاوت که GUI را با لایه سرویس روبات و استراتژی بازی عوض کرده ایم.
چند خط اول روش EggsClient.start() را نادیده ‌می‌گیریم، زیرا کاری که انجام ‌می‌دهند تنظیم GUI و تنظیمات شبکه است. احتمال دارد کد شما مشابه شکل ۸ باشد: بسیار شبیه کد تنظیم شبکه در EggsClient.start() است اما از آبجکت BotServiceLayer به عنوان عامل اجرایی OutputViewer به جای آبجکت GuiOutputViewer که قبلا از آن استفاده ‌می‌کردیم، استفاده ‌می‌کنیم.

 

همروندی پیشرفته و هوش مصنوعی در جاوا

برای تمام کردن روش main()، خط‌های زیر را اضافه کنید:

BotStrategy strategy = new
BotStrategy(serviceLayer);
serviceLayer.addObserver(clientHead);
strategy.run();

تا خروجی و استراتژی روبات را به سرور باز گردانید. اگر با کد خود کار ‌می‌کنید، احتمالا برای عملکرد باید ک‌می‌آن را تغییر دهید.
آزمایش روبات کار ساده ایست: سرور را آغاز کنید، سپس کلاینت تخم مرغ‌های نرمال را آغاز کنید (بنابراین ‌می‌توانید ببینید چه اتفاقی ‌می‌افتد) و در آخر کلاینت روبات را آغاز کنید. روبات را به وسیله بازیکن، دور نقشه دنبال کنید تا ببینید درست کار ‌می‌کند یا خیر.
از آنجا که به انتهای این سری رسیدیم، ‌می‌خواهیم از این پروژه لذت ببرید. امتحان کنید و ببینید چه اعمالی از روبات قابل انجام است. و اگر ‌می‌خواهید بازگشته و GUI یا جزء دیگری از برنامه را ارتقا دهید، همیشه ‌می‌توانید آن را عملی کنید. خلاق باشید و ممنون که شرکت ‌می‌کنید.

خلاصه ای کوتاه از همروندی جاوا
جاوا از اولین زبان‌های برنامه نویسی در پشتیبانی از برنامه نویسی همروندی بود. ماشین مجازی جاوا از رشته‌ها پشتیبانی ‌می‌کند، که واحدهای پایه ای در همروندی جاوا هستند. مکانیسم wait/notify از همان ابتدا برای توقف رشته‌ها زمانی که منتظر بعضی اقدامات توسط رشته‌های دیگر هستند، وجود داشت. اما این روش تاحدودی محدود و مستعد خطاست- به خصوص اگر جایی فراخوانی notify() انجام شده باشد. به بازار آمدن جاوا۵، پکیج java.util.concurrent را معرفی کرد که ویژگی‌های قدرتمند، جدید و همروندی را به جاوا آورد. این پکیج آبجکت‌های سرویس ExecutorServices را معرفی ‌می‌کند که ‌می‌توان برای اینکه در مخزن رشته‌ها به صورت نامتقارن اجرا شوند، اموری را به آن‌ها انتصاب کرد؛ ساختارهای داده ای مثل BlockingQueue و ConcurrentHashMap که برای کار با همروندی ساخته شده اند؛ قفل‌های هماهنگی‌های صریح و رابط Future<E> که نسخه محدودی از عملکرد فعلی کلاس CompletableFuture<E> که در این مقاله با آن سروکار داریم، فراهم ‌می‌کند.
جاوا۷ کلاس ForkJoinPool را معرفی کرد، اجراکننده رابط ExecutorService که به محاسبات فرصت ‌می‌داد در دو رشته مجزا fork شوند و سپس وقتی کامل شدند به جای قبلی join شوند. کلاس CompletableFuture نسخه ای جدید و اصلی در کتابخانه همروندی در جاوا۸ بود. اگر این مقاله را تمام کرده اید، پس روی لبه علم هستید.

 

تماس‌های بلاک نشدنی
تا کنون تماس‌های ما بلاک ‌می‌شوند: وقتی یک آبجکت CompletableFuture به دست ‌می‌آوریم، بلافاصله get() را برای آن فراخوانی ‌می‌کنیم که باعث ‌می‌شود رشته تا زمانی که future تکمیل شود، به حالت تعلیق درآید. قدرت واقعی CompletableFuture از قابلیت آن برای برنامه‌ریزی رویدادها در قالب بلاک نشدنی ‌می‌آید، که ‌می‌تواند در جلوگیری از مشکلات همروندی بسیار مفید باشد. از مهم ترین روش‌های بلاک نشدنی در کلاس

 

همروندی پیشرفته و هوش مصنوعی در جاوا

CompletableFuture در شکل ۹ قابل مشاهده است. مثلا ‌می‌توانیم کد شکل ۶ را با کد زیر عوض کنیم:

serviceLayer.requestMapTiles()
.thenCompose(mapGrid ->
findAndPickUpEgg(mapGrid))
.get();

در چنین موقعیتی فراخوانی findAndPickUpEgg(mapGrid) به سرعت برنامه‌ریزی ‌می‌شود زودتر از اینکه future اولی کامل شود. در سیستم‌های پیچیده تر با رشته‌های زیاد دترسی به داده‌های مشابه، این قضیه جلوی مشکلات را خواهد گرفت.

 

استفاده از Spring برای این ماموریت
اگر پروژه مقاله قبلی را کامل کرده باشید، شاید مشتاق باشید دوباره از چهارچوب Spring استفاده کنید. فکر خوبیست، جایگاه‌های زیادی در این برنامه وجود دارد که ‌می‌توانید از اضافه کردن dependency استفاده کنید. تنها مشکل این است که اگر کلاس‌های حاشیه گذاری شده زیادی از @SpringApplication داشته باشید، Spring خوب عمل ن‌می‌کند. راه حل مناسب مطالعه راجع به پیکربندی XML است- در اینصورت ‌می‌توانید سرور و کلاینت روبات را در فایل‌های XML مجزا پیکربندی کنید.

 

روش‌های ناهمگون Spring
Spring چیزی فراتر از چهارچوب افزونه dependency است. یک ابزرا مفید @Async annotation است، که مشخص ‌می‌کند یک روش در رشته خود باید به صورت ناهمگون اجرا شود.
یکی از مزیت‌های Thread(runnable). start(); این است که ‌می‌تواند پارامتر بگیرد. برای استفاده از @Async کلاس اصلی را با روش @EnableAsync حاشیه گذاری ‌می‌کنیم. همچنین نوعی TaskExecutor اجرا ‌می‌کنیم که رابطی تولیدشده توسط Spring است که آبجکتی معرفی ‌می‌کند که مخزنی از رشته‌ها را در اختیار دارد و ‌می‌تواند از آن‌ها به صورت ناهمگون برای اجرای روش‌های مختلف استفاده کند.

نظر بدهید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

It is main inner container footer text