จะรู้ได้อย่างไรว่ากระทู้อื่นจบแล้ว?


127

ฉันมีวัตถุที่มีเมธอดชื่อStartDownload()ซึ่งเริ่มต้นสามเธรด

ฉันจะรับการแจ้งเตือนได้อย่างไรเมื่อแต่ละเธรดดำเนินการเสร็จสิ้น

มีวิธีที่จะทราบว่าเธรดหนึ่ง (หรือทั้งหมด) เสร็จสิ้นหรือยังคงดำเนินการอยู่หรือไม่?


คำตอบ:


228

มีหลายวิธีที่คุณสามารถทำได้:

  1. ใช้Thread.join ()ในเธรดหลักของคุณเพื่อรอให้เธรดแต่ละเธรดเสร็จสมบูรณ์หรือ
  2. ตรวจสอบThread.isAlive ()ในรูปแบบการสำรวจ - โดยทั่วไปไม่สนับสนุน - เพื่อรอจนกว่าแต่ละเธรดจะเสร็จสมบูรณ์หรือ
  3. Unorthodox สำหรับแต่ละเธรดที่มีปัญหาให้เรียกsetUncaughtExceptionHandlerเพื่อเรียกใช้เมธอดในอ็อบเจ็กต์ของคุณและตั้งโปรแกรมเธรดแต่ละเธรดเพื่อโยนข้อยกเว้นที่ไม่ถูกจับเมื่อมันเสร็จสมบูรณ์หรือ
  4. ใช้ล็อคหรือซิงโครไนซ์หรือกลไกจากjava.util.concurrentหรือ
  5. ดั้งเดิมมากขึ้นสร้างผู้ฟังในเธรดหลักของคุณจากนั้นตั้งโปรแกรมเธรดแต่ละเธรดของคุณเพื่อบอกผู้ฟังว่าเสร็จสิ้นแล้ว

จะใช้ Idea # 5 ได้อย่างไร? วิธีหนึ่งคือสร้างอินเทอร์เฟซก่อน:

public interface ThreadCompleteListener {
    void notifyOfThreadComplete(final Thread thread);
}

จากนั้นสร้างคลาสต่อไปนี้:

public abstract class NotifyingThread extends Thread {
  private final Set<ThreadCompleteListener> listeners
                   = new CopyOnWriteArraySet<ThreadCompleteListener>();
  public final void addListener(final ThreadCompleteListener listener) {
    listeners.add(listener);
  }
  public final void removeListener(final ThreadCompleteListener listener) {
    listeners.remove(listener);
  }
  private final void notifyListeners() {
    for (ThreadCompleteListener listener : listeners) {
      listener.notifyOfThreadComplete(this);
    }
  }
  @Override
  public final void run() {
    try {
      doRun();
    } finally {
      notifyListeners();
    }
  }
  public abstract void doRun();
}

จากนั้นแต่ละเธรดของคุณจะขยายออกไปNotifyingThreadและแทนที่จะใช้run()มันจะใช้งานdoRun()ได้ ดังนั้นเมื่อเสร็จสิ้นพวกเขาจะแจ้งทุกคนที่รอการแจ้งเตือนโดยอัตโนมัติ

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

NotifyingThread thread1 = new OneOfYourThreads();
thread1.addListener(this); // add ourselves as a listener
thread1.start();           // Start the Thread

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

โปรดทราบว่าดีกว่าจะimplements Runnableมากกว่าextends ThreadสำหรับNotifyingThreadการขยายกระทู้มักจะเป็นกำลังใจในรหัสใหม่ แต่ฉันกำลังเขียนคำถามของคุณ หากคุณเปลี่ยนNotifyingThreadคลาสเพื่อใช้งานRunnableคุณจะต้องเปลี่ยนโค้ดบางส่วนของคุณที่จัดการเธรดซึ่งค่อนข้างตรงไปตรงมาที่จะทำ


