Thread ในการพัฒนา Mobile — คืออะไร ประเภท และการจัดการเธรด

ผู้แต่ง: IT Sectr เผยแพร่เมื่อ: 2026-03-16 เวลาอ่าน: 11 นาที

Thread — หน่วยพื้นฐานของเวลาโปรเซสเซอร์ ที่มีสแต็คของตัวเองและทำงานอย่างอิสระจากเธรดอื่น ในการพัฒนามือถือ เธรดถูกใช้สำหรับการทำงานแบบขนานของงานต่าง ๆ เพื่อให้อินเทอร์เฟซยังคงตอบสนองได้ดีระหว่างการดำเนินการที่ใช้เวลานาน Android รองรับ java.lang.Thread, Executors และ Kotlin Coroutines, iOS รองรับ Thread (Objective-C), GCD และ OperationQueue ตาม เอกสาร Android Thread การสร้างเธรดแบบเนทีฟต้องการการจัดสรรสแต็คประมาณ 1 MB โดยระบบปฏิบัติการ

ประเด็นสำคัญ

  • Thread — หน่วยการจัดตารางเวลา CPU ที่เล็กที่สุด: แต่ละเธรดเป็นอิสระและมีสแต็คของตัวเอง
  • การสร้างเธรด ต้องการ ~1 MB สแต็คบน Android และ 512 KB บน iOS ดังนั้นพูลจึงมีประสิทธิภาพมากกว่าการสร้างโดยตรง
  • Android: Thread, Executors, HandlerThread, Coroutines — สี่ระดับของนามธรรมเธรด
  • iOS: Thread (ระดับต่ำ), GCD (DispatchQueue), OperationQueue (ระดับสูง)
  • Thread safety — การเข้าถึงข้อมูล mutable ร่วมกันต้องการการซิงโครไนซ์: locks, atomic, serial queues

Thread คืออะไร

Thread (เธรดการทำงาน) — คือลำดับคำสั่งอิสระที่ระบบปฏิบัติการสามารถจัดตารางเวลาบนแกน CPU แต่ละกระบวนการ (แอปพลิเคชัน) มีอย่างน้อยหนึ่งเธรด — Main Thread เธรดเพิ่มเติมถูกสร้างขึ้นสำหรับการทำงานแบบขนานของงาน แต่ละเธรดมีสแต็คซอฟต์แวร์ของตัวเอง (พร้อมตัวแปรท้องถิ่น), ตัวนับคำสั่ง (PC) และรีจิสเตอร์ หน่วยความจำฮีป (heap) ถูกแชร์ระหว่างเธรดทั้งหมดของกระบวนการ

ในระบบปฏิบัติการมือถือ เธรดถูกจัดตารางเวลาด้วยการทำงานหลายอย่างแบบยึด抢先 (preemptive multitasking): ระบบปฏิบัติการสามารถขัดจังหวะการทำงานของเธรดได้ทุกเมื่อและโอนการควบคุมไปยังเธรดอื่น (context switch) การสลับบริบทเป็นการดำเนินการที่แพง (1-10 ไมโครวินาที) เนื่องจากต้องบันทึก/กู้คืนรีจิสเตอร์ CPU อัปเดต TLB และล้างแคช ดังนั้นจำนวนเธรดที่มากเกินไป (ร้อยหรือพัน) ทำให้ประสิทธิภาพลดลง — ระบบปฏิบัติการใช้เวลาในการสลับมากกว่าการทำงาน

เธรดและกระบวนการ — เป็นแนวคิดที่แตกต่างกัน กระบวนการคืออินสแตนซ์ของแอปพลิเคชันที่มีหน่วยความจำเสมือนเฉพาะ เธรดภายในกระบวนการแชร์หน่วยความจำนี้กับเธรดอื่น ใน Android แต่ละคอมโพเนนต์ของแอปพลิเคชัน (Activity, Service, BroadcastReceiver) ทำงานในกระบวนการเดียว แต่สามารถดำเนินการบนเธรดที่แตกต่างกัน แอปพลิเคชัน iOS ก็เป็นกระบวนการเดียวที่มีความสามารถในการสร้างเธรดเพิ่มเติมผ่าน GCD หรือ Thread

วงจรชีวิตของเธรด: สถานะและการเปลี่ยนผ่าน

