อยากเขียนเรื่อง Atomic Operations แต่ว่า…

Tae Keerati Jearjindarat
4 min readJul 26, 2021

สวัสดีครับทุกคน จริงๆ วันนี้จะเขียนเรื่อง Atomic Operations แต่อ่านไปอ่านมาก็เจอสิ่งที่พื้นฐานกว่านั้นเลยคิดว่า เราลงพื้นฐานของเรื่องนี้ก่อนดีกว่าแล้วค่อยไปขั้นต่อไปอีกที และ ผมมองว่าเรื่องพวกนี้มันกระจัดกระจายเลยเขียนเพื่อรวมเอาไว้ว่ามันมีที่มาที่ไปยังไง จึงเกิดเป็นบล็อคนี้ขึ้นมาครับ

และเนื่องจากวันนี้ก็เป็นวันที่เขียน Backend แบบเต็มตัวได้ปีกว่า ๆ แล้ว ซึ่งสิ่งที่เจอมาตลอดคือเรื่อง Locking ที่ทำเพื่อป้องกัน Race condition ที่จะเกิดขึ้นง่ายมากในการทำ Application สมัยนี้ ผมเลยอยากทำความเข้าใจให้มากขึ้นไปอีก

ซึ่งหากอยากอ่านเรื่อง Race condition ที่ง่ายกว่าบล็อคนี้หน่อยนึง ผมได้เขียนไว้ที่ Concurrency Control 101 แล้วติดตามกันได้ครับ

Shared Data

จากคำที่ว่า Race Condition มันเกิดขึ้นง่ายเพราะว่า Applicaiton สมัยนี้มักจะมี shared data เสมอ เช่น ระบบ Marketplace จะมี data ที่ถูก share กันระหว่างผู้ซื้อและผู้ขาย นั่นคือสินค้าที่จะขาย หรือ พวก Social Network ก็จะมี data ที่ถูก share ก็คือผู้ใช้งาน หรือ รายชื่อเพื่อน เป็นต้น

โดยความหมายของ shared data ในที่นี้คือ data ที่สามารถถูก access ได้จากหลายที่พร้อมๆ กัน เช่น ข้อมูลสินค้าอาจจะถูก access ได้พร้อมกัน 2 browser ประกอบไปด้วย browser ของผู้ซื้อกับผู้ขาย โดยผู้ซื้อก็จะเพิ่มสินค้าลงตระกร้า ส่วนผู้ขายกำลังจะแก้ไขสินค้าเพื่อลดราคาเป็นโปรโมชันของเดือนนั้น ๆ หรือข้อมูลระบบ Social Network ที่มีคนที่ติดตามบุคคลหนึ่งกำลังส่องโปรไฟล์อยู่ แต่ในขณะเดียวกันเจ้าของโปรไฟล์ดันเปลี่ยนชื่อพอดี หรือแม้กระทั่งการแก้ไขข้อมูลเกม offline ใน ram หรือการ scale kubernetes หลาย ๆ pod ให้ต่อ database ตัวเดียวกัน เป็นต้น

เวลามี shared data เราจะมักจะแก้ไขปัญหา Race condition ด้วยเทคนิคที่เรียกว่าการ Lock ตาม Feature ของ Transaction ที่มีใน Database ที่เราใช้ ซึ่งส่วนใหญ่จะมีติดมาเสมอในพวก RDBMS แต่ใน Product ของ NoSQL จะมี Atomic Operations ในบาง Level ของ Architecture ซึ่งสุดท้ายส่วนใหญ่จะใช้เทคนิค Optimistic Lock ในระดับ Application Level แต่พื้นฐานคือสิ่งเดียวกันนั่นคือ Compare and Swap ซึ่งจริงๆ แล้วก็เป็นพื้นฐานของหลาย ๆ Feature หรือ Product ต่าง ๆ และเป็นเทคนิคที่ Simple และแก้ปัญหาได้ดีทีเดียว

Compare and Swap (CAS)

เป็นสิ่งแรกที่เราจะมาดูกัน เพราะ Compare and Swap เป็นพื้นฐาน instruction หนึ่งที่ใช้เมื่อเราต้องการเปลี่ยนแปลงข้อมูลเวลาเราเขียนโปรแกรมแบบ Multithreading โดยทำเพื่อให้ instruction เป็นแบบ Atomic หรือเรียกว่าเล็กสุด ๆ

