ข้าม​ไป​ยัง​เนื้อหา

คุม​ด้วย lock แล้ว​จ่าย​ด้วย​การ​รอ

ก่อน​จะ​มี MVCC ปัญหา​นี้​มี​คำ​ตอบ​อยู่​แล้ว และ​คำ​ตอบ​นั้น​ตรง​กว่า​มาก คือ​ให้​ทุก​คน​ขอ​อนุญาต​ก่อน​แตะ​ข้อมูล

วิธี​นี้​ไม่ใช่​หุ่น​ฟาง​ที่​ยก​มา​เพื่อ​ล้ม มัน​ถูกต้อง​จริง และ​เอนจินที่​ใช้งาน​อยู่​ทุก​วัน​นี้​หลาย​ตัว ยัง​คุม​การ​เขียน​ด้วย​วิธี​นี้​อยู่

สิ่ง​ที่​มัน​เก็บ​เป็น​ค่า​ตอบแทน​คือ การ​รอ กับ การ​ที่​บาง​คน​ต้อง​ถูก​ยกเลิก​ทั้ง​ก้อน ทั้ง​สอง​อย่าง​นับ​ได้ และ​บท​นี้​จะ​นับ​ให้​ดู

📦 ทั้ง​บท​นี้​อยู่​ใน src/lock.rs ที่​เดียว

lock manager ของ​คอร์ส​นี้​ยาว​ไม่​ถึง​ร้อย​บรรทัด ไม่มี thread ไม่มี​การ​หน่วง​เวลา และ​ไม่​เรียก​นาฬิกา สัก​ครั้ง เทสต์ I7 กวาด​ซอร์ส​ทุก​ตัว​เพื่อ​บังคับ​ข้อ​นี้

ราคา​ที่​บท​นี้​รายงาน​จึง​เป็น​จำนวนนับ​สอง​ตัว คือ​จำนวน​ก้าว​ที่​ถูก​บล็อก กับ​จำนวน deadlock ไม่ใช่​เวลา​บน​นาฬิกา​ซึ่ง​เปลี่ยน​ไป​ตาม​เครื่อง​ที่​รัน

หัวข้อ​นี้​อธิบาย​กฎ​สอง​ช่วง​ของ 2PL และ​เหตุผล​ที่​การ​คืน lock ก่อน​ขอตัว​ถัด​ไป​ทำลาย​ความ​ถูกต้อง

Two-Phase LockingTwo-Phase Locking (2PL)วิธี​คุม​การ​เข้าถึง​ด้วย​การ​ขอ lock ให้​ครบ​ก่อน​ใน​ช่วง​แรก แล้ว​ค่อย​คืน​ทั้งหมด​ใน​ช่วง​หลัง ห้าม​สลับ​สอง​ช่วง​นี้ ได้​ความ​ถูกต้อง​มา​โดย​จ่าย​ด้วย​การ​รอ แบ่ง​อายุ​ของ transaction ออก​เป็น​สอง​ช่วง ช่วง​แรก​ขอ lock ได้​อย่าง​เดียว ช่วง​ที่​สอง​คืน​ได้​อย่าง​เดียว จุด​ที่​ขอ lock ตัว​สุดท้าย​คือ​เส้น​แบ่ง​ระหว่าง​สอง​ช่วง​นั้น

กฎ​มี​ข้อ​เดียว​คือ​ห้าม​คืน​แล้วกลับ​ไป​ขอ​ใหม่ และ​เหตุผล​ของ​กฎ​ข้อ​นี้​ไม่​ได้​อยู่​ที่​ความ​เป็น​ระเบียบ

สมมติ​ว่า t1 อ่าน order:1 ด้วย shared lock แล้ว​คืน​ทันที จาก​นั้น​ค่อย​ขอ lock ของ order:2 ระหว่าง​สอง​จังหวะ​นั้น t2 เข้า​มา​แก้ order:1 แล้ว commit ได้​เต็ม​ที่

