ตอน 34 ความสัมพันธ์ของ Object lifetime, Garbage collector และ Marshalling

ไปหน้าแรก | สารบัญ | Laploy.comระเบียนบทความ | บทความจากลาภลอย

เว็บไซต์นี้เป็นตัวอย่างเนื้อหาบางตอนในหนังสือ "เรียนรู้ด้วยตนเอง OOP C# ASP.NET" ครอบคลุม บทที่ 1 ถึงบทที่ 6 (ในหนังสือมี 21 บท) เนื้อหาใน Blog อาจอาจแตกต่างจากในหนังสือเพราะเป็นเนื้อหาที่ยังไม่ได้ตรวจแก้ขัดเกลา (edit)

กดที่นี่เพื่อดูรายละเอียดเนื้อหาในแต่ละบท

กดที่นี่เพื่อไปยังเว็บบอร์ด ถาม-ตอบ 

 

 

ความสัมพันธ์ของ Object lifetime, Garbage collector และ Marshalling

 

ท่านผู้อ่านชื่อคุณ myme เขียนมาในเว็บบอร์ด (www.aicybernetic.com/gbook) บอกเพียงสั้นๆ ว่า “อยากทราบการทำงาน ความสัมพันธ์ของ object lifetime, garbage collector, marshalling ค่ะ” ผู้เขียนตัดสินใจนำมาเขียนรับใช้เป็นบทความใน blog นี้ (แทนที่จะตอบในเว็บบอร์ด) เผื่อจะเป็นประโยชน์แก่ผู้ค้นหาข้อมูลเดียวกันนี้ท่านอื่นๆ

เนื้อหาในบทนี้ท่านจะได้เรียนเรื่องเกี่ยวกับ object lifetime หรือช่วงชีวิตของ object ซึ่งเป็นเรื่องเกี่ยวกับการเกิดและการดับของ object วิธีทำลาย object ของโปรแกรม Garbage collector (GC) ว่ามีคติอย่างไรในการตัดสินใจว่า object ใดควรอยู่หรือควรถูกทำลาย และ COM object มีช่วงชีวิตอย่างไร

ผู้เขียนจะเริ่มโดยอธิบายความหมาย (โดยสังเขป) ของคำสามคำในหัวเรื่อง คือคำว่า Object lifetime, Garbage collector และ Marshalling ก่อนจะเข้าเรื่องความสัมพันธ์ของสามสิ่งนี้ต่อไปในตอนท้าย

 

Object lifetime

Object lifetime คือช่วงชีวิตของ object ระยะเวลาเริ่มจาก object ถูกสร้างขึ้น คงอยู่ ถูกใช้งาน หมดหน้าที่การทำงานและถูกทำลาย คือช่วงชีวิตของ object

Object คือสิ่งที่เราสร้างจากคลาส เมื่อเราสร้าง object โปรแกรม CLR จะจัดสรรพื้นที่หน่วยความจำใน “ฮีพ” (หมายถึง managed heap ของ .NET) เพื่อทำหน้าที่เก็บ object และข้อมูลของ object (สมาชิกแบบ field) object จะคงอยู่ใน heap จนกระทั่งโปรแกรม GC ตรวจพบว่า object จะไม่ถูกใช้งานอีกแล้ว มันจะตัดสินใจทำลาย object นั้น โดยกำหนดให้พื้นที่ใน heap บริเวณที่เคยใช้เก็บ object นั้นกลายเป็นที่ว่าง

 

Garbage collector

Garbage collector คือโปรแกรมทำหน้าที่จัดการหน่วยความจำโดยอัตโนมัติ ภาษาแบบ OOP ส่วนมากเช่นภาษา Lisp, Smalltalk, Java และ C# ล้วนมี Garbage collector ในที่นี้ผู้เขียนจะพูดถึงเฉพาะ Garbage collector ซึ่งเป็นส่วนหนึ่งของบริการต่างๆ ใน .NET Framework

GC ตรวจสอบว่า object หมดอายุและสมควรถูกทำลายแล้วหรือไม่โดยใช้กระบวนการหรือ algorithm ที่แน่นอน ซึ่งจะอธิบายต่อไปภายหลัง

 

Marshalling