แต่ละเธรด ใน Java/Kotlin (Android) และ NSThread (iOS) ผ่านห้าสถานะ: New (สร้าง), Runnable (พร้อมทำงาน), Running (กำลังทำงานบน CPU), Blocked/Waiting (รอทรัพยากรหรือการแจ้งเตือน), Terminated (สิ้นสุด) การเปลี่ยนผ่านระหว่างสถานะถูกจัดการโดยตัวจัดตารางเวลาของระบบปฏิบัติการและไพรมิทีฟการซิงโครไนซ์ นักพัฒนาสามารถมีอิทธิพลต่อลำดับความสำคัญของเธรด (Thread.setPriority()) และสถานะของมัน (sleep, join, interrupt)

ใน Android เธรดเปลี่ยนไปยังสถานะ Blocked เมื่อพยายามยึดมอนิเตอร์ที่ถูกใช้งาน (synchronized), เรียก Object.wait() หรือ Thread.sleep() ใน iOS — เมื่อเรียก NSCondition.wait(), pthread_cond_wait() หรือ dispatch_semaphore_wait() ในสถานะ Blocked เธรดไม่กิน CPU แต่กินหน่วยความจำ (สแต็ค) เธรดสามารถถูกขัดจังหวะ (interrupted) จากเธรดอื่น โดยได้รับ InterruptedException (Java) หรือตรวจสอบ isCancelled (Kotlin Coroutines)

สถานะคำอธิบายวิธีการเปลี่ยนผ่าน
Newเธรดถูกสร้างแต่ยังไม่ได้เริ่มคอนสตรัคเตอร์ Thread()
Runnableเธรดพร้อมทำงาน รอ CPUthread.start()
Runningเธรดกำลังทำงานบนแกน CPUตัวจัดตารางเวลาของ OS
Blocked/Waitingเธรดรอทรัพยากร มอนิเตอร์ หรือการแจ้งเตือนsynchronized, wait(), sleep()
Terminatedเธรดทำงาน run() เสร็จหรือถูกขัดจังหวะrun() เสร็จ, interrupt()

Context Switch และต้นทุนของมัน

Context switch (การสลับบริบท) — การดำเนินการที่ระบบปฏิบัติการบันทึกสถานะของเธรดปัจจุบัน (รีจิสเตอร์, PC, TLB) และโหลดสถานะที่บันทึกไว้ของเธรดอื่น ในระบบมือถือ (Linux + ART, XNU สำหรับ iOS) context switch ใช้เวลา 1-10 ไมโครวินาที ถ้าเธรดทำงานเสร็จใน 100 ไมโครวินาทีและ context switch ใช้ 5 ไมโครวินาที แสดงว่าเสียเวลา 5% เพื่อลด context switch iOS ใช้ GCD แบบ work stealing, Android ใช้พูลแบบ fixedThreadCount

Thread ใน Android: จาก Thread สู่ Coroutines

Android ได้วิวัฒนาการจาก java.lang.Thread ระดับต่ำสู่ coroutines สมัยใหม่ แต่ละระดับของนามธรรมให้ความสามารถมากขึ้นด้วยค่าใช้จ่ายที่น้อยลง Thread เป็นคลาสพื้นฐาน แต่การสร้างโดยตรงไม่แนะนำ: เธรดใหม่ไม่ถูกจัดการโดยพูล ยากต่อการตรวจสอบและยกเลิก AsyncTask (เลิกใช้งานตั้งแต่ API 30) เป็นก้าวไปข้างหน้า แต่ประสบปัญหาการรั่วไหลของหน่วยความจำและการจัดการคอนฟิกูเรชันที่ไม่สะดวก

HandlerThread เป็นคลาสย่อยพิเศษของ Thread ที่มี Looper ซึ่งสามารถประมวลผลคิวข้อความ ถูกใช้สำหรับการทำงานตามลำดับของงานบนเธรดพื้นหลัง เช่น การเขียนข้อมูลลงใน Room หรือไฟล์ HandlerThread ถูกสร้างโดยเรียก start() จากนั้นผ่าน Handler(handlerThread.looper) สามารถส่งข้อความและ Runnable การเรียก handlerThread.quit() หยุด Looper และสิ้นสุดเธรด

