سوالات مصاحبه حیلهآمیز Golang – قسمت 6: خواندن غیرمسدود

Summarize this content to 400 words in Persian Lang
این مشکل بیشتر به بررسی کد مربوط می شود. این نیاز به دانش در مورد کانال ها و موارد منتخب دارد، همچنین مسدود کردن، آن را به یکی از سخت ترین سوالات مصاحبه ای تبدیل می کند که در حرفه خود با آن مواجه شدم. در این نوع سوالات، زمینه در نگاه اول نامشخص است و نیاز به درک عمیقی از انسداد و بن بست دارد. در حالی که مقالههای قبلی بیشتر موضوعات پیشرفته دورههای متوسطه یا ابتدایی را هدف قرار میدادند، این یکی یک مشکل در سطح ارشدتر است.
سوال: یکی از هم تیمی های شما این کد را برای بررسی کد ارسال کرده است. این کد یک تهدید بالقوه دارد. آن را شناسایی کنید و برای حل آن راه حل بدهید.
package main
import (
“fmt”
“time”
)
func main() {
ch := make(chan int)
go func() {
time.Sleep(2 * time.Second)
ch 42
fmt.Println(“Sent: 42”)
}()
val := ch
fmt.Println(“Received:”, val)
fmt.Println(“Continuing execution…”)
}
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
در نگاه اول هیچ چیز مشکوکی در این کد وجود ندارد. اگر بخواهیم آن را اجرا کنیم در واقع بدون هیچ مشکلی کامپایل و اجرا می شود.
[Running] go run “main.go”Sent: 42
Received: 42
Continuing execution…
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
خود کد نیز خوب به نظر می رسد. ما مصرف همزمان را با 2 گوروتین که به طور مستقل کار می کنند به درستی اجرا کرده ایم. بیایید کد را تجزیه کنیم و ببینیم چه اتفاقی می افتد:
یک کانال ch ایجاد شده است با make(chan int). این یک کانال بافر نشده است.
یک گوروتین شروع می شود که به مدت 2 ثانیه می خوابد و سپس مقدار 42 را به کانال ارسال می کند.
تابع اصلی یک عملیات خواندن را روی ch با انجام می دهد val := .
باز هم خوب به نظر می رسد اما چیزی که ما در اینجا داریم این است که عملیات ارسال به تاخیر افتاده است. گوروتین ناشناس قبل از ارسال مقدار به کانال، 2 ثانیه منتظر می ماند. بنابراین وقتی این کد را اجرا می کنیم، تابع اصلی شروع به خواندن کانال می کند و قبل از اینکه کانال با یک مقدار پر شود، انتظار یک مقدار را در آنجا دارد. این عملیات اجرای بیشتر کد را مسدود می کند.
خواندن عملیات در کانال های خالی
در Go، وقتی سعی می کنید از یک کانال خالی بخوانید، عملیات خواندن مسدود می شود تا زمانی که یک مقدار در دسترس قرار گیرد. این بدان معناست که گوروتینی که خواندن را انجام میدهد، متوقف میشود و تا زمانی که نتواند با موفقیت یک مقدار را از کانال بخواند، عملیات بعدی را ادامه نخواهد داد.
هنگامی که کد یک عملیات خواندن را در یک کانال انجام می دهد:
کانال بافر نشده: اگر کانال بدون بافر باشد و مقداری در دسترس نباشد، عملیات خواندن مسدود می شود تا زمانی که گوروتین دیگری مقداری را به کانال ارسال کند.
کانال بافر شده: اگر کانال بافر باشد، در صورت خالی بودن بافر، عملیات خواندن مسدود می شود.
تأخیر 2 ثانیه ای در این مورد چندان محسوس نخواهد بود و کسی که اجرا را مشاهده می کند حتی متوجه شکاف نمی شود، اما از منظر زمان اجرا، کل جریان اجرا بود. به مدت 2 ثانیه متوقف شد. تا زمانی که مقدار 42 پس از 2 ثانیه ارسال شود، گوروتین اصلی مسدود می شود val := . A blocking read halts all subsequent code execution until the read operation completes. This can lead to a program that appears to be frozen if there is no other goroutine sending data to the channel. If more operations are supposed to follow, they are delayed.
به عنوان مثال، در سناریوهای دنیای واقعی، ما یک برنامه مینی یوتیوب ایجاد کرده ایم. یکی از سنگینترین اجزای یوتیوب، رمزگذار ویدیو است که برای مثال، به عنوان مجموعهای از خدمات کارگری نشان داده میشود.
فرآیند رمزگذاری ویدیو می تواند از چند دقیقه تا چند ساعت طول بکشد. تصور کنید عملکرد اصلی ما یک ویدیوی طولانی 24 ساعته را به رمزگذار ارسال می کند که ممکن است پردازش آن 3-4 ساعت طول بکشد. هر چیزی که بعد از خط خواندن کانال نوشته شود برای ساعت ها مسدود خواهد شد. در نتیجه، تا زمانی که رمزگذاری ویدیو کامل نشود، باطن شما قادر به انجام هیچ کار دیگری نخواهد بود. اگر تایمر خواب را به 20 ثانیه افزایش دهید time.Sleep(20 * time.Second) متوجه خواهید شد که چقدر طول می کشد تا آخرین دستور چاپ در گزارش خروجی ظاهر شود.
عواقب مسدود کردن
همانطور که قبلاً بحث کردیم، خواندن مسدود کردن، اجرای کد بعدی را تا پایان عملیات خواندن متوقف میکند. این میتواند منجر به برنامهای شود که به نظر میرسد در صورت عدم ارسال گوروتین دیگری به کانال، مسدود شده است.
ممکن است باعث مشکلات همزمانی جدی شود. اگر گوروتین اصلی (یا هر گوروتین بحرانی) به طور نامحدود انتظار برای داده ها را مسدود کند، می تواند از اجرای سایر وظایف مهم جلوگیری کند که منجر به بن بست یا رفتار بی پاسخ شود.
مشکلات استفاده از منابع در حالی که گوروتین مسدود است، منابع CPU را به طور فعال مصرف نمی کند، اما منابع منطقی مانند پشته های گوروتین و به طور بالقوه سایر وظایف وابسته را به هم متصل می کند.
جایگزین های غیر مسدود کننده
برای جلوگیری از مسدود کردن خواندن، میتوانید از گزینههای غیرمسدود مانند عبارت select با حروف پیشفرض استفاده کنید. دستور select در Go یک ویژگی قدرتمند است که به یک گوروتین اجازه میدهد تا در چند عملیات ارتباطی منتظر بماند و این امکان را فراهم میکند تا عملیات غیر مسدودکننده را انجام دهد و چندین کانال را مدیریت کند. دستور select با ارزیابی چندین عملیات کانال و ادامه اولین مورد آماده کار می کند. اگر چندین عملیات آماده باشد، یکی از آنها به صورت تصادفی انتخاب می شود. اگر هیچ عملیاتی آماده نباشد، حالت پیشفرض، در صورت وجود، اجرا میشود و آن را به یک عملیات غیر مسدود تبدیل میکند.
نحو اصلی دستور select:
select {
case ch1:
// Do something when ch1 is ready for receiving
case ch2 value:
// Do something when ch2 is ready for sending
default:
// Do something when no channels are ready (non-blocking path)
}
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
بررسی کد
به عنوان یک مرورگر کد، باید بتوانید این کد بالقوه خطرناک را شناسایی کنید، توضیح خوبی در مورد نحوه اجتناب از آن ارائه دهید و هم تیمی را تشویق کنید تا مشکل را برطرف کند. برای رفع مشکل اجازه دهید a را پیاده سازی کنیم select بیانیه. اصلاح به شکل زیر خواهد بود:
package main
import (
“fmt”
“time”
)
func main() {
ch := make(chan int)
// Goroutine to send data to the channel after 2 seconds
go func() {
time.Sleep(2 * time.Second)
ch 42
fmt.Println(“Sent: 42”)
}()
// Main function performing a non-blocking read
for {
select {
case val := ch:
fmt.Println(“Received:”, val)
fmt.Println(“Continuing execution…”)
return
default:
fmt.Println(“No value received”)
time.Sleep(500 * time.Millisecond) // Sleep for a while to prevent busy looping
// handle the execution flow of instructions and operations that must continue
}
}
}
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
حالا اگر این را اجرا کنیم، رفتار زیر را خواهیم دید:
[Running] go run “main.go”No value received
No value received
No value received
No value received
Received: 42
Continuing execution…
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
این main در مواقعی که کانال خالی است، تابع به طور مکرر “بدون دریافت داده” را چاپ می کند و با در دسترس قرار گرفتن مقادیر با “دریافت: 42” در هم آمیخته می شود. کیس پیشفرض تضمین میکند که عملکرد اصلی مسدود نمیشود و میتواند عملیات دیگری را انجام دهد (مانند چاپ «دادهای دریافت نشده» و خوابیدن). این مکانیسم تضمین میکند که عملکرد اصلی، حتی اگر یک یا هر دو کانال دادهای در دسترس نداشته باشند، پاسخگو باقی بماند.
به همین راحتی!
این مشکل بیشتر به بررسی کد مربوط می شود. این نیاز به دانش در مورد کانال ها و موارد منتخب دارد، همچنین مسدود کردن، آن را به یکی از سخت ترین سوالات مصاحبه ای تبدیل می کند که در حرفه خود با آن مواجه شدم. در این نوع سوالات، زمینه در نگاه اول نامشخص است و نیاز به درک عمیقی از انسداد و بن بست دارد. در حالی که مقالههای قبلی بیشتر موضوعات پیشرفته دورههای متوسطه یا ابتدایی را هدف قرار میدادند، این یکی یک مشکل در سطح ارشدتر است.
سوال: یکی از هم تیمی های شما این کد را برای بررسی کد ارسال کرده است. این کد یک تهدید بالقوه دارد. آن را شناسایی کنید و برای حل آن راه حل بدهید.
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan int)
go func() {
time.Sleep(2 * time.Second)
ch 42
fmt.Println("Sent: 42")
}()
val := ch
fmt.Println("Received:", val)
fmt.Println("Continuing execution...")
}
در نگاه اول هیچ چیز مشکوکی در این کد وجود ندارد. اگر بخواهیم آن را اجرا کنیم در واقع بدون هیچ مشکلی کامپایل و اجرا می شود.
[Running] go run "main.go"
Sent: 42
Received: 42
Continuing execution...
[Done] exited with code=0 in 2.124 seconds
خود کد نیز خوب به نظر می رسد. ما مصرف همزمان را با 2 گوروتین که به طور مستقل کار می کنند به درستی اجرا کرده ایم. بیایید کد را تجزیه کنیم و ببینیم چه اتفاقی می افتد:
- یک کانال
ch
ایجاد شده است باmake(chan int)
. این یک کانال بافر نشده است. - یک گوروتین شروع می شود که به مدت 2 ثانیه می خوابد و سپس مقدار 42 را به کانال ارسال می کند.
- تابع اصلی یک عملیات خواندن را روی ch با انجام می دهد
val := .
باز هم خوب به نظر می رسد اما چیزی که ما در اینجا داریم این است که عملیات ارسال به تاخیر افتاده است. گوروتین ناشناس قبل از ارسال مقدار به کانال، 2 ثانیه منتظر می ماند. بنابراین وقتی این کد را اجرا می کنیم، تابع اصلی شروع به خواندن کانال می کند و قبل از اینکه کانال با یک مقدار پر شود، انتظار یک مقدار را در آنجا دارد. این عملیات اجرای بیشتر کد را مسدود می کند.
خواندن عملیات در کانال های خالی
در Go، وقتی سعی می کنید از یک کانال خالی بخوانید، عملیات خواندن مسدود می شود تا زمانی که یک مقدار در دسترس قرار گیرد. این بدان معناست که گوروتینی که خواندن را انجام میدهد، متوقف میشود و تا زمانی که نتواند با موفقیت یک مقدار را از کانال بخواند، عملیات بعدی را ادامه نخواهد داد.
هنگامی که کد یک عملیات خواندن را در یک کانال انجام می دهد:
- کانال بافر نشده: اگر کانال بدون بافر باشد و مقداری در دسترس نباشد، عملیات خواندن مسدود می شود تا زمانی که گوروتین دیگری مقداری را به کانال ارسال کند.
- کانال بافر شده: اگر کانال بافر باشد، در صورت خالی بودن بافر، عملیات خواندن مسدود می شود.
تأخیر 2 ثانیه ای در این مورد چندان محسوس نخواهد بود و کسی که اجرا را مشاهده می کند حتی متوجه شکاف نمی شود، اما از منظر زمان اجرا، کل جریان اجرا بود. به مدت 2 ثانیه متوقف شد. تا زمانی که مقدار 42 پس از 2 ثانیه ارسال شود، گوروتین اصلی مسدود می شود val := . A blocking read halts all subsequent code execution until the read operation completes. This can lead to a program that appears to be frozen if there is no other goroutine sending data to the channel. If more operations are supposed to follow, they are delayed.
به عنوان مثال، در سناریوهای دنیای واقعی، ما یک برنامه مینی یوتیوب ایجاد کرده ایم. یکی از سنگینترین اجزای یوتیوب، رمزگذار ویدیو است که برای مثال، به عنوان مجموعهای از خدمات کارگری نشان داده میشود.
فرآیند رمزگذاری ویدیو می تواند از چند دقیقه تا چند ساعت طول بکشد. تصور کنید عملکرد اصلی ما یک ویدیوی طولانی 24 ساعته را به رمزگذار ارسال می کند که ممکن است پردازش آن 3-4 ساعت طول بکشد. هر چیزی که بعد از خط خواندن کانال نوشته شود برای ساعت ها مسدود خواهد شد. در نتیجه، تا زمانی که رمزگذاری ویدیو کامل نشود، باطن شما قادر به انجام هیچ کار دیگری نخواهد بود. اگر تایمر خواب را به 20 ثانیه افزایش دهید time.Sleep(20 * time.Second)
متوجه خواهید شد که چقدر طول می کشد تا آخرین دستور چاپ در گزارش خروجی ظاهر شود.
عواقب مسدود کردن
- همانطور که قبلاً بحث کردیم، خواندن مسدود کردن، اجرای کد بعدی را تا پایان عملیات خواندن متوقف میکند. این میتواند منجر به برنامهای شود که به نظر میرسد در صورت عدم ارسال گوروتین دیگری به کانال، مسدود شده است.
- ممکن است باعث مشکلات همزمانی جدی شود. اگر گوروتین اصلی (یا هر گوروتین بحرانی) به طور نامحدود انتظار برای داده ها را مسدود کند، می تواند از اجرای سایر وظایف مهم جلوگیری کند که منجر به بن بست یا رفتار بی پاسخ شود.
- مشکلات استفاده از منابع در حالی که گوروتین مسدود است، منابع CPU را به طور فعال مصرف نمی کند، اما منابع منطقی مانند پشته های گوروتین و به طور بالقوه سایر وظایف وابسته را به هم متصل می کند.
جایگزین های غیر مسدود کننده
برای جلوگیری از مسدود کردن خواندن، میتوانید از گزینههای غیرمسدود مانند عبارت select با حروف پیشفرض استفاده کنید. دستور select در Go یک ویژگی قدرتمند است که به یک گوروتین اجازه میدهد تا در چند عملیات ارتباطی منتظر بماند و این امکان را فراهم میکند تا عملیات غیر مسدودکننده را انجام دهد و چندین کانال را مدیریت کند. دستور select با ارزیابی چندین عملیات کانال و ادامه اولین مورد آماده کار می کند. اگر چندین عملیات آماده باشد، یکی از آنها به صورت تصادفی انتخاب می شود. اگر هیچ عملیاتی آماده نباشد، حالت پیشفرض، در صورت وجود، اجرا میشود و آن را به یک عملیات غیر مسدود تبدیل میکند.
نحو اصلی دستور select:
select {
case ch1:
// Do something when ch1 is ready for receiving
case ch2 value:
// Do something when ch2 is ready for sending
default:
// Do something when no channels are ready (non-blocking path)
}
بررسی کد
به عنوان یک مرورگر کد، باید بتوانید این کد بالقوه خطرناک را شناسایی کنید، توضیح خوبی در مورد نحوه اجتناب از آن ارائه دهید و هم تیمی را تشویق کنید تا مشکل را برطرف کند. برای رفع مشکل اجازه دهید a را پیاده سازی کنیم select
بیانیه. اصلاح به شکل زیر خواهد بود:
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan int)
// Goroutine to send data to the channel after 2 seconds
go func() {
time.Sleep(2 * time.Second)
ch 42
fmt.Println("Sent: 42")
}()
// Main function performing a non-blocking read
for {
select {
case val := ch:
fmt.Println("Received:", val)
fmt.Println("Continuing execution...")
return
default:
fmt.Println("No value received")
time.Sleep(500 * time.Millisecond) // Sleep for a while to prevent busy looping
// handle the execution flow of instructions and operations that must continue
}
}
}
حالا اگر این را اجرا کنیم، رفتار زیر را خواهیم دید:
[Running] go run "main.go"
No value received
No value received
No value received
No value received
Received: 42
Continuing execution...
[Done] exited with code=0 in 2.31 seconds
این main
در مواقعی که کانال خالی است، تابع به طور مکرر “بدون دریافت داده” را چاپ می کند و با در دسترس قرار گرفتن مقادیر با “دریافت: 42” در هم آمیخته می شود. کیس پیشفرض تضمین میکند که عملکرد اصلی مسدود نمیشود و میتواند عملیات دیگری را انجام دهد (مانند چاپ «دادهای دریافت نشده» و خوابیدن). این مکانیسم تضمین میکند که عملکرد اصلی، حتی اگر یک یا هر دو کانال دادهای در دسترس نداشته باشند، پاسخگو باقی بماند.
به همین راحتی!