คำว่า Marshall ใน .NET และภาษา C# มีความหมายสองอย่างคือ

  1. การแปลง object ให้อยู่ในรูปของ byte ที่เรียงกัน ปรกติจะเป็นข้อมูลแบบ text ในรูปแบบ XML เพื่อการรับ-ส่งตัว object และข้อมูล ไปยังระบบอื่น เช่นบันทึกเป็นไฟล์ลงในดิสก์ หรือส่งไปยังคอมพิวเตอร์เครื่องอื่นในเครือข่าย
  2. การแปลง COM object (โค้ด native ในสภาพของ DLL แบบ WinAPI/Win32) ให้เป็น object แบบ .NET การแปลงทำได้โดยใช้บริการต่างๆ ที่ .NET เตรียมไว้ให้คือ Platform Invocation Services ที่มักเรียกย่อว่า P/Invoke

บทความนี้จะอธิบาย Marshalling ในความหมายที่สอง โดยจะพูดถึงความสัมพันธ์ของ P/Invoke กับ Garbage collector เป็นหลัก

 

การสร้างและทำลาย object

การจะเข้าใจความสัมพันธ์ของ P/Invoke กับ Garbage collector ได้จะต้องเข้าใจพื้นฐานหลักการทำงานของ Garbage collector ดังนั้นในหัวข้อนี้ผู้เขียนจะอธิบายวิธีที่ CLR สร้าง object และวิธีที่ GC ทำลาย object โดยสังเขป

ในสภาพแวดล้อมที่ไม่ถูกจัดการโดย GC ยกตัวอย่างเช่นใช้ภาษา C++ แบบ WinAPI เมื่อเราสร้างและใช้งาน object จะเกิดกระบวนการต่างๆ ต่อไปนี้

1. C runtime จองพื้นที่ในหน่วยความจำเพื่อเก็บตัว object และข้อมูลต่างๆ ของ object
2. โค้ดของเรากำหนดค่าเริ่มต้นต่างๆ เพื่อให้ object ทำงานได้
3. โค้ดของเราใช้งาน object โดยเรียกใช้สมาชิกต่างๆ ของ object
4. โค้ดของเราล้างค่าอ้างอิงต่างๆ เพื่อทำลาย object
5. โค้ดของเราแจ้งให้ C runtime ปลดปล่อยหน่วยความจำ

ขั้นตอนทั้งห้าเป็นสิ่งที่เกิดขั้นตลอดเวลาเมื่อโปรแกรมทำงาน ปัญหาหลักที่อาจเกิดขึ้นกับกระบวนการนี้มีสองอย่างคือ

1. โปรแกรมของเราลืมเรียกโค้ดส่วนล้างค่าอ้างอิงและปลดปล่อยหน่วยความจำ
2. โปรแกรมของเราจองพื้นที่ทับพื้นที่ในหน่วยความจำที่ไม่ว่าง (กำลังถูกใช้โดยโปรแกรมอื่น)

ผลลัพธ์ที่เกิดจากปัญหาสองประการนี้ทำนายได้ยาก ผู้สร้าง.NET Framework จึงคิดป้องกันปัญหาทั้งสอง โดยให้ runtime (Common Language Runtime หรือ CLR) จัดการขั้นตอนทั้งหมดให้เราดังนี้

1. CLR จองพื้นที่ในหน่วยความจำเป็นพื้นที่ขนาดใหญ่เรียกว่า heap
2. CLR กำหนดพื้นที่ใน heap ให้แก่ object
3. โปรแกรมของเราใช้งาน object โดยเรียกใช้สมาชิกต่างๆ ของ object
4. GC ล้างค่าอ้างอิงต่างๆ ของ object เพื่อยกเลิกการใช้งาน object
5. GC ปลดปล่อย heap ส่วนที่เคยเก็บ object

จะเห็นว่าขั้นตอนเป็นอัตโนมัติเกือบทั้งหมด เราเพียงเรียกใช้งาน object (ในขั้นตอน 3) โดยไม่ต้องกังวลเรื่องการจองหรือการปลดปล่อยหน่วยความจำ

 

การจัดสรรหน่วยความจำของ .NET CLR

เมื่อโปรแกรมของเราเริ่มทำงาน CLR จะจองพื้นที่ในหน่วยความจำไว้ส่วนหนึ่ง เป็นพื้นที่ติดต่อกันทั้งหมด เรียกว่า heap หรือ managed heap เพื่อใช้เป็นที่เก็บ object (โปรดดูรายละเอียดเรื่อง heap ในบทที่ 2) CLR จะสร้าง pointer ตัวหนึ่งชื่อ NextObjPrt ทำหน้าที่ระบุตำแหน่งในหน่วยความจำที่จะใช้เก็บ object ตัวใหม่ โดยในตอนเริ่มงานจะกำหนดให้ NextObjPrt มีค่าเท่ากับตำแหน่งล่างสุดของ heap