ผล​ที่​ได้​คือ t1 อ่าน​ค่า​ก่อน​ที่ t2 จะ​แก้ แต่​เขียน​หลัง​จาก t2 เขียน​ไป​แล้ว มัน​จึง​อยู่​ทั้ง​ก่อน​และ​หลัง t2 พร้อม​กัน และ​ไม่มี​ลำดับ​เรียง​ที​ละ​ตัว​ลำดับ​ไหน​อธิบาย​ผล​นั้น​ได้

ตราบ​ใด​ที่​ทุก​คน​ไม่​คืน​ก่อน​ขอ​ครบ เส้น​แบ่ง​ของ​แต่ละ transaction เรียง​ลำดับ​กัน​ได้​เสมอ และ​ลำดับ​ของ​เส้น​แบ่ง​นั้น​เอง​คือ​ลำดับ​เรียง​ที​ละ​ตัว​ที่​อธิบาย​ผลลัพธ์​ได้ นี่​คือ​สิ่ง​ที่ 2PL ซื้อ​มา

src/lock.rs · ช่วง​ที่​สอง​เริ่มต้น​ที่​นี่
/// คืน lock ทั้งหมดของ transaction หนึ่ง — จุดเริ่มของช่วงที่สองใน 2PL
pub fn release_all(&mut self, xid: Xid) {
for held in self.holders.values_mut() {
held.retain(|(other, _)| *other != xid);
}
self.waits.remove(&xid);
self.waits.retain(|_, waiting_for| *waiting_for != xid);
}

เอนจิน​นี้​ไม่มี​ทาง​คืน lock ที​ละ​ตัว​เลย มี​แต่​คืน​ทั้งหมด​ที​เดียว รูปร่าง​ของ API จึง​บังคับ​กฎ​สอง​ช่วง ไว้​ใน​ตัว​มัน​เอง

การ​คืน​ที​เดียว​ทั้งหมด​ตอน​จบ​งาน​คือ​รูปแบบ​ที่​เข้ม​ที่สุด​ของ​กฎ​นี้ และ​มัน​มี​ของ​แถม​มา​ด้วย คือ​ไม่มี​ใคร​อ่าน​ค่าที่​ยัง​ไม่ commit ได้​เลย

ถ้า​คืน exclusive ก่อน​จบ​งาน คน​ที่​อ่าน​ต่อ​จาก​นั้น​จะ​อ่าน​ค่าที่​ยัง​ไม่​แน่นอน​ไป​ใช้ และ​ต้อง​ถูก​ยกเลิก ตาม​ไป​ทั้ง​แถว​เมื่อ​เจ้าของ​ค่า​ถูก​ยกเลิก

หัวข้อ​นี้​แสดง​สอง​โหมด​ของ lock สาม​ผลลัพธ์​ที่​การ​ขอ​คืน​กลับ​มา และ​กฎ​ความ​เข้า​กัน​ได้​ทั้งหมด​ของ​เอนจิน

src/lock.rs · สอง​โหมด กับ​สาม​ผลลัพธ์​ของ​การ​ขอ
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Mode {
Shared,
Exclusive,
}
#[derive(Debug, PartialEq, Eq)]
pub enum Outcome {
Granted,
/// ต้องรอ transaction ที่ระบุ
Blocked(Xid),
/// รอแล้วจะวนเป็นวง ต้องมีคนถูก abort
Deadlock(Xid),
}

Granted แปล​ว่า​ได้​ไป​เลย ส่วน​อีก​สอง​ตัว​พ่วง​หมายเลข transaction กลับ​มา​ด้วย เพราะ​คำ​ตอบ​ว่า​รอ ไม่มี​ประโยชน์​ถ้า​ไม่​บอกว่า​รอ​ใคร