kotlin
// Android: Thread, HandlerThread และ Executors
import android.os.Handler
import android.os.HandlerThread
import java.util.concurrent.Executors

class ThreadExample {

    // 1. การสร้าง Thread โดยตรง (ไม่แนะนำ)
    fun directThread() {
        val thread = Thread(Runnable {
            Thread.sleep(1000)
            print("Direct thread executed")
        })
        thread.start()
    }

    // 2. HandlerThread สำหรับงานพื้นหลังแบบลำดับ
    fun handlerThreadExample() {
        val handlerThread = HandlerThread("BackgroundQueue")
        handlerThread.start()

        val handler = Handler(handlerThread.looper)
        handler.post {
            // การทำงานตามลำดับบนเธรดพื้นหลัง
            Thread.sleep(500)
            print("HandlerThread: งานเสร็จสมบูรณ์")
        }

        // หยุดเธรด (ทำงานเมื่องานเสร็จ)
        handlerThread.quitSafely()
    }

    // 3. Executors — พูลเธรด
    fun executorExample() {
        val executor = Executors.newFixedThreadPool(4)
        for (i in 1..10) {
            executor.execute {
                print("Task $i on thread ${Thread.currentThread().getName()}")
            }
        }
        executor.shutdown()
    }

    // 4. Kotlin Coroutines — มาตรฐานสมัยใหม่
    suspend fun coroutineExample() = kotlinx.coroutines.withContext(
        kotlinx.coroutines.Dispatchers.Default
    ) {
        print("Coroutine on thread: ${Thread.currentThread().getName()}")
    }
}

ตัวอย่าง ThreadExample แสดงนามธรรมเธรดทั้งสี่ระดับใน Android การสร้าง Thread โดยตรงเป็นวิธีระดับต่ำสุดและมีประสิทธิภาพน้อยที่สุด HandlerThread มีประโยชน์สำหรับงานลำดับพื้นหลัง Executors.newFixedThreadPool(4) สร้างพูล 4 เธรดสำหรับการทำงานแบบขนานสูงสุด 10 งาน Kotlin Coroutines กับ Dispatchers.Default — วิธีที่ทันสมัย มีประสิทธิภาพ และปลอดภัย

HandlerThread: งานพื้นหลังแบบลำดับ

HandlerThread — คลาสย่อยเฉพาะของ Thread ที่มี Looper ในตัวและคิวข้อความ มันถูกสร้างโดยเรียก start() จากนั้นผ่าน Handler(handlerThread.looper) สามารถส่ง Runnable และข้อความ HandlerThread ทำงานตามลำดับอย่างเคร่งครัด — งานถัดไปจะไม่เริ่มจนกว่างานก่อนหน้าจะเสร็จ สะดวกสำหรับการเขียนข้อมูลลงใน Room หรือไฟล์ที่ลำดับการดำเนินการสำคัญ การเรียก quitSafely() หยุด Looper หลังจากงานปัจจุบันเสร็จ

Thread ใน iOS: Thread, GCD และ OperationQueue

iOS ก็มีสามระดับในการทำงานกับเธรด Thread (Thread ใน Swift, NSThread ใน Objective-C) — API ระดับต่ำที่สร้างเธรดเนทีฟโดยตรง GCD (Grand Central Dispatch) ผ่าน DispatchQueue — เครื่องมือหลักสำหรับนักพัฒนา iOS จัดการพูลเธรดโดยอัตโนมัติ OperationQueue — นามธรรมระดับสูงเหนือ GCD ที่รองรับการพึ่งพา ลำดับความสำคัญ และการยกเลิก

การใช้ Thread โดยตรงในการพัฒนา iOS สมัยใหม่พบได้ยากมาก — GCD มีความสามารถที่จำเป็นทั้งหมดพร้อมการจัดการหน่วยความจำและเธรดอัตโนมัติ Thread ใช้เฉพาะในกรณีพิเศษ: การตั้งค่า thread-local storage (threadDictionary), การสร้าง RunLoop สำหรับเธรดพื้นหลัง หรือการรวมเข้ากับไลบรารี C ที่คาดหวัง pthread_t

swift
import Foundation

class ThreadManager {

