การตัดสินใจระหว่างกระบวนการย่อยการประมวลผลหลายกระบวนการและเธรดใน Python?


110

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

การพกพาเป็นสิ่งสำคัญเพราะฉันต้องการให้สิ่งนี้ทำงานบน Python เวอร์ชันใดก็ได้บน Mac, Linux และ Windows ด้วยข้อ จำกัด เหล่านี้โมดูล Python ใดที่เหมาะสมที่สุดสำหรับการใช้งานสิ่งนี้ ฉันกำลังพยายามตัดสินใจระหว่างเธรดกระบวนการย่อยและการประมวลผลหลายขั้นตอนซึ่งดูเหมือนจะมีฟังก์ชันที่เกี่ยวข้อง

มีความคิดเห็นเกี่ยวกับเรื่องนี้ไหม ฉันต้องการโซลูชันที่ง่ายที่สุดที่พกพาได้


ที่เกี่ยวข้อง: stackoverflow.com/questions/1743293/… (อ่านคำตอบของฉันที่นั่นเพื่อดูว่าเหตุใดเธรดจึงไม่เริ่มต้นสำหรับโค้ด Python แท้)

1
"Python เวอร์ชันใดก็ได้" FAR คลุมเครือเกินไป Python 2.3? 1.x? 3.x? เป็นเพียงเงื่อนไขที่เป็นไปไม่ได้ที่จะตอบสนอง
ย้อนกลับ

คำตอบ:


64

multiprocessingเป็นโมดูลประเภทมีดสวิส - กองทัพที่ยอดเยี่ยม เป็นเรื่องทั่วไปมากกว่าเธรดเนื่องจากคุณสามารถทำการคำนวณระยะไกลได้ นี่คือโมดูลที่ฉันขอแนะนำให้คุณใช้

subprocessโมดูลนอกจากนี้ยังจะช่วยให้คุณสามารถเปิดหลายกระบวนการ แต่ผมพบว่ามันจะสะดวกสบายน้อยลงกว่าที่จะใช้โมดูล multiprocessing ใหม่

เธรดมีความละเอียดอ่อนอย่างฉาวโฉ่และด้วย CPython คุณมักจะถูก จำกัด ไว้ที่หนึ่งคอร์ด้วย (แม้ว่าตามที่ระบุไว้ในความคิดเห็นใด ๆ Global Interpreter Lock (GIL) สามารถเผยแพร่ในรหัส C ที่เรียกว่าจากรหัส Python) .

ฉันเชื่อว่าฟังก์ชั่นส่วนใหญ่ของโมดูลทั้งสามที่คุณอ้างถึงสามารถใช้งานได้โดยไม่ขึ้นกับแพลตฟอร์ม ในด้านความสามารถในการพกพาโปรดทราบว่าmultiprocessingมีเฉพาะในเวอร์ชันมาตรฐานตั้งแต่ Python 2.6 เท่านั้น (มีเวอร์ชันสำหรับ Python เวอร์ชันเก่าบางรุ่นอยู่) แต่เป็นโมดูลที่ยอดเยี่ยม!


1
สำหรับงานที่กำหนดฉันใช้โมดูล "มัลติโพรเซสเซอร์" และเมธอด pool.map () ชิ้นเค้ก!
kmonsoor

คื่นฉ่ายอยู่ภายใต้การพิจารณาด้วยหรือไม่? ทำไมถึงเป็นหรือไม่?
user3245268

เท่าที่ฉันสามารถบอกได้ว่าขึ้นฉ่ายมีส่วนเกี่ยวข้องมากกว่า (คุณต้องติดตั้งนายหน้าข้อความ) แต่เป็นตัวเลือกที่ควรพิจารณาขึ้นอยู่กับปัญหาในมือ
Eric O Lebigot

186

สำหรับฉันมันค่อนข้างง่ายจริง ๆ :

กระบวนการย่อยตัวเลือก:

subprocessมีไว้สำหรับเรียกใช้ไฟล์ปฏิบัติการอื่น ๆ - โดยพื้นฐานแล้วจะเป็นกระดาษห่อหุ้มรอบ ๆos.fork()และos.execve()ด้วยการสนับสนุนท่อประปาที่เป็นทางเลือก (การตั้งค่า PIPE ไปยังและจากกระบวนการย่อยเห็นได้ชัดว่าคุณสามารถใช้กลไกการสื่อสารระหว่างกระบวนการ (IPC) อื่น ๆ เช่นซ็อกเก็ตหรือ Posix หรือ หน่วยความจำที่ใช้ร่วมกัน SysV แต่คุณจะถูก จำกัด เฉพาะอินเทอร์เฟซและช่อง IPC ที่รองรับโดยโปรแกรมที่คุณเรียกใช้

โดยทั่วไปเราใช้แบบsubprocessซิงโครนัส - เพียงแค่เรียกยูทิลิตีภายนอกบางตัวและอ่านเอาต์พุตหรือรอให้เสร็จสิ้น (อาจอ่านผลลัพธ์จากไฟล์ชั่วคราวหรือหลังจากโพสต์ไปยังฐานข้อมูลบางส่วน)

อย่างไรก็ตามเราสามารถสร้างกระบวนการย่อยหลายร้อยขั้นตอนและสำรวจความคิดเห็นได้ ยูทิลิตี้เองโปรดของฉันclasshไม่ตรงที่ ข้อเสียที่ใหญ่ที่สุดของsubprocessโมดูลคือการรองรับ I / O โดยทั่วไปจะปิดกั้น มีร่างPEP-3145สำหรับแก้ไขใน Python 3.x เวอร์ชันอนาคตและasyncprocทางเลือก(คำเตือนที่นำไปสู่การดาวน์โหลดโดยตรงไม่ใช่เอกสารประเภทใด ๆ หรือ README) ฉันยังพบว่ามันค่อนข้างง่ายที่จะนำเข้าfcntlและจัดการกับPopenตัวอธิบายไฟล์ PIPE ของคุณโดยตรง - แม้ว่าฉันจะไม่รู้ว่ามันพกพาไปยังแพลตฟอร์มที่ไม่ใช่ UNIX ได้หรือไม่

(อัปเดต: 7 สิงหาคม 2019: Python 3 รองรับกระบวนการย่อยayncio : asyncio Subprocesses )

subprocess แทบจะไม่มีการรองรับการจัดการเหตุการณ์ ... แม้ว่าคุณจะสามารถใช้signalโมดูลและสัญญาณ UNIX / Linux แบบเก่า ๆ - ฆ่ากระบวนการของคุณอย่างนุ่มนวลเหมือนที่เคยเป็นมา

multiprocessingตัวเลือก:

multiprocessingมีไว้สำหรับการเรียกใช้ฟังก์ชันภายในโค้ด (Python) ที่มีอยู่ของคุณโดยรองรับการสื่อสารที่ยืดหยุ่นมากขึ้นระหว่างกระบวนการตระกูลนี้ โดยเฉพาะอย่างยิ่งคุณควรสร้างmultiprocessingIPC ของคุณรอบ ๆQueueอ็อบเจ็กต์ของโมดูลหากเป็นไปได้ แต่คุณยังสามารถใช้Eventอ็อบเจ็กต์และคุณสมบัติอื่น ๆ ได้อีกด้วย (ซึ่งบางส่วนน่าจะสร้างขึ้นจากmmapการสนับสนุนบนแพลตฟอร์มที่การรองรับนั้นเพียงพอ)

multiprocessingโมดูลของ Python มีจุดมุ่งหมายเพื่อจัดเตรียมอินเทอร์เฟซและคุณสมบัติที่คล้ายกัน มากthreadingในขณะที่อนุญาตให้ CPython ปรับขนาดการประมวลผลของคุณระหว่าง CPU / คอร์หลายตัวแม้จะมี GIL (Global Interpreter Lock) ใช้ประโยชน์จากการล็อก SMP แบบละเอียดและความพยายามในการทำงานร่วมกันที่เกิดขึ้นโดยนักพัฒนาเคอร์เนลระบบปฏิบัติการของคุณ

เกลียวตัวเลือก:

threadingมีไว้สำหรับแอปพลิเคชันที่ค่อนข้างแคบซึ่งมีการเชื่อมต่อ I / O (ไม่จำเป็นต้องปรับขนาดในแกน CPU หลายตัว) และได้รับประโยชน์จากเวลาแฝงที่ต่ำมากและการเปลี่ยนค่าใช้จ่ายในการสลับเธรด (ที่มีหน่วยความจำหลักที่ใช้ร่วมกัน) เทียบกับกระบวนการ / การสลับบริบท บน Linux นี่เกือบจะเป็นชุดที่ว่างเปล่า (เวลาในการสลับกระบวนการของ Linux อยู่ใกล้กับเธรดสวิตช์มาก)

threadingทนทุกข์ทรมานจากสองข้อเสียที่สำคัญในหลาม

แน่นอนว่าหนึ่งคือการใช้งานเฉพาะ - ส่วนใหญ่มีผลต่อ CPython นั่นคือ GIL โดยส่วนใหญ่โปรแกรม CPython ส่วนใหญ่จะไม่ได้รับประโยชน์จากความพร้อมใช้งานของซีพียู (คอร์) มากกว่าสองตัวและบ่อยครั้งที่ประสิทธิภาพจะได้รับผลกระทบจากข้อขัดแย้งในการล็อก GIL

ปัญหาที่ใหญ่กว่าซึ่งไม่ใช่เฉพาะการนำไปใช้งานคือเธรดใช้หน่วยความจำตัวจัดการสัญญาณตัวอธิบายไฟล์และทรัพยากรระบบปฏิบัติการอื่น ๆ ร่วมกัน ดังนั้นโปรแกรมเมอร์จึงต้องระมัดระวังอย่างมากเกี่ยวกับการล็อกอ็อบเจ็กต์การจัดการข้อยกเว้นและด้านอื่น ๆ ของโค้ดซึ่งทั้งละเอียดอ่อนและสามารถฆ่าหยุดหรือหยุดชะงักกระบวนการทั้งหมด (ชุดเธรด)

โดยการเปรียบเทียบmultiprocessingโมเดลจะให้หน่วยความจำของตัวเองตัวอธิบายไฟล์ ฯลฯ ข้อผิดพลาดหรือข้อยกเว้นที่ไม่สามารถจัดการได้ในหนึ่งในนั้นจะฆ่าทรัพยากรนั้นเท่านั้นและการจัดการกับการหายไปของกระบวนการย่อยหรือพี่น้องอย่างมีประสิทธิภาพนั้นทำได้ง่ายกว่าการดีบักแยก และแก้ไขหรือแก้ไขปัญหาที่คล้ายกันในชุดข้อความ

  • (หมายเหตุ: การใช้threadingกับระบบ Python หลัก ๆ เช่นNumPyอาจได้รับผลกระทบจากการโต้แย้ง GIL น้อยกว่ารหัส Python ส่วนใหญ่ของคุณเองนั่นเป็นเพราะพวกเขาได้รับการออกแบบมาโดยเฉพาะเพื่อทำเช่นนั้นส่วนเนทีฟ / ไบนารีของ NumPy ตัวอย่างเช่นจะปล่อย GIL เมื่อปลอดภัย)

บิดตัวเลือก:

นอกจากนี้ยังเป็นที่น่าสังเกตว่าบิดข้อเสนอยังอีกทางเลือกหนึ่งซึ่งเป็นทั้งหรูหราและมากความท้าทายที่จะเข้าใจ โดยทั่วไปแล้วหากมีความเสี่ยงที่จะทำให้ง่ายขึ้นจนถึงจุดที่แฟน ๆ ของ Twisted อาจบุกบ้านของฉันด้วยโกยและคบเพลิง Twisted จะจัดเตรียมการทำงานร่วมกันหลายอย่างที่ขับเคลื่อนด้วยเหตุการณ์ภายในกระบวนการใด ๆ (เดียว)

เพื่อให้เข้าใจว่าสิ่งนี้เป็นไปได้อย่างไรควรอ่านเกี่ยวกับคุณสมบัติของselect()(ซึ่งสามารถสร้างขึ้นจากการเลือก ()หรือแบบสำรวจ ()หรือการเรียกระบบปฏิบัติการที่คล้ายกัน) โดยพื้นฐานแล้วทั้งหมดนี้เกิดจากความสามารถในการร้องขอให้ระบบปฏิบัติการเข้าสู่โหมดสลีปเพื่อรอดำเนินกิจกรรมใด ๆ ในรายการตัวอธิบายไฟล์หรือการหมดเวลา

การปลุกจากการเรียกแต่ละครั้งถึงselect()เป็นเหตุการณ์หนึ่งซึ่งเกี่ยวข้องกับอินพุตที่พร้อมใช้งาน (อ่านได้) บนซ็อกเก็ตหรือตัวอธิบายไฟล์จำนวนหนึ่งหรือพื้นที่บัฟเฟอร์ที่พร้อมใช้งานบนตัวอธิบายหรือซ็อกเก็ตอื่น ๆ (เขียนได้) เงื่อนไขพิเศษบางประการ (TCP แพ็กเก็ต PUSH ที่ไม่อยู่ในวงดนตรีเป็นต้น) หรือ TIMEOUT

ดังนั้นแบบจำลองการเขียนโปรแกรม Twisted จึงถูกสร้างขึ้นโดยใช้การจัดการเหตุการณ์เหล่านี้จากนั้นวนซ้ำบนตัวจัดการ "หลัก" ที่เป็นผลลัพธ์ทำให้สามารถส่งเหตุการณ์ไปยังตัวจัดการของคุณได้

โดยส่วนตัวฉันคิดว่าชื่อTwistedเป็นตัวกระตุ้นให้เกิดรูปแบบการเขียนโปรแกรม ... เนื่องจากแนวทางของคุณในการแก้ปัญหาต้องเป็น "บิด" จากภายในสู่ภายนอก แทนที่จะคิดโปรแกรมของคุณเป็นชุดของการดำเนินการกับข้อมูลอินพุตและเอาต์พุตหรือผลลัพธ์คุณกำลังเขียนโปรแกรมของคุณเป็นบริการหรือดีมอนและกำหนดวิธีที่จะตอบสนองต่อเหตุการณ์ต่างๆ (ในความเป็นจริงแกนหลัก "ลูปหลัก" ของโปรแกรม Twisted คือ (โดยปกติคือเสมอ?) กreactor())

ความท้าทายที่สำคัญในการใช้บิดเกี่ยวข้องกับการบิดใจรอบแบบจำลองเหตุการณ์ที่ขับเคลื่อนด้วยและยังละทิ้งการใช้ห้องสมุดชั้นใด ๆ หรือชุดเครื่องมือที่ไม่ได้เขียนขึ้นเพื่อร่วมดำเนินการภายในกรอบบิด นี่คือเหตุผลที่ Twisted จัดหาโมดูลของตัวเองสำหรับการจัดการโปรโตคอล SSH สำหรับคำสาปและฟังก์ชั่นกระบวนการย่อย / Popen ของตัวเองรวมถึงโมดูลและตัวจัดการโปรโตคอลอื่น ๆ อีกมากมายซึ่งในตอนแรกบลัชออนดูเหมือนจะทำซ้ำสิ่งต่างๆในไลบรารีมาตรฐาน Python

ฉันคิดว่าการทำความเข้าใจ Twisted ในระดับแนวคิดมีประโยชน์แม้ว่าคุณจะไม่เคยตั้งใจใช้ก็ตาม อาจให้ข้อมูลเชิงลึกเกี่ยวกับประสิทธิภาพการแข่งขันและการจัดการเหตุการณ์ในเธรดการประมวลผลหลายกระบวนการและแม้แต่การจัดการกระบวนการย่อยรวมทั้งการประมวลผลแบบกระจายที่คุณดำเนินการ

( หมายเหตุ: Python 3.x เวอร์ชันใหม่กว่าจะรวมคุณสมบัติ asyncio (asynchronous I / O) เช่นasync def , @ async.coroutine decorator และawait keyword และผลตอบแทนจากการสนับสนุนในอนาคตทั้งหมดนี้มีความคล้ายคลึงกับบิดจากมุมมองของกระบวนการ (การทำงานหลายอย่างร่วมกัน) (สำหรับสถานะปัจจุบันของการสนับสนุน Twisted สำหรับ Python 3 โปรดดู: https://twistedmatrix.com/documents/current/core/howto/python3.html )

กระจายตัวเลือก:

อีกหนึ่งขอบเขตของการประมวลผลที่คุณไม่ได้ถามถึง แต่สิ่งที่ควรค่าแก่การพิจารณาคือการประมวลผลแบบกระจาย มีเครื่องมือและเฟรมเวิร์ก Python มากมายสำหรับการประมวลผลแบบกระจายและการคำนวณแบบขนาน โดยส่วนตัวแล้วฉันคิดว่าวิธีที่ง่ายที่สุดในการใช้คือสิ่งที่มักถูกมองว่าอยู่ในพื้นที่นั้นน้อยที่สุด

มันเป็นเรื่องเล็ก ๆ น้อยเกือบจะสร้างประมวลผลแบบกระจายทั่วRedis สามารถใช้ที่เก็บคีย์ทั้งหมดเพื่อจัดเก็บหน่วยงานและผลลัพธ์ Redis LIST สามารถใช้เป็นQueue()เหมือนวัตถุและการรองรับ PUB / SUB สามารถใช้สำหรับการEventจัดการที่เหมือนกันได้ คุณสามารถแฮชคีย์และใช้ค่าที่จำลองแบบข้ามกลุ่มอินสแตนซ์ Redis แบบหลวม ๆ เพื่อจัดเก็บโทโพโลยีและการแมปแฮชโทเค็นเพื่อจัดเตรียมการแฮชที่สอดคล้องกันและการล้มเหลวสำหรับการปรับขนาดเกินขีดความสามารถของอินสแตนซ์เดียวสำหรับการประสานงานคนงานของคุณ และข้อมูลการจัดกลุ่ม (ดอง JSON BSON หรือ YAML)

แน่นอนว่าเมื่อคุณเริ่มสร้างสเกลที่ใหญ่ขึ้นและโซลูชันที่ซับซ้อนมากขึ้นรอบ ๆ Redis คุณกำลังนำคุณสมบัติหลายอย่างที่ได้รับการแก้ไขไปแล้วมาใช้ใหม่โดยใช้Celery , Apache SparkและHadoop , Zookeeper , etcd , Cassandraและอื่น ๆ ทั้งหมดนี้มีโมดูลสำหรับ Python ในการเข้าถึงบริการของตน

[Update: คู่ของทรัพยากรสำหรับการพิจารณาถ้าคุณกำลังพิจารณาหลามสำหรับคอมพิวเตอร์อย่างเข้มข้นในระบบการกระจาย: ขนาน IPythonและPySpark แม้ว่าระบบเหล่านี้จะเป็นระบบคอมพิวเตอร์แบบกระจายจุดประสงค์ทั่วไป แต่ระบบย่อยเหล่านี้สามารถเข้าถึงได้และเป็นที่นิยมโดยเฉพาะวิทยาศาสตร์ข้อมูลและการวิเคราะห์]

สรุป

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


1
มันยากที่จะใช้การประมวลผลหลายขั้นตอนกับคลาส / OOP แม้ว่า
Tjorriemorrie

2
@Tjorriemorrie: ฉันจะเดาว่าคุณหมายความว่ามันยากที่จะส่งวิธีการเรียกไปยังอินสแตนซ์ของวัตถุที่อาจอยู่ในกระบวนการอื่น ฉันขอแนะนำว่านี่เป็นปัญหาเดียวกับที่คุณมีกับเธรด แต่สามารถมองเห็นได้ง่ายกว่า (แทนที่จะเปราะบางและอยู่ภายใต้เงื่อนไขการแข่งขันที่คลุมเครือ) ฉันคิดว่าแนวทางที่แนะนำคือการจัดเตรียมการจัดส่งทั้งหมดให้เกิดขึ้นผ่านอ็อบเจ็กต์คิวซึ่งทำงานแบบเธรดเดียวมัลติเธรดและข้ามกระบวนการ (ด้วยการใช้ Redis หรือ Celery Queue บางส่วนแม้กระทั่งในกลุ่มของโหนด)
Jim Dennis

2
นี่คือคำตอบที่ดีจริงๆ ฉันหวังว่าจะเป็นการแนะนำการทำงานพร้อมกันในเอกสาร Python3
รูท -11

1
@ root-11 คุณสามารถเสนอให้ผู้ดูแลเอกสารได้ ฉันได้เผยแพร่ที่นี่สำหรับการใช้งานฟรี คุณและพวกเขาสามารถใช้ได้ไม่ว่าจะทั้งหมดหรือบางส่วน
Jim Dennis

"สำหรับฉันแล้วมันค่อนข้างง่ายจริงๆ:" รักเลย ขอบคุณมาก
jerome

5

ในกรณีที่คล้ายกันฉันเลือกใช้กระบวนการแยกต่างหากและซ็อกเก็ตเครือข่ายรางสื่อสารที่จำเป็นเล็กน้อย มันพกพาได้สูงและค่อนข้างง่ายในการทำโดยใช้ python แต่อาจจะไม่ง่ายกว่านั้น (ในกรณีของฉันฉันมีข้อ จำกัด อีกอย่างหนึ่งคือการสื่อสารกับกระบวนการอื่น ๆ ที่เขียนด้วย C ++)

ในกรณีของคุณฉันอาจจะใช้หลายกระบวนการเนื่องจากเธรด python อย่างน้อยเมื่อใช้ CPython ไม่ใช่เธรดจริง พวกเขาเป็นเธรดระบบดั้งเดิม แต่โมดูล C ที่เรียกจาก Python อาจหรืออาจไม่ปล่อย GIL และอนุญาตให้เธรดอื่นทำงานเมื่อเรียกใช้รหัสบล็อก


4

หากต้องการใช้โปรเซสเซอร์หลายตัวใน CPython ทางเลือกเดียวของคุณคือmultiprocessingโมดูล CPython ช่วยล็อคไว้ภายใน ( GIL ) ซึ่งป้องกันไม่ให้เธรดบนซีพียูอื่นทำงานแบบขนาน multiprocessingโมดูลสร้างกระบวนการใหม่ (ชอบsubprocess) และจัดการการสื่อสารระหว่างพวกเขา


5
ไม่เป็นความจริงเลย AFAIK คุณสามารถปล่อย GIL โดยใช้ C API และมีการใช้งาน Python อื่น ๆ เช่น IronPython หรือ Jython ซึ่งไม่ได้รับผลกระทบจากข้อ จำกัด ดังกล่าว ฉันไม่ได้โหวตลงคะแนน
Bastien Léonard

1

ปอกเปลือกและปล่อยให้ยูนิกซ์ทำงานของคุณ:

ใช้iterpipesเพื่อตัดกระบวนการย่อยจากนั้น:

จากเว็บไซต์ของ Ted Ziuba

INPUTS_FROM_YOU | xargs -n1 -0 -P NUM ./process #NUM กระบวนการแบบขนาน

หรือ

Gnu Parallelจะให้บริการด้วย

คุณออกไปเที่ยวกับ GIL ในขณะที่ส่งเด็ก ๆ ในห้องทำงานออกไปทำงานหลายคอร์ของคุณ


6
"ความสามารถในการพกพาเป็นสิ่งสำคัญเพราะฉันต้องการให้สิ่งนี้ทำงานบน Python เวอร์ชันใดก็ได้บน Mac, Linux และ Windows"
ย้อนกลับ

ด้วยโซลูชันนี้คุณสามารถโต้ตอบกับงานซ้ำ ๆ ได้หรือไม่? คุณสามารถทำได้ในหลายกระบวนการ แต่ฉันไม่คิดอย่างนั้นในกระบวนการย่อย
หอยทาก
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.