กฎ​ที่​ตัดสิน​ว่า​ใคร​ต้อง​รอ​มี​อยู่​บรรทัด​เดียว​ใน​ทั้ง​เอนจิน

src/lock.rs · กฎ​ความ​เข้า​กัน​ได้​ทั้งหมด
/// lock สองตัวอยู่ร่วมกันได้ก็ต่อเมื่อทั้งคู่เป็น shared
fn compatible(existing: Mode, wanted: Mode) -> bool {
existing == Mode::Shared && wanted == Mode::Shared
}

อ่าน​ออก​มา​เป็น​ภาษา​พูด​ได้​ประโยค​เดียว lock สอง​ตัว​อยู่​ร่วม​กัน​ได้​ก็​ต่อ​เมื่อ​ทั้ง​คู่​เป็น shared นอก​นั้น​รอ​ทั้งหมด

ตัว​ที่​ถือ​อยู่ตัว​ที่​ขอผล
sharedsharedอยู่​ร่วม​กัน​ได้ ไม่มี​ใคร​รอ
sharedexclusiveต้อง​รอ
exclusivesharedต้อง​รอ
exclusiveexclusiveต้อง​รอ

ตอน​ที่ acquire มอง​หา​ตัว​ขวาง มัน​ข้าม​รายการ​ของ​ตัวเอง​ทิ้ง​เสมอ คน​ที่​ถือ shared อยู่​แล้ว จึง​ขอ​ยก​ระดับ​เป็น exclusive ได้ โดย​ไม่​ติด lock ของ​ตัวเอง

ข้อ​นี้​ฟัง​ดูเหมือน​ความ​สะดวก​เล็กๆ แต่​มัน​คือ​ปาก​ทาง​ของ​ทั้ง​บท

หัวข้อ​นี้​อ่าน​ผล​รัน​ที​ละ​บรรทัด และ​ชี้​ว่า​ตัวเลข​สอง​ตัว​ใน​บรรทัด​สรุป​คือ​ราคา​ของ​บท​นี้

cargo run --quiet -- 03
== บทที่ 3 คุมด้วย lock แล้วจ่ายด้วยการรอ ==
t1 ขอ shared order:1 Granted
t2 ขอ shared order:1 Granted
→ shared สองตัวอยู่ร่วมกันได้ ไม่มีใครต้องรอ
t1 ขอ exclusive order:1 Blocked(2)
t2 ขอ exclusive order:1 Deadlock(1)
ก้าวที่ถูกบล็อก 1 · deadlock 1
ทั้งคู่ถือ shared อยู่แล้วต่างขอยกระดับ จึงรอกันเป็นวง
ไม่มีใครถอยเองได้ ต้องมีคนถูก abort นี่คือราคาของ 2PL ที่นับได้
abort t1 แล้วคืน lock ทั้งหมดของมัน → t2 เดินต่อได้

สาม​บรรทัด​ถัด​จาก​หัวเรื่อง​คือ​กำไร​ของ shared mode ทั้ง t1 และ t2 ขอ​อ่าน​คีย์​เดียวกัน​พร้อม​กัน และ​ได้​ทั้ง​คู่ ไม่มี​ใคร​ต้อง​รอ​ใคร​สัก​ก้าว

บรรทัด​ถัด​มา t1 ขอ​ยก​ระดับ​เป็น exclusive แล้ว​ได้ Blocked(2) กลับ​มา มัน​ต้อง​รอ​ให้ t2 คืน shared ก่อน ซึ่ง​เป็น​คำ​ตอบ​ที่​ตรง​ไป​ตรง​มา