    // 1. Thread (ระดับต่ำ)
    func createThread() {
        let thread = Thread {
            // โค้ดทำงานบนเธรดใหม่
            print("Current thread: \(Thread.current)")
        }
        thread.name = "com.app.worker"
        thread.qualityOfService = .utility
        thread.start()
    }

    // 2. GCD — DispatchQueue
    func gcdExample() {
        // คิวขนาน
        let queue = DispatchQueue(label: "com.app.concurrent",
                                 qos: .utility,
                                 attributes: .concurrent)

        queue.async {
            print("GCD async task")
        }

        // Barrier สำหรับการซิงโครไนซ์การเขียน
        queue.async(flags: .barrier) {
            // การเข้าถึงเฉพาะระหว่างการเขียน
            print("Barrier write: exclusive access")
        }
    }

    // 3. OperationQueue พร้อมการพึ่งพา
    func operationQueueExample() {
        let queue = OperationQueue()
        queue.maxConcurrentOperationCount = 2
        queue.qualityOfService = .background

        let download = BlockOperation {
            print("Downloading...")
        }
        let process = BlockOperation {
            print("Processing...")
        }
        let save = BlockOperation {
            print("Saving...")
        }

        // การพึ่งพา: download -> process -> save
        process.addDependency(download)
        save.addDependency(process)

        queue.addOperations([download, process, save], waitUntilFinished: false)
    }
}

// คอลเลกชัน thread-safe ผ่าน GCD barrier
class ThreadSafeArray<T> {
    private var array: [T] = []
    private let queue = DispatchQueue(label: "com.app.concurrent",
                                       attributes: .concurrent)

    var count: Int {
        return queue.sync { array.count } // concurrent read
    }

    func append(_ element: T) {
        queue.async(flags: .barrier) { // exclusive write
            self.array.append(element)
        }
    }
}

คลาส ThreadSafeArray แสดงรูปแบบการอ่านพร้อมกัน / เขียนเฉพาะผ่าน GCD barrier การอ่านผ่าน queue.sync{} ทำงานแบบขนานจากหลายเธรด การเขียนผ่าน queue.async(flags: .barrier) บล็อกการดำเนินการอื่นทั้งหมด (ทั้งอ่านและเขียน) จนกว่าการเขียนจะเสร็จ มีประสิทธิภาพมากกว่าบล็อก synchronized เพราะไม่บล็อกผู้อ่านเมื่อไม่มีการเขียน

iOS Thread vs GCD: เมื่อใดควรใช้ Thread โดยตรง

การใช้ Thread โดยตรงใน iOS สมเหตุสมผลในสามกรณี: สำหรับ thread-local storage (Thread.current.threadDictionary) — การเก็บข้อมูลที่ผูกกับเธรด; สำหรับการสร้าง RunLoop พิเศษบนเธรดพื้นหลังด้วย performSelector:onThread:; สำหรับการรวมเข้ากับไลบรารี C/C++ ที่คาดหวัง pthread_t ในกรณีอื่นทั้งหมด GCD ผ่าน DispatchQueue ดีกว่า — จัดการพูลเธรดและการใช้พลังงานโดยอัตโนมัติ

การซิงโครไนซ์เธรด: locks, atomic, serial queues

Race condition (สภาวะการแข่งขัน) เกิดขึ้นเมื่อเธรดสองเธรดขึ้นไปเข้าถึงข้อมูลร่วมกันพร้อมกัน และอย่างน้อยหนึ่งเธรดทำการเขียน ผลลัพธ์ขึ้นอยู่กับลำดับการทำงาน (timing) และไม่สามารถคาดเดาได้ เพื่อป้องกัน race condition ใช้ไพรมิทีฟการซิงโครไนซ์ ในการพัฒนามือถือ มีล็อค (synchronized, NSLock), การดำเนินการอะตอมิก (AtomicInteger, คุณสมบัติ atomic ของ iOS) และคิว (serial queue)

การเลือกไพรมิทีฟ ขึ้นอยู่กับสถานการณ์ สำหรับตัวนับและธงอย่างง่าย การดำเนินการอะตอมิกก็เพียงพอ (AtomicInteger, คุณสมบัติ atomic) สำหรับส่วนวิกฤตที่มีหลายการดำเนินการ — ล็อค (synchronized, NSLock) สำหรับโครงสร้างข้อมูลที่ซับซ้อน — serial DispatchQueue หรือ GCD barrier ล็อคเข้าใจง่ายกว่าแต่อ่อนแอต่อ deadlock และ livelock คิวซับซ้อนกว่าแต่ปลอดภัยกว่า