ยกตัวอย่างการทำงานของ CLR เป็นดังนี้ เมื่อ CLR พบว่าโปรแกรมของเราต้องการสร้าง object A มันจะจองที่ใน heap ให้แก่ object A แล้วเพิ่มค่า NextObjPrt ไปยังตำแหน่งถัดไป หากเราสร้าง object อีกสองตัว คือ B และ C ผลลัพธ์จะเป็นอย่างที่เห็นในภาพ

 

เมื่อจัดสรรหน่วยความใน heap ให้ object แล้ว CLR จะเพิ่มค่า pointer ชื่อ NextObjPrt ให้ชี้ไปยังตำแหน่งถัดไปจาก object ตัวสุดท้าย หาก CLR ต้องการสร้าง object ใหม่ แต่ heap เต็ม จะเกิด error แบบ OutOfMemoryException เมื่อทำคำสั่ง new

 

Root และ Garbage collection

GC จะตรวจสอบว่ามี object ใดใน heap ที่ไม่ถูกโปรแกรมเรียกใช้อีกต่อไปแล้วหรือไม่ (เป็นขยะ) หากมีมันจะลบ object นั้นออกจาก heap วิธีการตรวจสอบว่า object เป็นขยะหรือไม่ ทำได้โดยการตรวจสอบ root

โปรแกรมทุกโปรแกรมจะมีสิ่งที่เรียกว่า root ซึ่งประกอบขึ้นจากสิ่งต่างๆ ดังนี้

1. pointer ที่ชี้ไปยัง object แบบ static หรือ global
2. pointer ที่ชี้ไปยังตัวแปรท้องถิ่นใน stack ของ thread นั้นๆ
3. pointer ที่ชี้ไปยังพารามิเตอร์ใน stack ของ thread นั้นๆ
4. รีจิสเตอร์ของซีพียูซึ่งเก็บ pointer ที่ชี้ไปยัง object ใน heap

ผู้ทำหน้าที่ดูแลรักษาสถานะของ root คือ CLR และ JIT (Just-in-time compiler) โดยทั้งสองจะยินยอมให้ GC ตรวจสอบ root ของโปรแกรมใดๆ ก็ได้

เมื่อ GC เริ่มทำงานมันจะอนุมานว่า object ทุกตัวใน heap เป็นขยะหรือพูดอีกอย่างคือ GC จะอนุมานว่าไม่มี root ของโปรแกรมใดๆ อ้างไปยัง object ใน heap เลย จากนั้น GC จะเริ่มท่องไปใน root แล้วทำแผนที่แสดงความสัมพันธ์ของ object ต่างๆ ใน heap

 

ภาพนี้แสดง heap เมื่อมี object เกิดขึ้นจำนวนมาก แต่ root อ้างอิงโดยตรงไปยัง object เพียงสี่ตัวคือ A, C, D และ F เมื่อ GC ท่องไปใน root แล้วทำแผนที่แสดงความสัมพันธ์ของ object ต่างๆ ใน heap มันพบว่า object C อ้างถึง object H ด้วย GC จึงนำ H มารวมไว้ในแผ่นที่ด้วยเช่นกัน และ GC จะท่องไปใน heap เพื่อตรวจปรับปรุงแผนที่อยู่เสมอ

 

เมื่อ GC ตรวจสอบ root ครบแล้ว GC จะรู้ว่ามี object ใดบ้างที่ถูกอ้างถึงโดย root และ object อื่นๆ และมี object ใดบ้างที่ไม่ถูกอ้างถึงเลย ซึ่ง object เหล่านั้นจะถูกพิจารณาว่าเป็นขยะ ยกตัวอย่างในภาพ object B, E, G, I และ J ถูกมองว่าเป็นขยะ ดังนั้นพื้นที่ใน heap บริเวณ B, E, G, I และ J จึงถูกมองว่าเป็นที่ว่าง

GC จะขจัดที่ว่างใน heap โดยขยับ object H ลงมาติดกับ object D เพื่อให้ object ที่ดีทั้งหมด อยู่ในพื้นที่ๆ ต่อเนื่องกัน ผลลัพธ์ที่ได้จะเป็นดังภาพนี้

 

สภาพของ heap หลัง GC นำขยะออกไปแล้ว NextObjPrt จะถูกปรับตำแหน่งใหม่ให้ถูกต้อง เมื่อถึงตอนนี้หากโปรแกรม run คำสั่ง new อีกครั้ง object จะถูกสร้างขึ้นได้โดยไม่มี error แบบ OutOfMemoryException

 