บรรทัด​ที่​ต้อง​หยุด​อ่าน​คือ​บรรทัด​ของ t2 มัน​ขอ​สิ่ง​เดียวกัน​แล้ว​ได้ Deadlock(1) นี่​คือ DeadlockDeadlockสภาพ​ที่ transaction สอง​ตัว​ต่าง​ถือ lock ที่​อีก​ฝั่ง​รอ​อยู่ จึง​ไม่มี​ใคร​เดิน​ต่อ​ได้ ต้อง​มี​คน​เลือก abort ตัว​ใด​ตัว​หนึ่ง​เพื่อ​ให้​ที่​เหลือ​ไป​ต่อ​ได้ สภาพ​ที่​ต่าง​ฝ่าย​ต่าง​ถือ​ของ​ที่​อีก​ฝ่าย​รอ​อยู่

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

หัวข้อ​นี้​อธิบาย​ว่า​ทำไม​ทางออก​เดียว​ของวง​นี้​คือ​การ​ยกเลิก​ใคร​สัก​คน​ทั้ง​ก้อน

t1 ถือ shared อยู่ และ​รอ​ให้ t2 คืน shared ส่วน t2 ก็​ถือ shared อยู่ และ​รอ​ให้ t1 คืน​เหมือน​กัน

ทางออก​ที่​ดู​สม​เหตุ​สม​ผล​ที่สุด​คือ​ให้​ใคร​สัก​คน​คืน shared ของ​ตัวเอง​ไป​ก่อน แล้ว​ค่อย​ขอ​ใหม่​ทีหลัง

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

ทางออก​ที่​เหลือ​จึง​มี​ทาง​เดียว คือ abortAbortการ​ที่​เอนจิน​ยกเลิก transaction ทิ้ง​ทั้ง​ก้อน​แล้ว​คืน​สภาพ​เหมือน​ไม่​เคย​เริ่ม เป็น​ราคา​ที่ isolation level สูง​เรียก​เก็บ ไม่ใช่​ความ​ผิดพลาด​ของ​โปรแกรม ตัว​ใด​ตัว​หนึ่ง​ทิ้ง​ทั้ง​ก้อน แล้ว​คืน lock ทุก​ตัว​ของ​มัน การ​คืน​ครั้ง​นั้น​ถูก​กติกา เพราะ transaction ตัว​นั้น​จบ​ไป​แล้ว ไม่มี​อะไร​จะ​ขอ​เพิ่ม​อีก

บรรทัด​สุดท้าย​ของ​ผล​รันทำ​แบบ​นั้น​พอดี เอนจิน​คืน lock ทั้งหมด​ของ t1 หลัง​จาก​นั้น​ไม่มี​ตัว​ขวาง​ของ t2 เหลือ​อยู่​บน​คีย์​นั้น​อีก

งาน​ที่ t1 ทำ​มา​ทั้งหมด​หาย​ไป​ด้วย และ​คน​ที่​ต้อง​เริ่ม​ใหม่​คือ​คน​เขียน​โปรแกรม ไม่ใช่​เอนจิน บท​ที่ 6 จะ​ได้​บิล​ใบ​เดียวกัน​นี้​อีก​ครั้ง​จาก​กลไก​คนละ​ตัว

flowchart LR
  S["ก้าวแรก · ทั้งคู่ขอ shared บน order 1<br/>Granted ทั้งคู่ ไม่มีใครรอ"]
  T1["t1 · ถือ shared อยู่<br/>ขอยกระดับเป็น exclusive"]
  T2["t2 · ถือ shared อยู่<br/>ขอยกระดับเป็น exclusive"]
  S --> T1
  S --> T2
  T1 ==>|รอ t2 คืน shared · Blocked| T2
  T2 -.->|รอ t1 คืน shared · การเดินสายเจอวง| T1

คำ​บรรยาย​ภาพ: ขอบ​สอง​เส้น​ชี้​กลับ​หา​กัน​จน​ครบ​วง เอนจิน​จึง​ตอบ Deadlock แทนที่​จะ​ให้ t2 รอ​เพิ่ม​อีก​ตัว

หัวข้อ​นี้​เดิน​ตาม​การ​ตรวจ​หา​วง​ที​ละ​ก้าว แล้ว​เทียบ​กับ​วิธี​ที่ PostgreSQL ใช้​จริง

