מבוא לקוטלין קורוטינים

1. סקירה כללית

במאמר זה נבחן קורוטינים משפת קוטלין. פשוט שים, coroutines מאפשרים לנו ליצור תוכניות אסינכרוניות בצורה קולחת מאודוהם מבוססים על הרעיון של סגנון המשך מעבר תִכנוּת.

שפת קוטלין מעניקה לנו מבנים בסיסיים אך יכולה לקבל גישה לבעלי קורות חיים שימושיים יותר באמצעות ה- kotlinx-coroutines-core סִפְרִיָה. אנו נסתכל בספריה זו ברגע שנבין את אבני הבניין הבסיסיות של שפת קוטלין.

2. יצירת Coroutine עם BuildSequence

בואו ליצור קורוטין ראשון באמצעות ה- buildSequence פוּנקצִיָה.

ובואו ליישם מחולל רצף פיבונאצ'י באמצעות פונקציה זו:

val retracementSeq = buildSequence {var a = 0 var b = 1 תשואה (1) ואילו (true) {תשואה (a + b) val tmp = a + b a = b b = tmp}}

החתימה של א תְשׁוּאָה הפונקציה היא:

תקציר ציבורי משעשע תשואה מהנה (ערך: T)

ה לְהַשְׁעוֹת מילת מפתח פירושה שפונקציה זו יכולה לחסום. פונקציה כזו יכולה להשעות א buildSequence קורוטין.

ניתן ליצור פונקציות השעיה כפונקציות קוטלין סטנדרטיות, אך עלינו להיות מודעים לכך שאנו יכולים להתקשר אליהם רק מתוך קורוטין. אחרת, נקבל שגיאת מהדר.

אם השעינו את השיחה בתוך buildSequence, קריאה זו תומר למדינה הייעודית במכונת המדינה. ניתן להעביר קורוטין ולהקצות אותו למשתנה כמו כל פונקציה אחרת.

בתוך ה פיבונאצ'י קורוטין, יש לנו שתי נקודות השעיה. ראשית, כשאנחנו מתקשרים תשואה (1) ושנית כשאנחנו מתקשרים תשואה (a + b).

אם זה תְשׁוּאָה פונקציה גורמת לשיחת חסימה כלשהי, השרשור הנוכחי לא יחסום עליה. היא תוכל לבצע קוד אחר. לאחר שהפונקציה המושהית מסיימת את ביצועה, השרשור יכול לחדש את ביצוע ה- פיבונאצ'י קורוטין.

אנו יכולים לבדוק את הקוד שלנו על ידי לקיחת כמה אלמנטים מרצף פיבונאצ'י:

val res = retracementSeq .take (5) .toList () assertEquals (res, listOf (1, 1, 2, 3, 5))

3. הוספת התלות של Maven עבור kotlinx-coroutines

בואו נסתכל על kotlinx-coroutines ספרייה הכוללת מבנים שימושיים הבנויים על גבי קורוטינים בסיסיים.

בואו נוסיף את התלות ב- kotlinx-coroutines-core סִפְרִיָה. שים לב שעלינו להוסיף גם את ה- jcenter מאגר:

 org.jetbrains.kotlinx kotlinx-coroutines-core 0.16 מרכזי //jcenter.bintray.com 

4. תכנות אסינכרוני באמצעות ה- השקת () גאורואוטינה

ה kotlinx-coroutines הספרייה מוסיפה הרבה מבנים שימושיים המאפשרים לנו ליצור תוכניות אסינכרוניות. בואו נגיד שיש לנו פונקציית חישוב יקרה שמוסיפה a חוּט לרשימת הקלטים:

להשעות כיף יקר Computation (res: MutableList) {delay (1000L) res.add ("word!")}

אנחנו יכולים להשתמש ב- לְהַשִׁיק קורוטין שיבצע את פונקציית ההשעיה בצורה לא חוסמת - עלינו להעביר מאגר חוטים כטיעון אליו.

ה לְהַשִׁיק הפונקציה מחזירה א עבודה מופע עליו אנו יכולים לקרוא לְהִצְטַרֵף() שיטה להמתין לתוצאות:

@Test כיף givenAsyncCoroutine_whenStartIt_thenShouldExecuteItInTheAsyncWay () {// נתון val res = mutableListOf () // כאשר runBlocking {val הבטחה = השקה (CommonPool) {יקר מחשבים (res)} res.add ("שלום,") הבטחה.צטרף ()} / / ואז assertEquals (res, listOf ("שלום,", "מילה!"))}

כדי להיות מסוגל לבדוק את הקוד שלנו, אנו מעבירים את כל ההיגיון אל ה- runBlocking coroutine - שזו שיחה חוסמת. לכן שלנו assertEquals () ניתן לבצע באופן סינכרוני לאחר הקוד בתוך ה- runBlocking () שיטה.

שים לב שבדוגמה זו, למרות שה- לְהַשִׁיק() השיטה מופעלת תחילה, זהו חישוב מושהה. החוט הראשי ימשיך על ידי הוספת ה- "שלום," מחרוזת לרשימת התוצאות.

לאחר העיכוב של שנייה אחת שמוצג ב- יקר מחשוב () פונקציה, ה "מִלָה!" חוּט יצורף לתוצאה.

5. קורוטינים קלים מאוד

בואו נדמיין מצב בו אנו רוצים לבצע 100000 פעולות באופן אסינכרוני. ההשרצה של מספר חוטים כה גבוה תהיה יקרה מאוד ואולי תניב OutOfMemoryException.

למרבה המזל, כאשר משתמשים ב coroutines, זה לא מקרה. אנו יכולים לבצע כמה פעולות חסימה שאנחנו רוצים. מתחת למכסה המנוע, פעולות אלה יטופלו על ידי מספר קבוע של חוטים ללא יצירת חוטים מוגזמת:

כיף @Test נתוןHugeAmountOfCoroutines_whenStartIt_thenShouldExecuteItWithoutOutOfMemory () {runBlocking {// given val counter = AtomicInteger (0) val numberOfCoroutines = 100_000 // when val jobs = List (numberOfCoroutines) {השגוי (CommonPool)) jobs.forEach {it.join ()} // ואז assertEquals (counter.get (), numberOfCoroutines)}}

שים לב שאנחנו מבצעים 100,000 קורוטינים וכל ריצה מוסיפה עיכוב משמעותי. עם זאת, אין צורך ליצור יותר מדי שרשורים מכיוון שפעולות אלה מבוצעות בצורה אסינכרונית באמצעות שרשור מה בריכה נפוצה.

6. ביטול ופסקי זמן

לפעמים, לאחר שהפעלנו חישוב אסינכרוני ארוך טווח, אנו רוצים לבטל אותו מכיוון שאנחנו כבר לא מעוניינים בתוצאה.

כאשר אנו מתחילים את הפעולה האסינכרונית שלנו עם ה- לְהַשִׁיק() אנו יכולים לבחון את הוא פעיל דֶגֶל. דגל זה מוגדר כ- false בכל פעם שהחוט הראשי קורא ל- לְבַטֵל() השיטה למשל עבודה:

@Test fun givenCancellableJob_whenRequestForCancel_thenShouldQuit () {runBlocking {// given val job = launch (CommonPool) {while (isActive) {println ("is working")}} עיכוב (1300L) // כאשר job.cancel () // ואז בטל בהצלחה}}

זהו אלגנטי מאוד דרך קלה להשתמש במנגנון הביטול. בפעולה האסינכרונית, עלינו רק לבדוק אם ה- הוא פעיל הדגל שווה ל- שֶׁקֶר ובטל את העיבוד שלנו.

כאשר אנו מבקשים עיבוד כלשהו ואיננו בטוחים כמה זמן ייקח לחישוב, מומלץ לקבוע את פסק הזמן לפעולה כזו. אם העיבוד לא יסתיים בתוך זמן הקצוב הנתון, נקבל חריג ונוכל להגיב אליו כראוי.

לדוגמה, אנו יכולים לנסות שוב את הפעולה:

@Test (צפוי = CancellationException :: class) fun givenAsyncAction_whenDeclareTimeout_thenShouldFinishWhenTimedOut () {runBlocking {withTimeout (1300L) {repeat (1000) {i -> println ("כמה חישוב יקר $ i ...") עיכוב (500L)}}} }

אם אנחנו לא מגדירים פסק זמן, יתכן שהשרשור שלנו ייחסם לנצח מכיוון שהחישוב הזה יתלה. איננו יכולים לטפל במקרה זה בקוד שלנו אם לא מוגדר פסק הזמן.

7. הפעלת פעולות אסינכרוניות במקביל

בואו נגיד שעלינו להתחיל שתי פעולות אסינכרוניות במקביל ולחכות לתוצאותיהן לאחר מכן. אם העיבוד שלנו לוקח שנייה אחת ואנחנו צריכים לבצע את העיבוד הזה פעמיים, זמן הריצה של ביצוע חסימה סינכרונית יהיה שתי שניות.

עדיף שנוכל להריץ את שתי הפעולות האלה בשרשור נפרד ולהמתין לתוצאות הללו בשרשור הראשי.

אנחנו יכולים למנף את אסינכרון () קורוטין כדי להשיג זאת על ידי התחלת עיבוד בשני שרשורים נפרדים במקביל:

@Test fun givenHaveTwoExpensiveAction_whenExecuteThemAsync_thenTheyShouldRunConcurrently () {runBlocking {val delay = 1000L val time = measureTimeMillis {// given val one = async (CommonPool) {someExpensiveComputation (delay)} val two = asyncation (Commonpool) runBlocking {one.await () two.await ()}} // ואז assertTrue (זמן <עיכוב * 2)}}

לאחר הגשת שני החישובים היקרים, אנו משעים את קוריוטינה על ידי ביצוע ה- runBlocking () שִׂיחָה. פעם תוצאות אחד ו שתיים זמינים, קוריוטין יתחדש והתוצאות יוחזרו. ביצוע שתי משימות באופן זה אמור להימשך כשנייה אחת.

אנחנו יכולים לעבור CoroutineStart.LAZY כטיעון השני ל אסינכרון () שיטה, אך פירוש הדבר שהחישוב האסינכרוני לא יופעל עד שיתבקש. מכיוון שאנחנו מבקשים חישוב ב runBlocking קורוטין, זה אומר הקריאה ל two.await () ייעשה רק פעם אחת one.await () סיים:

@Test כיף givenTwoExpensiveAction_whenExecuteThemLazy_thenTheyShouldNotConcurrently () {runBlocking {val delay = 1000L val time = measureTimeMillis {// given val one = async (CommonPool, CoroutineStart.LAZY) {someExpensiveComputation (delay)} common (ערך) שכיח) someExpensiveComputation (עיכוב)} // כאשר runBlocking {one.await () two.await ()}} // ואז assertTrue (זמן> עיכוב * 2)}}

העצלנות של הביצוע בדוגמה מסוימת זו גורמת לקוד שלנו לפעול באופן סינכרוני. זה קורה כי כשאנחנו מתקשרים לְהַמתִין(), החוט הראשי חסום ורק לאחר המשימה אחד מסיים את המשימה שתיים יופעל.

עלינו להיות מודעים לביצוע פעולות אסינכרוניות בצורה עצלה מכיוון שהן עשויות לרוץ בצורה חוסמת.

8. מסקנה

במאמר זה בחנו את היסודות של קורוטינים קורטלין.

ראינו את זה buildSequence הוא אבן הבניין העיקרית של כל קורוטיין. תיארנו כיצד נראה זרימת הביצוע בסגנון תכנות זה שעובר המשך.

לבסוף, הסתכלנו על kotlinx-coroutines ספרייה המשלחת הרבה מבנים שימושיים מאוד ליצירת תוכניות אסינכרוניות.

ניתן למצוא את היישום של כל הדוגמאות וקטעי הקוד בפרויקט GitHub.


$config[zx-auto] not found$config[zx-overlay] not found