kotlin
// การซิงโครไนซ์ใน Android/Kotlin
import java.util.concurrent.atomic.AtomicInteger
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock

class Counter {

    // 1. AtomicInteger — สำหรับตัวนับอย่างง่าย
    private val atomicCount = AtomicInteger(0)
    fun incrementAtomic() = atomicCount.incrementAndGet()

    // 2. synchronized — สำหรับส่วนวิกฤต
    @Synchronized
    fun synchronizedOperation() {
        // ทีละหนึ่งเธรดเท่านั้น
        doWork()
    }

    // 3. Mutex จาก coroutine — suspend-safe
    private val mutex = Mutex()
    suspend fun mutexOperation() {
        mutex.withLock {
            // protected code — thread-safe
            doWork()
        }
    }

    private fun doWork() { /* critical section */ }
}

// ตัวอย่าง Deadlock: A ล็อค B, B ล็อค A
class DeadlockExample {
    private val lockA = Any()
    private val lockB = Any()

    fun methodA() = synchronized(lockA) {
        Thread.sleep(100)
        synchronized(lockB) { print("OK") }
    }

    fun methodB() = synchronized(lockB) {
        Thread.sleep(100)
        synchronized(lockA) { print("OK") }
    }
}

Counter แสดงสามวิธีในการซิงโครไนซ์ AtomicInteger.incrementAndGet() — การดำเนินการอะตอมิกแบบไม่มีล็อค (CAS) @Synchronized — มอนิเตอร์ในตัวของ Java ล็อคทั้งออบเจ็กต์ Mutex.withLock — มิวเท็กซ์ของ coroutine ระงับ coroutine แทนที่จะบล็อกเธรด (มีประสิทธิภาพกว่า) DeadlockExample แสดง deadlock แบบคลาสสิก: สองเธรดรับล็อคในลำดับที่ต่างกัน

Thread Pools: ทำไม Executors ถึงดีกว่า Thread

Thread Pool (พูลเธรด) — ชุดของเธรดที่สร้างไว้ล่วงหน้าที่ถูกนำกลับมาใช้ใหม่สำหรับการทำงาน แทนที่จะสร้างเธรดใหม่สำหรับแต่ละงาน (แพง) พูลนำเธรดว่างจากพูล ถ้าไม่มีเธรดว่าง งานจะถูกใส่ในคิว พูลจัดการขนาดโดยอัตโนมัติ: เธรดใหม่ถูกสร้างขึ้นเมื่อโหลดสูง เธรดที่ว่างจะถูกสิ้นสุด ซึ่งลดค่าใช้จ่ายในการสร้างเธรดลงหลายสิบเท่า

ใน Android Executors.newFixedThreadPool(4) สร้างพูล 4 เธรด ถ้ามี 10 งานมาพร้อมกัน 4 งานเริ่มทันที 6 งานรอในคิว Executors.newCachedThreadPool() สร้างเธรดตามความจำเป็น (ไม่จำกัด) และสิ้นสุดเธรดที่ว่างหลังจาก 60 วินาที สำหรับ iOS GCD จัดหาพูลคิวทั่วโลกโดยอัตโนมัติ ซึ่งขนาดสอดคล้องกับจำนวนแกน CPU และโหลดปัจจุบัน

ใน Kotlin Coroutines พูลเธรดถูกซ่อนอยู่ใน dispatcher Dispatchers.Default ใช้พูลขนาดเท่ากับจำนวนแกน CPU (ขั้นต่ำ 2) Dispatchers.IO — 64 เธรด (เพียงพอสำหรับงาน IO-bound หลายร้อยงาน เพราะส่วนใหญ่จะรอ I/O โดยไม่ใช้ CPU) แต่ละ dispatcher ปรับขนาดพูลตามโหลดโดยอัตโนมัติ ประหยัดพลังงานแบตเตอรี่เมื่อว่าง

คำถามที่พบบ่อย

Thread ในการพัฒนามือถือคืออะไร?