สวัสดี !! ฉันชอบความคิดสุดท้าย ฉันจะใช้ผู้ฟังเพื่อทำสิ่งนั้น? ขอบคุณ
Ricardo Felgueiras

4
แต่เมื่อใช้วิธีนี้ notifiyListeners จะถูกเรียกภายใน run () ดังนั้นมันจะถูกเรียกว่า insisde the thread และจะมีการเรียกต่อไปที่นั่นด้วยใช่หรือไม่?
Jordi Puigdellívol

1
@Eddie Jordi ถามว่าเป็นไปได้หรือไม่ที่จะเรียกnotifyวิธีการไม่ได้แทรกซึมrunวิธีการ แต่หลังจากนั้น
Tomasz Dzięcielewski

1
คำถามจริงๆคือคุณจะปิดเธรดรองได้อย่างไร ฉันรู้ว่ามันเสร็จแล้ว แต่ฉันจะเข้าสู่เธรดหลักได้อย่างไร?
Patrick

1
กระทู้นี้ปลอดภัยหรือไม่? ดูเหมือนว่าจะมีการเรียกใช้ alertListeners (และดังนั้น informOfThreadComplete) ภายใน NotifyingThread แทนที่จะอยู่ภายในเธรดที่สร้างขึ้นซึ่งสร้าง Listener ขึ้นมาเอง
Aaron

13

วิธีแก้ปัญหาโดยใช้CyclicBarrier

public class Downloader {
  private CyclicBarrier barrier;
  private final static int NUMBER_OF_DOWNLOADING_THREADS;

  private DownloadingThread extends Thread {
    private final String url;
    public DownloadingThread(String url) {
      super();
      this.url = url;
    }
    @Override
    public void run() {
      barrier.await(); // label1
      download(url);
      barrier.await(); // label2
    }
  }
  public void startDownload() {
    // plus one for the main thread of execution
    barrier = new CyclicBarrier(NUMBER_OF_DOWNLOADING_THREADS + 1); // label0
    for (int i = 0; i < NUMBER_OF_DOWNLOADING_THREADS; i++) {
      new DownloadingThread("http://www.flickr.com/someUser/pic" + i + ".jpg").start();
    }
    barrier.await(); // label3
    displayMessage("Please wait...");
    barrier.await(); // label4
    displayMessage("Finished");
  }
}

label0 - สิ่งกีดขวางแบบวนถูกสร้างขึ้นโดยมีจำนวนปาร์ตี้เท่ากับจำนวนเธรดที่ดำเนินการบวกหนึ่งสำหรับเธรดหลักของการดำเนินการ (ซึ่ง startDownload () กำลังดำเนินการ)

ป้ายกำกับ 1 - n-th DownloadingThread เข้าสู่ห้องรอ

ป้ายกำกับ 3 - NUMBER_OF_DOWNLOADING_THREADS เข้าห้องรอ เธรดหลักของการดำเนินการเผยแพร่เพื่อเริ่มทำการดาวน์โหลดงานในเวลาเดียวกันไม่มากก็น้อย

ป้ายกำกับ 4 - เธรดหลักของการดำเนินการเข้าสู่ห้องรอ นี่คือส่วนที่ 'ยากที่สุด' ของโค้ดที่จะต้องทำความเข้าใจ ไม่สำคัญว่าเธรดใดจะเข้าสู่ห้องรอเป็นครั้งที่สอง สิ่งสำคัญคือเธรดใด ๆ ก็ตามที่เข้ามาในห้องสุดท้ายเพื่อให้แน่ใจว่าเธรดการดาวน์โหลดอื่น ๆ ทั้งหมดเสร็จสิ้นการดาวน์โหลดงานแล้ว

ป้ายกำกับ 2 - n-th DownloadingThread เสร็จสิ้นการดาวน์โหลดงานและเข้าสู่ห้องรอ หากเป็นรายการสุดท้ายเช่นป้อนไปแล้ว NUMBER_OF_DOWNLOADING_THREADS รวมถึงเธรดหลักของการดำเนินการเธรดหลักจะดำเนินการต่อเมื่อเธรดอื่น ๆ ทั้งหมดเสร็จสิ้นการดาวน์โหลดเท่านั้น