ที่อธิบายมานี้เป็นหลักการทำงานของ GC อย่างง่าย อันที่จริง GC จะไม่ทำลาย object ทันทีที่กลายเป็นขยะ แต่จะเก็บไว้ก่อน และจัดลำดับไว้เป็นรุ่นๆ (generation) object บางตัวที่ตายแล้วอาจคืนพื้นชีพขึ้นได้ (ด้วยกระบวนการ resurrection ในกรณีมีการทำ finalization) โปรดดูรายละเอียดเรื่องการลำดับรุ่นของ GC ในบทที่ 2 หัวข้อ Garbage collector

 

Runtime-Callable Wrappers

หากต้องการเรียกใช้ method ในไฟล์ .DLL แบบ Win32 ทำได้โดย copy ไฟล์ .DLL ตัวที่ต้องการไปใส่ในโฟลเดอร์ bin แล้วเขียนโค้ดเรียกใช้ดังนี้

using System;
using System.Collections.Generic;
using System.Text;
using System.Runtime.InteropServices;

namespace testInput
{
    class PortAccess
    {
        [DllImport("inpout32.dll", EntryPoint = "Out32")]
        public static extern void PortOutput(int adress, int value);
        [DllImport("inpout32.dll", EntryPoint = "Inp32")]
        public static extern int PortInPut(int address);
        int portAddress;

        public PortAccess()
        {
            portAddress = 0xF300;
        }

        public void OutPut(int value)
        {
            PortOutput(portAddress, value);
        }
        public int InPut()
        {
            return PortInPut(portAddress);
        }
    }
} 

โค้ดข้างบนแสดงวิธีเรียกใช้ method Out32 และ Inp32 จากไฟล์ inpout32.dll ที่ผู้เขียนมักใช้เพื่อรับ-ส่งข้อมูลทางพอร์ทขนาน (printer port) เมื่อต้องการเชื่อมต่อกับอุปกรณ์ฮาร์ดแวร์ต่างๆ

ตัวอย่างข้างบนเป็นการทำ DLLImport ในกรณีที่ต้องการนำ COM object มาทำเป็น object ใน managed code ก็ทำได้โดยใช้กลไกที่เรียกว่า Interoperability (หรือมักเรียกย่อว่า interop) ซึ่งทำได้ง่ายมากด้วยการ add reference (ใน Visual Studio .NET เลือกเมนู Project/Add Reference เลือก tab COM แล้วเลือก COM object ที่แสดงในรายการ)

เมื่อ add reference แล้วเราสามารถใช้งาน COM object ได้เหมือน managed object เพราะผู้สร้าง .NET ได้จัดเตรียมกลไกเรียกว่า Runtime-Callable Wrappers (RCW) เพื่อให้เราสามารถใช้งาน COM component ได้ใกล้เคียง .NET object มาก โดย RCW จะจัดการด้านต่างๆ ต่อไปนี้ให้โดยอัตโนมัติ

1. Marshall ข้อมูลและ method ระหว่าง .NET กับ COM
2. เป็น proxy กับอินเตอร์เฟสของ COM
3. จัดการ identity (เหมือน root) ของ object
4. จัดการ interface มาตรฐานของ COM (เช่น IUnkonwn และ IDispatch)
5. ดูแลเมื่อเกิด error และจัดให้มี exception handling
6. จัดการกับ object lifetime

ในบทนี้เราจะพิจารณาเฉพาะหัวข้อสุดท้ายคือ object lifetime หรือช่วงชีวิตของ object

 

RCW ทำหน้าที่กันเราไว้จากความยุ่งยากในการเชื่อมต่อกับ COM object โดยมันจะ consume interface ต่างๆ ของ COM object และเผยให้เราเห็นเพียงบางตัว ในภาพนี้ RWC consume interface สามตัวคือ IUnkonw, IDispatch และ INew แต่เผยให้เราเข้าถึงได้เฉพาะ INew หลักการนี้มีชื่อเรียกว่า Marshaling selected interfaces

 

ยกตัวอย่างเช่นหากเรา add reference คลาสไลบรารีของ Microsoft Word (ซึ่งทำหน้าที่อ่านและเขียนเอกสารแบบ .doc) แล้ว เราสามารถสร้าง object ได้ด้วยคำสั่งดังนี้

Word.Application myWord = new Word.ApplicationClass();

 

Object lifetime

ในหัวข้อที่ผ่านมาผู้เขียนพูดถึงการสร้างและทำลาย object แบบ .NET ซึ่งเป็น object ที่เกิดจากคลาสที่เราเขียนในภาษา C# แต่ในกรณี COM object โปรแกรม GC จะจัดการกับมันอย่างไร