Thread — หน่วยพื้นฐานของการทำงานของโค้ดในแอปพลิเคชัน แต่ละกระบวนการสามารถมีหลายเธรดที่แชร์หน่วยความจำแต่มีสแต็คของตัวเอง ในการพัฒนามือถือ เธรดถูกใช้สำหรับการทำงานแบบขนานของงานโดยไม่บล็อก UI Android ใช้ Thread, Executors, HandlerThread และ Coroutines iOS ใช้ Thread, GCD (DispatchQueue) และ OperationQueue

ทำไมไม่แนะนำให้สร้าง Thread โดยตรง?

การสร้าง Thread ต้องการการจัดสรรสแต็ค ~1 MB ใน Android และ ~512 KB ใน iOS — เป็นการดำเนินการที่แพง สำหรับ 1,000 งาน การสร้าง 1,000 เธรดโดยตรงจะต้องใช้ ~1 GB สำหรับสแต็คอย่างเดียว บวกกับค่าใช้จ่ายของ context switch แทนที่จะใช้ Thread ให้ใช้พูล (Executors, GCD) หรือ coroutine — พวกมันนำเธรดกลับมาใช้ใหม่ ลดค่าใช้จ่ายลงหลายสิบเท่า

Race condition คืออะไรและจะหลีกเลี่ยงได้อย่างไร?

Race condition — พฤติกรรมที่ไม่สามารถคาดเดาได้เมื่อหลายเธรดเข้าถึงข้อมูลร่วมกันพร้อมกันด้วยการเขียน สามารถหลีกเลี่ยงได้สามวิธี: ใช้ประเภทอะตอมิก (AtomicInteger), ใช้ล็อค (synchronized, NSLock) หรือจัดลำดับการเข้าถึงผ่านคิว (DispatchQueue serial, Actor ใน Kotlin) แนวปฏิบัติที่ดีที่สุดคือลดสถานะ mutable ร่วมกันให้น้อยที่สุดและใช้ immutability

Thread แตกต่างจาก coroutine อย่างไร?

Thread — ออบเจ็กต์ระบบแบบเนทีฟ กินสแต็ค ~1 MB และผูกกับแกน OS Coroutine — หน่วยการทำงานแบบเบาของ Kotlin ที่ไม่ผูกกับเธรดเฉพาะและสามารถระงับ (suspend) ได้โดยไม่บล็อก เธรดเดียวสามารถทำงาน coroutine ได้หลายพัน Coroutine มีประสิทธิภาพด้านหน่วยความจำมากกว่าและช่วยให้เขียนโค้ดอะซิงโครนัสโดยไม่ต้องใช้ callback

จะตรวจจับ deadlock ในแอปพลิเคชันมือถือได้อย่างไร?

Deadlock แสดงออกเป็นการค้างของแอปพลิเคชันโดยไม่มี ANR ใน Android ใช้ Thread.getAllStackTraces() เพื่อ dump สแต็คของเธรดทั้งหมด — สองเธรดจะรอล็อคของกันและกัน ใน iOS — Thread.callStackSymbols เครื่องมือ: Android Studio Profiler (แท็บ Threads), Instruments (iOS, Thread State View) การป้องกัน: รับล็อคในลำดับที่固定, ใช้ tryLock แบบมี timeout

สรุป

  • Thread — หน่วย CPU ที่เล็กที่สุด: การทำงานอิสระกับสแต็คของตัวเอง ฮีปหน่วยความจำร่วมกัน
  • ห้าสถานะ ของเธรด: New, Runnable, Running, Blocked/Waiting, Terminated
  • Android วิวัฒนาการจาก Thread → AsyncTask → Executors → HandlerThread → Coroutines
  • iOS มี Thread, GCD (DispatchQueue) และ OperationQueue — จากต่ำไปสูง
  • Race condition แก้ไขด้วยล็อค (synchronized, NSLock), ประเภทอะตอมิก และ serial queues
  • Deadlock เกิดจากการรับล็อคข้ามกัน — ป้องกันด้วยลำดับที่固定
  • Thread Pool มีประสิทธิภาพกว่าการสร้าง Thread ใหม่: นำเธรดกลับมาใช้ใหม่ ลดค่าใช้จ่าย context switch

เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร

IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ

ปรึกษาโครงการ

อ่านเพิ่มเติม