9

คุณควรจริงๆjava.util.concurrentต้องการแก้ปัญหาที่ใช้ ค้นหาและอ่าน Josh Bloch และ / หรือ Brian Goetz ในหัวข้อนี้

หากคุณไม่ได้ใช้งานjava.util.concurrent.*และรับผิดชอบในการใช้เธรดโดยตรงคุณควรjoin()ทราบเมื่อเธรดเสร็จสิ้น นี่คือกลไกการโทรกลับที่เรียบง่ายสุด ๆ ขั้นแรกให้ขยายRunnableอินเทอร์เฟซเพื่อให้มีการติดต่อกลับ:

public interface CallbackRunnable extends Runnable {
    public void callback();
}

จากนั้นสร้าง Executor ที่จะเรียกใช้ runnable ของคุณและโทรกลับหาคุณเมื่อดำเนินการเสร็จสิ้น

public class CallbackExecutor implements Executor {

    @Override
    public void execute(final Runnable r) {
        final Thread runner = new Thread(r);
        runner.start();
        if ( r instanceof CallbackRunnable ) {
            // create a thread to perform the callback
            Thread callerbacker = new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        // block until the running thread is done
                        runner.join();
                        ((CallbackRunnable)r).callback();
                    }
                    catch ( InterruptedException e ) {
                        // someone doesn't want us running. ok, maybe we give up.
                    }
                }
            });
            callerbacker.start();
        }
    }

}

สิ่งที่ชัดเจนอื่น ๆ ที่จะเพิ่มในCallbackRunnableอินเทอร์เฟซของคุณคือวิธีการจัดการข้อยกเว้นใด ๆ ดังนั้นอาจใส่public void uncaughtException(Throwable e);บรรทัดไว้ที่นั่นและในตัวดำเนินการของคุณติดตั้งเธรด UncaughtExceptionHandler เพื่อส่งคุณไปยังวิธีการเชื่อมต่อนั้น

แต่การทำทั้งหมดนั้นเริ่มมีกลิ่นหอมjava.util.concurrent.Callableจริงๆ คุณควรพิจารณาใช้java.util.concurrentว่าโครงการของคุณอนุญาตหรือไม่


ฉันไม่ชัดเจนเล็กน้อยเกี่ยวกับสิ่งที่คุณได้รับจากกลไกการโทรกลับนี้เทียบกับการเรียกเพียงอย่างเดียวrunner.join()จากนั้นรหัสใด ๆ ที่คุณต้องการหลังจากนั้นเนื่องจากคุณรู้ว่าเธรดเสร็จสิ้นแล้ว เป็นเพียงการที่คุณกำหนดรหัสนั้นเป็นคุณสมบัติของ runnable ดังนั้นคุณอาจมีสิ่งที่แตกต่างกันสำหรับ runnables ที่แตกต่างกัน?
Stephen

2
ใช่runner.join()เป็นวิธีที่ตรงที่สุดในการรอ ฉันสมมติว่า OP ไม่ต้องการบล็อกเธรดการโทรหลักของพวกเขาเนื่องจากพวกเขาขอให้ "แจ้งเตือน" สำหรับการดาวน์โหลดแต่ละครั้งซึ่งสามารถดำเนินการตามลำดับใดก็ได้ นี่เป็นวิธีหนึ่งในการรับการแจ้งเตือนแบบอะซิงโครนัส
broc.seib

4

คุณต้องการรอให้เสร็จหรือไม่ ในกรณีนี้ให้ใช้วิธีเข้าร่วม

นอกจากนี้ยังมีคุณสมบัติ isAlive หากคุณต้องการตรวจสอบ