ในภาษา C++ เราสามารถควบคุม object lifetime ได้ตามใจชอบโดยใช้ตัวกระทำ new เพื่อสร้าง object และ delete เพื่อทำลาย object แต่ COM object เราทำเช่นนั้นไม่ได้ เพราะ object ตัวเดียวอาจถูกใช้งานได้จากหลายๆ โปรแกรม หากโปรแกรมตัวใดตัวหนึ่งทำลาย object โปรแกรมที่เหลือย่อมจะทำงานล้มเหลว COM object จึงใช้หลักการ reference count เพื่อควบคุม object lifetime

หลักการทำงานของ reference count (RC) คือเมื่อ client ต้องการใช้งาน object มันจะต้องเรียก interface IUnknown::AddRef ซึ่งจะเพิ่มค่าของ RC ขึ้นหนึ่ง เมื่อ client ใช้งาน object เสร็จแล้วมันจะต้องเรียก interface IUnknown::Release ซึ่งจะทำหน้าที่ลดค่า RC ลงหนึ่ง เมื่อค่าของ RC ลดลงเท่ากับศูนย์แสดงว่าไม่มีโปรแกรมใดต้องการใช้งาน object อีกต่อไป runtime จึงรู้ว่าสามารถทำลาย object นั้นได้

เมื่อเรานำ COM object มาใช้ในสภาพแวดล้อม .NET managed โดยใช้วิธี add reference และสร้าง object เป็น RCW โปรแกรม CLR จะรักษาค่าของ RC ไว้ให้เท่ากับหนึ่งเสมอ (ยกเว้น object ถูก marshal ไปยัง AppDomain อื่น CLR จะเพิ่มค่า RC ขึ้นหนึ่ง) ทำให้ COM object ไม่ทำลายตัวเองก่อนจะถูกทำลายโดย GC

เนื่องจาก RCW เป็นคลาสใน .NET Framework ดังนั้น object ที่เกิดจาก RCW จึงเป็น managed object โปรแกรม GC สามารถจัดการกับ object แบบ RCW ได้เหมือน object อื่นๆ ทำให้ lifetime ของ COM object กับ .NET managed object ไม่แตกต่างกัน

หากต้องการให้ RCW ถูกกำจัดทันทีก็สามารถทำได้ตามขั้นตอนต่อไปนี้

  1. กำหนดให้ตัวแปรที่อ้างอิง RCW มีค่าเป็น null
  2. เรียก method System.GC.Collect เพื่อบังคับให้ GC ทำงาน
  3. เรียก method System.GC.WaitForPendingFinalizers เพื่อหยุดการทำของ thread ไว้จนกว่า thread ที่กำลังดำเนินการกับคิว finalizer จะล้างคิวหมด
  4. เรียก method Marshal.ReleaseComObject เพื่อลดค่าของ RC ลงหนึ่ง

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

 

สรุปเรื่อง ความสัมพันธ์ของ Object lifetime, Garbage collector และ Marshalling

• Object lifetime คือช่วงเวลาตั้งแต่ object ถูกสร้างถึง object ถูกทำลาย
• Garbage collector คือบริการใน CLR ทำหน้าที่กำจัด object ซึ่งหมดประโยชน์แล้ว
• Marshalling คือการรับ-ส่งข้อมูลและ method ระหว่าง unmanaged และ managed
• GC เก็บ object ใน heap และใช้ pointer NextObjPrt เก็บสถานะของ heap
• GC พิจารณาว่า object เป็นขยะโดยใช้ root เป็นตัวอ้างอิง
• การนำ COM object มาทำเป็น object แบบ managed ทำได้โดยใช้ RCW
• RCW มี object lifetime เหมือน managed object ทั่วไป

ตอนต่อไป : กายวิภาคของของ ASP.NET

Post a comment or leave a trackback: Trackback URL.

ความเห็น

  • Komonnarat  On มีนาคม 15, 2007 at 11:28 am

    หนังสื่อมีขายที่ ใหน หรอ ครับ ผม ว่าน่าสนใจจัง คิคิ

  • Nine  On กรกฎาคม 27, 2007 at 3:39 pm

    ขอบคุณสำหรับความรู้ครับ ผมจะก๊อบทั้งหมมดเลย อิอิ

ใส่ความเห็น

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / เปลี่ยนแปลง )

Twitter picture

You are commenting using your Twitter account. Log Out / เปลี่ยนแปลง )

Facebook photo

You are commenting using your Facebook account. Log Out / เปลี่ยนแปลง )

Google+ photo

You are commenting using your Google+ account. Log Out / เปลี่ยนแปลง )

Connecting to %s

%d bloggers like this: