קריאה אסינכרונית משרת HTTP בGO
-
@dovid
קודם כל אני מופתע שאתה מונח טוב בGO (מספיק בשביל לדבג)
בדקתי עכשיו מה קורה אם אני משתמש בשרת fasthttp שעליו פיבר מבוסס (הוא גם משתמש בctx)
ושם התוצאה תקינה
כנראה שהבעיה היא בפיברpackage main import ( "flag" "fmt" "log" "time" "github.com/valyala/fasthttp" ) var ( addr = flag.String("addr", ":3000", "TCP address to listen to") compress = flag.Bool("compress", false, "Whether to enable transparent response compression") ) func main() { flag.Parse() h := requestHandler if *compress { h = fasthttp.CompressHandler(h) } if err := fasthttp.ListenAndServe(*addr, h); err != nil { log.Fatalf("Error in ListenAndServe: %s", err) } } func requestHandler(ctx *fasthttp.RequestCtx) { go myfunc(string(ctx.Path())) fmt.Fprintf(ctx, "Requested path is %q\n", ctx.Path()) } func myfunc(number string) { fmt.Printf("number is %s \n", number) time.Sleep(1 * time.Second) fmt.Printf("number is now %s \n", number) }
-
פוסט זה נמחק!
-
מצאתי את מקור הבעיה
https://github.com/gofiber/fiber/blob/efe89d243de6d8cf964581f5832b4341e2baa427/utils.go#L128-L130
הפונקציה הנ"ל בשימוש רחב בקובץ context.go
וזה הרי כבר מתועד בתיעוד הרשמי של fasthttp שאסור להשתמש ב-ctx
אחרי שה-handler חוזר. וכן בהערה כאן, ועיין עוד: https://github.com/valyala/fasthttp/issues/146אם fiber לא היו מחפים על השימוש ב-byte array על ידי המרה בצורה לא בטוחה למחרוזת היה יותר קל למצוא את הבעיה.
@dovid אמר בקריאה אסינכרונית משרת HTTP בGO:
אז הנה למדנו משהו על go
לפי דרכי למדנו שלא כדאי להשתמש ב-
unsafe
בלי להבין טוב טוב את ההשלכות... (דבר פשוט מאוד...) -
@dovid אם הבנתי נכון מה שאתה שואל אז התשובה היא שמשתנה מסוג string ב-GO לא מכיל את הבייטים של המחרוזת, הוא רק מצביע למקום בזכרון. לכן על אף שלכל קריאה ל-myfunc מיוצר string חדש, אבל כולם מצביעים על אותו איזור בזיכרון.
עיין כאן: https://www.reddit.com/r/golang/comments/57ica9 -
@yossiz אני שואל איך יכול להיות שהnumber נהרס ע"י גישה ישירה לזיכרון לtype של הctx.
הרי הוא עותק. שהרי כשאתה משים משתנה מחרוזת למשתנה אחר זה כן כתובת שונה ותוכן בלתי תלוי, אז מה זה שונה.
בכל מקרה גם בשימוש בunsafe לא מתקבל על הדעת שיהיה דרך בשפה עילית מודרנית לשנות ערכים של זיכרון שלא באחריות הקוד שלי, אני ממש לא מבין איך זה ייתכן.
אני מבין שאני מפספס משהו, מה קורה כעושים number:= ctx.XXX. -
@dovid אמר בקריאה אסינכרונית משרת HTTP בGO:
שהרי כשאתה משים משתנה מחרוזת למשתנה אחר זה כן כתובת שונה ותוכן בלתי תלוי, אז מה זה שונה
הבייטים של המחרוזת הם אותם בייטים, אממה הכתובת של המשתנה הוא לא הכתובת של הבייטים אלא הכתובת של המצביע. (ומכיון ש-string ב-GO הוא immutable - בלי משחקים ב-unsafe - זה בטוח לגמרי, אבל ברגע שמשחקים עם הבייטים עצמם על ידי קוד unsafe כאן יתחילו בעיות)
תעשה ניסוי קטן:
package main import ( "fmt" "unsafe" ) func main() { b := []byte("ABC") s := *(*string)(unsafe.Pointer(&b)) s2 := s s3 := string(b) // this is safe (it makes a copy of the original bytes) b[0] = 'X' b[1] = 'Y' b[2] = 'Z' fmt.Println(s) fmt.Println(s2) fmt.Println(s3) }
https://play.golang.org/p/EpPwsoM-z8g
זה פחות או יותר מה שקורה בתוכנה של @nigun
-
@yossiz
נראה לי שעכשיו הבנתי קצת מה קורה כאן
המשתנה number הוא מערך של bytes
והוא מצביע על מערך אחר של bytes (שנמצא בCtx ???)
כשאני מדפיס בפעם הראשונה את number זה מיד אחרי שקראתי לfunc(c *fiber.Ctx)
ולפני שנעשה קריאה חדשה לfunc(c *fiber.Ctx)
ולכן הוא מדפיס את הערך האחרון (שהוא הנכון)
אבל בפעם השניה הערך הוא המערך במקורי הוא[57 49]
שזה "19" (כי זה היה הקריאה האחרונה)
אבל המצביע number נשאר עם מערך של איבר אחד
לכן הוא ממיר לסטרינג רק את האיבר הראשון במערך ולכן הוא מדפיס "1"
עד שהוא מגיע למערכים בני שני איברים (כי בקריאה המקורית זה היה עם שני ספרות) ואז הוא מדפיס "19"
והסיבה שהוא נשאר מצביע ולא משתנה חדש זה בגלל שהוא נוצר עם unsafe (שנשאר להצביע תמיד על הכתובת הראשונה)`
-
@nigun עד כמה שחשבתי עד היום...
אני כעת בדקתי וזה אכן קורה גם בC#:void Main() { var t = new strTest(); var str = t.GetStr(); t.Change(); Console.WriteLine(str); } public class strTest { string str = "ABC"; public string GetStr() => str; public void Change() { unsafe { fixed (char* p = str) p[0] = 'a'; } } }
אכן לא תמיד חוסר ידע שלי זה אשמת השפות
-
@nigun אמר בקריאה אסינכרונית משרת HTTP בGO:
נראה לי שעכשיו הבנתי קצת מה קורה כאן
בגדול אני מסכים עם ההסבר למרות שהייתי משנה טיפה את הנוסח.
המשתנה number הוא מערך של bytes
והוא מצביע על מערך אחר של bytesהמשתנה number הוא string. ותמיד משתנה מסוג string ב-GO הוא struct שיש בו 2 שדות, 1) מצביע לבייטים של המחרוזת 2) מספר שאומר מה אורך המחרוזת (כמה בייטים לקחת מהמיקום ששדה 1 מצביע עליו)
עכשיו, אם אתה מייצר סטרינג רגיל, סביבת ההרצה דואגת שהסטרינג יהיה immutable (שאין דרך - בלי קוד unsafe - לשנות את הערכים של הבייטים שעליו הוא מצביע), ולכן, אם סביבת ההרצה רואה שיצרת ממערך בייטים שהוא הרי mutable, הוא ידאוג להעתיק את הבייטים המקוריים למיקום חדש שלא ניתן לגישה מקוד רגיל, אבל אם ייצרת אותו ממערך בייטים עם המילה unsafe זה כאילו להגיד לסביבת ההרצה "אל תדאג, אני יודע מה אני עושה, אל תתערב לי" ואז אפשר לשנות את המערך המקורי ובזה לשנות את המחרוזת שהסטרינג ידפיס.עד שהוא מגיע למערכים בני שני איברים (כי בקריאה המקורית זה היה עם שני ספרות) ואז הוא מדפיס "19"
יפה! לנקודה הזאת לא שמתי לב, זה החלק שהיה חסר לי בפאזל.
והסיבה שהוא נשאר מצביע ולא משתנה חדש זה בגלל שהוא נוצר עם unsafe
לא ממש מדוייק, תמיד string יהיה מצביע, אבל כנ"ל באריכות, ההבדל הוא אם הוא מצביע למיקום שנגיש מקוד אחר או לא
מקוה שהסברתי ברור...
-
@nigun אמר בקריאה אסינכרונית משרת HTTP בGO:
בדקתי עכשיו מה קורה אם אני משתמש בשרת fasthttp שעליו פיבר מבוסס (הוא גם משתמש בctx)
ושם התוצאה תקינה
כנראה שהבעיה היא בפיברעיין כאן איך אפשר לממש את הבאג גם ב-fasthttp.
הדוגמה שלך עובד טוב כי הפונקציה string מייצר string חדש בצורה בטוחה.@dovid אמר בקריאה אסינכרונית משרת HTTP בGO:
אני כעת בדקתי וזה אכן קורה גם בC#:
השימוש במילת unsafe מחזיר אותך אחורה לימי ה-C שיש לך שליטה מלאה על כל מרחב הזיכרון של התהליך
ב-JS אסור בהחלט שזה יקרה כי כך יהיה אפשר לפרוץ החוצה מארגז החול של הדפדפן, ולכן באמת אין unsafe ב-JS. -
@yossiz אמר בקריאה אסינכרונית משרת HTTP בGO:
לא ממש מדוייק, תמיד string יהיה מצביע, אבל כנ"ל באריכות, ההבדל הוא אם הוא מצביע למיקום שנגיש מקוד אחר או לא
תודה על כל ההסברים המדוייקים
רק לא הבנתי את הקטע האחרון
האם כל פעם שאני יוצר סטרינג חדש (רגיל) הוא מצביע לכתובת חדשה, ואם משתמשים בunsafe אז הוא מצביע על המקורי
או שזה עובד אחרת? -
@nigun אמר בקריאה אסינכרונית משרת HTTP בGO:
האם כל פעם שאני יוצר סטרינג חדש (רגיל) הוא מצביע לכתובת חדשה, ואם משתמשים בunsafe אז הוא מצביע על המקורי
או שזה עובד אחרת?זה עובד אחרת, זה תלוי אם
סביבת ההרצההקומפיילר חוששת שהבייטים ישתנו, משום שלכל סטרינג יש חוזה (implicit contract) שהוא לא הולך להשתנות (immutable), וסביבת ההרצההקומפיילר דואגת לאכוף את זה. (אבל בשימוש ב-unsafe אתה אומר לסביבת ההרצהקומפיילר "אל תדאג, אני יודע מה אני עושה, אל תתערב לי")
(אפשר לבדוק את זה על ידי הדפסת תוכן אובייקט ה-string כמו כאן)
(או שאפשר לקרוא את קוד המקור שלסביבת ההרצההקומפיילר...)package main import ( "fmt" "reflect" "unsafe" ) func main() { b := []byte("ABC") s := *(*string)(unsafe.Pointer(&b)) // מצביע על הבייטים המקוריים s2 := s // עדיין מצביע על הבייטים המקוריים s3 := string(b) // מעתיק את הבייטים המקוריים ומצביע על המיקום של הבייטים החדשים s4 := s3 // לא מעתיק את הבייטים המקוריים כי סביבת ההרצה בטוח שהבייטים המקוריים לא ישתנו s5 := string(s4) // עדיין לא מעתיק וכנ"ל dumpStringAddr(s) dumpStringAddr(s2) dumpStringAddr(s3) dumpStringAddr(s4) dumpStringAddr(s5) } func dumpStringAddr(s string) { address := (*reflect.StringHeader)(unsafe.Pointer(&s)).Data fmt.Printf("%08x\n", address) }
-
@yossiz
מה הצורה הכי נוחה ליצור משתנה חדש אחרי שכבר המשתנה הקודם נשמר בunsafe
אני עשיתי המרה לביטים ואז המרה חזרה לסטרינג השאלה האם יש משהו יותר אלגנטי?app.Get("/:number", func(c *fiber.Ctx) { number := c.Path() str := []byte(number) go myfunc(string(str) )