3
โปรดทราบว่า isAlive จะส่งคืนค่าเท็จหากเธรดยังไม่เริ่มดำเนินการ (แม้ว่าเธรดของคุณเองจะเรียกว่า start แล้วก็ตาม)
Tom Hawtin - แทคไลน์

@ TomHawtin-tackline คุณค่อนข้างแน่ใจเกี่ยวกับเรื่องนี้หรือไม่? มันจะขัดแย้งกับเอกสาร Java ("เธรดยังมีชีวิตอยู่หากมีการเริ่มต้นและยังไม่ตาย" - docs.oracle.com/javase/6/docs/api/java/lang/… ) นอกจากนี้ยังจะขัดแย้งกับคำตอบที่นี่ ( stackoverflow.com/questions/17293304/… )
Stephen

@ สตีเฟนนานมากแล้วที่ฉันเขียนแบบนั้น แต่ดูเหมือนว่าจะเป็นเรื่องจริง ฉันคิดว่ามันทำให้คนอื่นมีปัญหาที่เกิดขึ้นใหม่ในความทรงจำของฉันเมื่อเก้าปีก่อน สิ่งที่สังเกตได้จะขึ้นอยู่กับการนำไปใช้งาน คุณบอก a Threadถึงstartซึ่งเธรดทำ แต่การโทรกลับทันที isAliveควรจะมีการทดสอบธงที่เรียบง่าย แต่เมื่อฉัน googled nativeมันเป็นวิธีการที่
Tom Hawtin - แทคไลน์

4

คุณสามารถสอบถามอินสแตนซ์เธรดด้วย getState () ซึ่งส่งคืนอินสแตนซ์ของ Thread.State enumeration ด้วยค่าใดค่าหนึ่งต่อไปนี้:

*  NEW
  A thread that has not yet started is in this state.
* RUNNABLE
  A thread executing in the Java virtual machine is in this state.
* BLOCKED
  A thread that is blocked waiting for a monitor lock is in this state.
* WAITING
  A thread that is waiting indefinitely for another thread to perform a particular action is in this state.
* TIMED_WAITING
  A thread that is waiting for another thread to perform an action for up to a specified waiting time is in this state.
* TERMINATED
  A thread that has exited is in this state.

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


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

3

คุณยังสามารถใช้Executorsอ็อบเจ็กต์เพื่อสร้าง เธรดพูลExecutorService จากนั้นใช้invokeAllวิธีการเรียกใช้แต่ละเธรดของคุณและดึงข้อมูล Futures สิ่งนี้จะบล็อกจนกว่าทั้งหมดจะเสร็จสิ้นการดำเนินการ ตัวเลือกอื่นของคุณคือดำเนินการแต่ละตัวโดยใช้พูลจากนั้นเรียกawaitTerminationไปยังบล็อกจนกว่าพูลจะดำเนินการเสร็จสิ้น อย่าลืมโทรหาshutdown() เมื่อคุณเพิ่มงานเสร็จแล้ว


2

มีการเปลี่ยนแปลงหลายอย่างในช่วง 6 ปีที่ผ่านมาในด้านหน้าแบบมัลติเธรด

แทนที่จะใช้join()และล็อก API คุณสามารถใช้ไฟล์

1. ExecutorService invokeAll() API

ดำเนินงานที่กำหนดส่งคืนรายการ Futures ที่มีสถานะและผลลัพธ์เมื่อทั้งหมดเสร็จสมบูรณ์

2. CountDownLatch

ตัวช่วยในการซิงโครไนซ์ที่อนุญาตให้เธรดตั้งแต่หนึ่งเธรดขึ้นไปรอจนกว่าชุดของการดำเนินการในเธรดอื่นจะเสร็จสิ้น