src/lock.rs · ทุก​อย่าง​ที่ lock manager จำ​ไว้
#[derive(Debug, Default)]
pub struct LockManager {
holders: BTreeMap<String, Vec<(Xid, Mode)>>,
/// ใครรอใครอยู่ · หนึ่ง transaction รอได้ทีละหนึ่งตัวเท่านั้น
waits: BTreeMap<Xid, Xid>,
pub blocked_steps: u32,
pub deadlocks: u32,
}

waits คือ​ทะเบียน​ว่า​ใคร​รอ​ใคร​อยู่ และ​มัน​เก็บ​ได้​ตัว​ละ​หนึ่ง​รายการ​เท่านั้น สาย​การ​รอ​ใน​เอนจิน​นี้ จึง​เป็น​เส้นตรง​เสมอ ไม่ใช่​กราฟ​ที่​แตก​กิ่ง

src/lock.rs · เดิน​ตาม​สาย​การ​รอ​เพื่อ​หา​วง
/// เดินตามสายการรอจาก `from` ว่าไปจบที่ `target` ไหม — ถ้าใช่แปลว่าจะเกิดวง
fn would_cycle(&self, from: Xid, target: Xid) -> bool {
let mut at = from;
// จำนวนขอบมีจำกัด จึงวนได้ไม่เกินจำนวน transaction ที่รออยู่
for _ in 0..=self.waits.len() {
match self.waits.get(&at) {
Some(&next) if next == target => return true,
Some(&next) => at = next,
None => return false,
}
}
false
}

การ​เดิน​เริ่ม​จาก​ตัว​ที่​ขวาง​เรา​อยู่ แล้ว​ถาม​ทะเบียน​ต่อ​ไป​เรื่อยๆ ว่า​ตัว​นั้น​รอ​ใคร​อยู่​อีก​ทอด

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

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

ลอง​เดิน​ตาม​ผล​รัน​จริง ตอน t1 ขอ ทะเบียน​ยัง​ว่าง การ​เดิน​จาก t2 จึง​จบ​ทันที​เพราะ t2 ไม่​ได้​รอ​ใคร เอนจินบันทึก​ว่า t1 รอ t2 แล้ว​บวก​ก้าว​ที่​ถูก​บล็อก​ไป​หนึ่ง

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

PostgreSQL ทำ​สอง​อย่าง​นี้​ต่าง​จาก​เอนจินของ​เรา อย่าง​แรก​มัน​ไม่​ตรวจ​ทุก​ครั้ง​ที่​มี​คน​รอ มัน​รอ​ตาม​ค่า deadlock_timeout ก่อน​แล้ว​ค่อย​ตรวจ เพราะ​มัน​สมมติ​ว่า deadlock ไม่​ได้​เกิด​บ่อย

อย่าง​ที่​สอง เอกสาร​ของ​มัน​เขียนไว้ตรงๆ ว่า​มัน​จะ abort ตัว​ใด​ตัว​หนึ่ง​เอง และ​คาด​เดา​ไม่​ได้​ว่า​ตัว​ไหน โปรแกรม​ที่​เรียก​ใช้​จึง​ต้อง​พร้อม​รับ error นั้น​ทุก​ตัว ไม่ใช่​พร้อม​เฉพาะ​ตัว​ที่​คิด​ว่า​จะ​แพ้

หัวข้อ​นี้​สรุป​ว่า 2PL ซื้อ​อะไร​มา​ด้วย​อะไร และ​บอกว่า​บท​ถัด​ไป​เปลี่ยน​คำถาม​อย่างไร

2PL ให้​ผล​ที่​ถูกต้อง​จริง ไม่ใช่​ถูกต้อง​โดย​ประมาณ ถ้า​ทุก​คน​เดิน​ตาม​กฎ​สอง​ช่วง ผล​ที่​ได้​อธิบาย ด้วย​ลำดับ​เรียง​ที​ละ​ตัว​ได้​เสมอ