คือเราจะใช้การ compare data เปรียบเทียบก่อนที่จะบันทึกข้อมูลจริงๆ ซึ่งถ้า data ตรงกับที่คาดหวังไว้ก็จะ action ต่อ แต่ถ้า data ไม่ตรงกับที่คาดหวังก็จะไม่ทำต่อ เราสามารถเปรียบเทียบโดยใช้ data ที่อยู่ใน address กับ data เก่าที่เคยดึงมาเก็บในตัวแปร ถ้า data ตรงกันจะทำการ assign ค่าใหม่ทันที (swap) และทำการจบ function ถ้า swap สำเร็จจะ return true ถ้าไม่ก็จะ return false กลับไป

Atomic Variables

เวลาทำงานจริง ๆ เราไม่สามารถใช้ตัวแปร primitive ปกติได้ เนื่องจากมีการทำงานแบบ multithread ทำให้ตัวแปรสามารถถูก access ได้จากหลาย thread พร้อมกันจึงทำให้ข้อมูลมีโอกาสเกิดความผิดพลาดเพราะ Race condition ทำให้เราต้องใช้ตัวแปรแบบ Atomic ที่ได้ Implement เรื่อง Compare and Swap และ Atomic Operation มาให้แล้ว โดยใช้พื้นฐานเรื่องการ Lock ครับ

Compare and Swap with Atomic Variable

เดี๋ยวเราจำลอง Diagram ขึ้นมา เพื่อให้เห็นภาพมากขึ้น โดยให้ Thread 1 และ 2 ทำงานพร้อม ๆ กัน ซึ่งทั้ง 2 Thread พยายามแก้ Variable ตัวเดียวกัน และจะทำให้ process ถูก lock และค้างไป เมื่อ variables locked เป็น true และจะสามารถแก้ไข Variable ได้ก็ต่อเมื่อไม่โดน lock เท่านั้น

จากภาพจะเห็นว่า Thread 1 สามารถ Lock ตัว Atomic Variable ได้ก่อน ทำให้ Thread 2 ไม่สามารถ Lock ได้ เพราะต้องรอ Thread 1 unlock ก่อนถึงจะสามารถ Lock ตัว Atomic Variable ได้อีกครั้ง

ลองเล่นใน Java !

ผมจะเขียนโค้ดที่สร้าง Thread ขึ้นมา 2 ตัว โดย sleep ไว้ 1 วินาที และใช้ Library Atomic Variable ที่ java ให้มาเพื่อควบคุมตัวแปรให้ถูก Lock อย่างถูกต้องและเรียกใช้ method compareAndSet ที่ java Implement Compare and Swap ไว้ให้แล้วในการแก้ไขข้อมูลตัวแปรแบบ Atomic

เมื่อดูผลลัพธ์ที่ standard output พ่น Log ออกมา จะเห็นว่ามี Timeline เหมือนกับ Sequence Diagram ที่อธิบายไว้ตอนแรก คือ Thread 1 lock + unlock ก่อน และ Thread 2 ค่อยทำหลังจากที่ thread 1 unlock แล้ว

แต่ในความจริงสิ่งที่ต่างออกไปนิดหน่อยก็คือเวลา Run จริง ๆ ตัว Thread 2 อาจจะทำก่อนเสร็จก่อนก็ได้ครับเพราะทำงานพร้อม ๆ กันอยู่ที่ใครจะแย่งกันแล้วได้สิทธิ lock ไปก่อน จะทำให้ Thread นั้นทำงานเสร็จก่อน

ทั้งหมดนี้จะคล้าย ๆ กับการ Lock Transaction ใน Database เลยคือตัว Transaction จะถูก Lock จนกว่าจะ Commit Transaction คนอื่นจะไม่สามารถมา Query Record เดียวกันที่อัพเดทอยู่ได้ ผลเสียก็คืออาจทำให้เกิดการรอถ้า instruction ในการ lock ทำอะไรมากเกินไป

ทั้งหมดนี้เอาไปใช้ตอนไหน

Use case 1

ถ้าเรารวมเรื่อง CAS กับ Atomic Variables เราจะใช้สามารถทำ Atomic Operations ได้ โดยวิธีก็จะทำเหมือน Diagram Compare and Swap with Atomic Variable เลย แต่เป้าหมายของเรื่องนี้คือ เราต้องการให้ Operation Read, Modify และ Write โดยที่ไม่มีใครสามารถมารบกวนและมั่นใจได้ว่า Data ที่ได้หลังจาก Operation นี้ควรจะเป็นสิ่งที่บันทึกอย่างถูกต้อง โดยการ test-and-set หรือก็คือ compare and swap นั่นเอง ตัวอย่างเช่น

  • การตัดเงินผ่านบัตรเครดิตเมื่อซื้อสินค้ากับห้างสรรพสินค้า
  • การบันทึก Transaction ของการสั่งสินค้าใน Database จะต้องบันทึกข้อมูลสินค้าที่ไม่มีการแก้ไขใน order และคำนวนราคาถูกต้อง
  • การอัพเดทข้อมูลเกม offline ใน RAM เช่น ตัวละครอัพจากเลเวล 10 ไป 11
  • จะเป็นการนับสถิติบางอย่าง เช่น จำนวนผู้ติดโควิดทั้งหมดของประเทศไทยในวันนี้ เช่น จำนวนปัจจุบัน (คน) + 15000 (คน)

Use case 2

ถ้าเรารวมเรื่อง CAS กับ Non-blocking Transaction เข้าด้วยกัน
เราจะสามารถต่อยอดไปถึงเรื่อง Optimistic Locking (Opt Lock) ได้

ปัจจุบันถ้าเราใช้ Pessimistic Locking ของ Database ตระกูล RDBMS จะส่งผลให้ระบบเราช้าและอาจจะ Scale ได้ยากเพราะเป็นการ Lock ที่ระดับ record ซึ่งวิธีแก้ปัญหาก็คือย้ายมาทำ Opt Lock ในจุดที่ Critical ของระบบ หรือ เป็นจุดที่ระบบไม่ควร Block การทำงานของ User ซึ่งจะช่วยให้มุม UX ดีขึ้น และยังทำให้ Database ทำงานได้อย่างดีมากขึ้น

ซึ่งวิธีทำจะใช้วิธีว่า ให้มี column หรือ attribute หนึ่งใน table มีการเก็บ version ของ Record นั้นไว้ และเมื่อมีการแก้ไข Record นั้นให้ทำการปรับ Version เพิ่มขึ้น ทำให้เราไม่ต้อง Lock ที่ระดับ Record แล้ว

ซึ่งตอนเกิด Race Condition เราจะต้องทำการ Compare ตัว Version นี่แหละ ถ้า Version ตรงกันจะทำการอัพเดทข้อมูลใหม่ แต่ถ้าไม่ตรงก็จะไม่อัพเดทและทำการ Retry หรือทำซ้ำอีกรอบแทน

ตัวอย่างเช่น มีลูกค้ากำลังจะกดยืนยันการสั่งซื้อสินค้า แต่ร้านมีการอัพเดทข้อมูลราคาสินค้าก่อนที่ลูกค้าจะกดยืนยัน เราก็จะเทียบข้อมูลสินค้าในตระกร้าก่อนที่จะบันทึกออเดอร์นั้น เมื่อเทียบแล้วก็จะรู้ว่าเป็นข้อมูลคนละเวอร์ชั่นทำให้ลูกค้าอาจจะไม่สามารถสั่งสินค้าได้จนกว่าจะอัพเดทข้อมูลนั่นเอง

ซึ่งจาก FAQ ของ MongoDB จะมี WiredTiger ที่เป็น Storage Engine ของ MongoDB จะใช้เทคนิค Optimistic Locking ในการทำ Read/Write ด้วยนั่นเอง เพราะ MongoDB เก็บแบบ Document ไม่ใช่ Record ทำให้ไม่สามารถ Lock แบบปกติได้

สรุป

  • Compare and Swap ใช้เพื่อทำ Atomic Operation ในหลาย ๆ คอนเซปและเครื่องมือ
  • ปัจจุบันส่วนใหญ่ภาษา หรือ Product ที่เราใช้ เค้า Implement ให้อยู่แล้ว แต่บาง Product ที่ไม่มี เราสามารถ Implement ขึ้นมาเองโดยอิง Concept ได้ไม่ยาก
    (แต่ตอนทำความเข้าใจนี่ยากจัง 😂)
  • ในบางครั้งสามารถใช้ Library ของ Atomic Variable ได้เลย เช่น AtomicInteger ใน Java หรือ Atomic ใน Golang สิ่งที่เราต้องทำคือเข้าใจและใช้ให้ถูกนั่นเอง
  • ส่วนตัวคิดว่าเทคนิคนี้ไม่จำเป็นต้องเป็นเรื่อง technical เท่านั้น ถ้าเข้าใจมากพอ น่าจะเอาไป apply กับเรื่องอื่น ๆ ได้ เช่น การใช้ชีวิต เพราะคอนเซปมันคือการเปรียบเทียบให้ดีถูกต้องก่อนแล้วค่อยตัดสินใจว่าควรทำ หรือ action ต่อหรือไม่

--

--

Tae Keerati Jearjindarat

Hi, I'm Tae, Associate Engineering Manager at LINEMAN Wongnai. Thanks for following <3