A CountDownLatchเริ่มต้นด้วยจำนวนที่กำหนด วิธีการรอคอยบล็อกจนกว่าจำนวนปัจจุบันจะถึงศูนย์เนื่องจากการเรียกใช้countDown()เมธอดหลังจากนั้นเธรดที่รอทั้งหมดจะถูกปล่อยออกและการเรียกใช้การรอคอยที่ตามมาจะส่งคืนทันที นี่เป็นปรากฏการณ์หนึ่งช็อต - ไม่สามารถรีเซ็ตการนับได้ หากคุณต้องการเวอร์ชันที่รีเซ็ตการนับให้พิจารณาใช้ CyclicBarrier

3. ForkJoinPoolหรือ newWorkStealingPool()ในExecutorsเป็นวิธีอื่น

4. ฝึกฝนFutureงานทั้งหมดจากการส่งExecutorServiceและตรวจสอบสถานะด้วยการปิดกั้นการโทรget()บนFutureวัตถุ

ดูคำถาม SE ที่เกี่ยวข้อง:

จะรอด้ายที่เกิดด้ายของตัวเองได้อย่างไร?

ผู้ดำเนินการ: จะรอให้งานทั้งหมดเสร็จสิ้นได้อย่างไรหากมีการสร้างงานซ้ำ ๆ


2

ฉันขอแนะนำให้ดูที่ javadoc สำหรับ คลาสThread

คุณมีกลไกหลายอย่างสำหรับการจัดการเธรด

  • เธรดหลักของคุณอาจjoin()เป็นสามเธรดแบบต่อเนื่องและจะไม่ดำเนินการต่อจนกว่าทั้งสามจะเสร็จสิ้น

  • สำรวจสถานะเธรดของเธรดที่เกิดในช่วงเวลา

  • ใส่ทั้งหมดของหัวข้อกลับกลายเป็นแยกต่างหากThreadGroupและสำรวจactiveCount()บนThreadGroupและรอคอยที่จะได้รับ 0

  • ตั้งค่าอินเทอร์เฟซการโทรกลับหรือตัวฟังที่กำหนดเองสำหรับการสื่อสารระหว่างเธรด

ฉันแน่ใจว่ายังมีวิธีอื่นอีกมากมายที่ฉันยังพลาด


1

นี่คือวิธีแก้ปัญหาที่ง่ายสั้นเข้าใจง่ายและเหมาะกับฉัน ฉันต้องการวาดไปที่หน้าจอเมื่อเธรดอื่นสิ้นสุดลง แต่ทำไม่ได้เนื่องจากเธรดหลักมีการควบคุมหน้าจอ ดังนั้น:

(1) ฉันสร้างตัวแปรส่วนกลาง: boolean end1 = false;เธรดตั้งค่าเป็นจริงเมื่อสิ้นสุด ที่หยิบขึ้นมาใน mainthread โดยลูป "postDelayed" ซึ่งจะถูกตอบกลับ

(2) เธรดของฉันประกอบด้วย:

void myThread() {
    end1 = false;
    new CountDownTimer(((60000, 1000) { // milliseconds for onFinish, onTick
        public void onFinish()
        {
            // do stuff here once at end of time.
            end1 = true; // signal that the thread has ended.
        }
        public void onTick(long millisUntilFinished)
        {
          // do stuff here repeatedly.
        }
    }.start();

}

(3) โชคดีที่ "postDelayed" ทำงานในเธรดหลักดังนั้นจึงเป็นจุดที่จะตรวจสอบอีกครั้งในแต่ละวินาที เมื่อเธรดอื่นสิ้นสุดลงสิ่งนี้สามารถเริ่มต้นสิ่งที่เราต้องการทำต่อไป

Handler h1 = new Handler();

private void checkThread() {
   h1.postDelayed(new Runnable() {
      public void run() {
         if (end1)
            // resond to the second thread ending here.
         else
            h1.postDelayed(this, 1000);
      }
   }, 1000);
}

(4) สุดท้ายเริ่มต้นทุกอย่างที่ทำงานในรหัสของคุณโดยโทร:

void startThread()
{
   myThread();
   checkThread();
}

1

ฉันเดาว่าวิธีที่ง่ายที่สุดคือการใช้ThreadPoolExecutorคลาส

  1. มีคิวและคุณสามารถกำหนดจำนวนเธรดที่จะทำงานควบคู่กันได้
  2. มีวิธีการโทรกลับที่ดี:

วิธีการเบ็ด

คลาสนี้จัดเตรียมการป้องกันที่สามารถลบล้างได้beforeExecute(java.lang.Thread, java.lang.Runnable)และafterExecute(java.lang.Runnable, java.lang.Throwable)วิธีการที่เรียกใช้ก่อนและหลังการดำเนินการของแต่ละงาน สิ่งเหล่านี้สามารถใช้เพื่อจัดการกับสภาพแวดล้อมการดำเนินการ ตัวอย่างเช่นการเริ่มต้น ThreadLocals ใหม่การรวบรวมสถิติหรือการเพิ่มรายการบันทึก นอกจากนี้เมธอดterminated()สามารถถูกแทนที่เพื่อดำเนินการประมวลผลพิเศษใด ๆ ที่จำเป็นต้องทำเมื่อ Executor สิ้นสุดลงอย่างสมบูรณ์

ซึ่งเป็นสิ่งที่เราต้องการ เราจะลบล้าง afterExecute()เพื่อรับการโทรกลับหลังจากแต่ละเธรดเสร็จสิ้นและจะลบล้างterminated()เพื่อให้ทราบเมื่อเธรดทั้งหมดเสร็จสิ้น

นี่คือสิ่งที่คุณควรทำ

  1. สร้างตัวดำเนินการ:

    private ThreadPoolExecutor executor;
    private int NUMBER_OF_CORES = Runtime.getRuntime().availableProcessors();    
    
    
    
    private void initExecutor() {
    
    executor = new ThreadPoolExecutor(
            NUMBER_OF_CORES * 2,  //core pool size
            NUMBER_OF_CORES * 2, //max pool size
            60L, //keep aive time
            TimeUnit.SECONDS,
            new LinkedBlockingQueue<Runnable>()
    ) {
    
        @Override
        protected void afterExecute(Runnable r, Throwable t) {
            super.afterExecute(r, t);
                //Yet another thread is finished:
                informUiAboutProgress(executor.getCompletedTaskCount(), listOfUrisToProcess.size());
            }
        }
    
    };
    
        @Override
        protected void terminated() {
            super.terminated();
            informUiThatWeAreDone();
        }
    
    }
  2. และเริ่มหัวข้อของคุณ:

    private void startTheWork(){
        for (Uri uri : listOfUrisToProcess) {
            executor.execute(new Runnable() {
                @Override
                public void run() {
                    doSomeHeavyWork(uri);
                }
            });
        }
        executor.shutdown(); //call it when you won't add jobs anymore 
    }

Inside method informUiThatWeAreDone();ทำทุกอย่างที่คุณต้องทำเมื่อเธรดทั้งหมดเสร็จสิ้นเช่นอัปเดต UI

หมายเหตุ:อย่าลืมใช้synchronizedวิธีการเนื่องจากคุณทำงานควบคู่กันไปและควรระวังอย่างยิ่งหากคุณตัดสินใจที่จะเรียกsynchronizedวิธีการจากsynchronizedวิธีอื่น! สิ่งนี้มักนำไปสู่การหยุดชะงัก

หวังว่านี่จะช่วยได้!


0

คุณยังสามารถใช้ SwingWorker ซึ่งมีการสนับสนุนการเปลี่ยนแปลงคุณสมบัติในตัว ดูaddPropertyChangeListener ()หรือget ()วิธีการสำหรับตัวอย่างตัวฟังการเปลี่ยนแปลงสถานะ


0

ดูเอกสาร Java สำหรับคลาสเธรด คุณสามารถตรวจสอบสถานะของเธรดได้ หากคุณใส่สามเธรดในตัวแปรสมาชิกเธรดทั้งสามจะอ่านสถานะของกันและกันได้

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

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