ราคา​ของ​มัน​มี​สอง​ก้อน​ที่​นับ​ได้ ก้อน​แรก​คือ​ก้าว​ที่​ถูก​บล็อก ซึ่ง​เป็น​งาน​ที่​หยุด​รอ​อยู่​เฉยๆ ก้อน​ที่​สอง​คือ abort ซึ่ง​เป็น​งาน​ที่​ทำ​ไป​แล้ว​และ​ถูก​โยน​ทิ้ง

ทั้ง​สอง​ก้อน​โต​ขึ้น​ตาม​จำนวน​คน​ที่​แย่ง​ของ​ชิ้น​เดียวกัน และ​ผู้​อ่าน​ซึ่ง​ไม่​ได้​แก้​อะไร​เลย​ก็​ต้อง​จ่าย​ด้วย เพราะ shared lock ของ​เขา​ยัง​ขวาง exclusive ของ​คน​อื่น​อยู่ดี

บท​ที่ 4 เปลี่ยน​คำถาม แทนที่​จะ​ถาม​ว่า​ใคร​มี​สิทธิ์​แตะ​ค่า​นี้ มัน​ถาม​ว่า​ใคร​ควร​เห็น​ค่า​ไหน ผู้​อ่าน​จึง​ไม่​ต้อง​ขอ​อนุญาต​ใคร​สัก​ครั้ง และ​ไม่มี​อะไร​ให้​รอ

MVCC ไม่​ได้​ทำให้​บิล​เป็น​ศูนย์ มัน​ย้าย​บิล​ไป​ไว้​ที่​อื่น ผู้​เขียน​สอง​คน​ที่​แย่ง​คีย์​เดียวกัน​ยัง​ต้อง​กัน​กันเอง เหมือน​เดิม และ​บท​ที่ 1 แสดง​ไป​แล้ว​ว่า​ของ​ที่​ไม่มี​ใคร​เห็น​ยัง​กิน​ที่​ใน​ที่​เก็บ​อยู่​จริง

บท​ที่ 5 จะ​เก็บ​บิล​ใบ​ที่​แพง​ที่สุด​ของ MVCC มา​วาง​บน​โต๊ะ คือ anomaly ที่ snapshot มอง​ไม่​เห็น​เลย และ​ไม่มี​ใคร​ถูก​บล็อก​หรือ​ถูก abort สัก​คน​ระหว่าง​ที่​มัน​เกิด


🔗 อ้างอิง​ต้นทาง​ของ​บท​นี้
  • PostgreSQL 18 Documentation — 13.3. Explicit Locking (ตรวจ​แล้ว 2026-08-13) — ตาราง​โหมด lock ของ​จริง​ที่​มี​มากกว่า​สอง​โหมด และ​หัวข้อ Deadlocks ที่​ระบุ​ว่า PostgreSQL ตรวจ​พบ​สภาพ​นี้​เอง​แล้ว abort ตัว​ใด​ตัว​หนึ่ง โดย​คาด​เดา​ไม่​ได้​ว่า​ตัว​ไหน
  • PostgreSQL 18 Documentation — 19.12. Lock Management (ตรวจ​แล้ว 2026-08-13) — deadlock_timeout และ​เหตุผล​ที่​มัน​มี​อยู่ คือ​รอ​ไว้​ก่อน​แล้ว​ค่อย​ตรวจ เพราะ​สมมติ​ว่า deadlock ไม่​ได้​เกิด​บ่อย​ใน​ระบบ​จริง

เช็กความเข้าใจ — บทที่ 3

ข้อ 1 / 3

2PL ห้ามคืน lock ตัวหนึ่งแล้วกลับไปขอ lock ตัวถัดไป ข้อใดอธิบายเหตุผลของข้อห้ามนี้ได้ถูกต้อง