วิธีรอเธรดให้เสร็จสิ้นด้วย. NET


178

ฉันไม่เคยใช้เธรดมาก่อนใน C # โดยที่ฉันต้องมีสองเธรดรวมถึงเธรด UI หลัก โดยทั่วไปฉันมีดังต่อไปนี้

public void StartTheActions()
{
  //Starting thread 1....
  Thread t1 = new Thread(new ThreadStart(action1));
  t1.Start();

  // Now, I want for the main thread (which is calling `StartTheActions` method) 
  // to wait for `t1` to finish. I've created an event in `action1` for this. 
  // The I wish `t2` to start...

  Thread t2 = new Thread(new ThreadStart(action2));
  t2.Start();
}

ดังนั้นโดยพื้นฐานแล้วคำถามของฉันคือการมีเธรดรอให้อีกเธรดหนึ่งจบได้อย่างไร วิธีที่ดีที่สุดในการทำเช่นนี้คืออะไร?


4
หากคุณ "... ไม่เคยใช้เธรดมาก่อน ... " คุณอาจต้องการพิจารณาทางเลือกที่ง่ายกว่าเช่นรูปแบบ * Async ()
Ðаn

4
หากคุณกำลังรอให้เธรด 1 ดำเนินการเสร็จสิ้นทำไมคุณไม่เรียกใช้วิธีการนั้นพร้อมกัน
Svish

13
การใช้เธรดเมื่อคุณประมวลผลในลักษณะเชิงเส้นคืออะไร
จอห์น

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

คำตอบของ Rotemโดยใช้ backgroundworker เพื่อการใช้งานที่ง่ายมันง่ายมาก
Kamil

คำตอบ:


264

ฉันเห็นตัวเลือก 5 ตัวเลือก:

1. ด้ายเข้าร่วม

เช่นเดียวกับคำตอบของมิทช์ แต่สิ่งนี้จะบล็อกเธรด UI ของคุณอย่างไรก็ตามคุณจะได้รับไทม์เอาต์ในตัวสำหรับคุณ


2. ใช้ WaitHandle

ManualResetEventเป็นWaitHandleเป็น jrista ปัญหา

สิ่งหนึ่งที่ควรทราบคือถ้าคุณต้องการรอหลายเธรดWaitHandle.WaitAll()จะไม่ทำงานตามค่าเริ่มต้นเนื่องจากต้องใช้เธรด MTA คุณสามารถแก้ไขปัญหานี้ได้โดยทำเครื่องหมายMain()วิธีการของคุณด้วยMTAThread- อย่างไรก็ตามสิ่งนี้จะบล็อกปั๊มข้อความของคุณและไม่แนะนำจากสิ่งที่ฉันอ่าน


3. เริ่มเหตุการณ์

ดูหน้านี้โดย Jon Skeetเกี่ยวกับกิจกรรมและมัลติเธรดอาจเป็นไปได้ว่าเหตุการณ์หนึ่งอาจไม่สามารถอธิบายได้ระหว่าง- ifและEventName(this,EventArgs.Empty)- มันเคยเกิดขึ้นกับฉันมาก่อน

(หวังว่ารวบรวมเหล่านี้ฉันไม่ได้ลอง)

public class Form1 : Form
{
    int _count;

    void ButtonClick(object sender, EventArgs e)
    {
        ThreadWorker worker = new ThreadWorker();
        worker.ThreadDone += HandleThreadDone;

        Thread thread1 = new Thread(worker.Run);
        thread1.Start();

        _count = 1;
    }

    void HandleThreadDone(object sender, EventArgs e)
    {
        // You should get the idea this is just an example
        if (_count == 1)
        {
            ThreadWorker worker = new ThreadWorker();
            worker.ThreadDone += HandleThreadDone;

            Thread thread2 = new Thread(worker.Run);
            thread2.Start();

            _count++;
        }
    }

    class ThreadWorker
    {
        public event EventHandler ThreadDone;

        public void Run()
        {
            // Do a task

            if (ThreadDone != null)
                ThreadDone(this, EventArgs.Empty);
        }
    }
}

4. ใช้ผู้รับมอบสิทธิ์

public class Form1 : Form
{
    int _count;

    void ButtonClick(object sender, EventArgs e)
    {
        ThreadWorker worker = new ThreadWorker();

        Thread thread1 = new Thread(worker.Run);
        thread1.Start(HandleThreadDone);

        _count = 1;
    }

    void HandleThreadDone()
    {
        // As before - just a simple example
        if (_count == 1)
        {
            ThreadWorker worker = new ThreadWorker();

            Thread thread2 = new Thread(worker.Run);
            thread2.Start(HandleThreadDone);

            _count++;
        }
    }

    class ThreadWorker
    {
        // Switch to your favourite Action<T> or Func<T>
        public void Run(object state)
        {
            // Do a task

            Action completeAction = (Action)state;
            completeAction.Invoke();
        }
    }
}

หากคุณใช้เมธอด _count อาจเป็นความคิด (เพื่อความปลอดภัย) เพื่อเพิ่มโดยใช้

Interlocked.Increment(ref _count)

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


5. ทำแบบอะซิงโครนัสแทน

คำตอบสำหรับคำถามนี้มีคำอธิบายที่ชัดเจนเกี่ยวกับตัวเลือกของคุณด้วยวิธีนี้


มอบหมาย / เหตุการณ์บนเธรดที่ไม่ถูกต้อง

วิธีการจัดกิจกรรม / ผู้รับมอบสิทธิ์จะหมายถึงวิธีการจัดการเหตุการณ์ของคุณอยู่ใน thread1 / thread2 ไม่ใช่เธรด UI หลักดังนั้นคุณจะต้องสลับกลับไปที่ด้านบนสุดของวิธีการ HandleThreadDone:

// Delegate example
if (InvokeRequired)
{
    Invoke(new Action(HandleThreadDone));
    return;
}

61

เพิ่ม

t1.Join();    // Wait until thread t1 finishes

หลังจากที่คุณเริ่มมัน แต่มันจะไม่สำเร็จเท่าที่ควรเพราะผลลัพธ์เดียวกันกับการวิ่งบนเธรดหลัก!

ฉันขอแนะนำให้อ่านThreadingของ Joe Albahari ใน e-book e-book ฟรีหากคุณต้องการทำความเข้าใจกับการทำเกลียวใน. NET


4
แม้ว่าจะJoinเป็นสิ่งที่ผู้ถามดูเหมือนจะได้รับการขอสิ่งนี้อาจไม่ดีอย่างยิ่งโดยทั่วไป การเรียกไปยังJoinจะวางเธรดซึ่งสิ่งนี้กำลังทำอยู่ ถ้าเกิดว่าเป็นเธรด GUI หลักนี่คือBAD ! ในฐานะผู้ใช้ฉันเกลียดแอปพลิเคชันที่ดูเหมือนจะทำงานอย่างนี้ ดังนั้นโปรดดูคำตอบอื่น ๆ ทั้งหมดสำหรับคำถามนี้และstackoverflow.com/questions/1221374/…
peSHIr

1
ฉันยอมรับว่าโดยทั่วไปเข้าร่วม () ไม่ดี ฉันอาจจะไม่ได้คำตอบที่ชัดเจนเพียงพอในคำตอบของฉัน
มิทช์ข้าวสาลี

2
Guys, หนึ่งขนาดไม่พอดีทั้งหมด มีสถานการณ์เมื่อต้องแน่ใจว่าเธรดนั้นทำงานเสร็จแล้ว: พิจารณาว่าเธรดประมวลผลข้อมูลซึ่งกำลังจะเปลี่ยนแปลง ในกรณีเช่นนี้การแจ้งให้เธรดยกเลิกอย่างสง่างามและรอจนกว่าจะเสร็จสิ้น (โดยเฉพาะเมื่อประมวลผลหนึ่งขั้นตอนเร็วมาก) IMO เป็นธรรมอย่างสมบูรณ์ ฉันอยากจะบอกว่าการเข้าร่วมเป็นสิ่งชั่วร้าย (ในข้อกำหนดของ C ++ คำถามที่พบบ่อย) เช่น มันจะไม่ถูกนำมาใช้เว้นแต่จำเป็นจริงๆ
Spook

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

2
เข้าร่วมเป็นเครื่องมือหรือไม่ ฉันคิดว่าคุณจะพบว่ามันเป็นวิธีการ
มิทช์ข้าวสาลี

32

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

public void StartTheActions()
{
    ManualResetEvent syncEvent = new ManualResetEvent(false);

    Thread t1 = new Thread(
        () =>
        {
            // Do some work...
            syncEvent.Set();
        }
    );
    t1.Start();

    Thread t2 = new Thread(
        () =>
        {
            syncEvent.WaitOne();

            // Do some work...
        }
    );
    t2.Start();
}

ManualResetEventเป็นหนึ่งในหลาย ๆWaitHandleที่. NET Framework มีให้ พวกเขาสามารถให้ความสามารถในการซิงโครไนซ์เธรดที่สมบูรณ์ยิ่งขึ้นกว่าเครื่องมือที่เรียบง่าย แต่พบบ่อยมากเช่น lock () / Monitor, Thread.Join เป็นต้นนอกจากนี้ยังสามารถใช้ซิงโครไนซ์มากกว่าสองเธรดได้ ที่เชื่อมโยงหลาย ๆ หัวข้อ 'child' หลาย ๆ กระบวนการที่เกิดขึ้นพร้อมกันซึ่งขึ้นอยู่กับหลายขั้นตอนของกันและกันที่จะซิงโครไนซ์ ฯลฯ


30

หากใช้จาก. NET 4 ตัวอย่างนี้สามารถช่วยคุณได้:

class Program
{
    static void Main(string[] args)
    {
        Task task1 = Task.Factory.StartNew(() => doStuff());
        Task task2 = Task.Factory.StartNew(() => doStuff());
        Task task3 = Task.Factory.StartNew(() => doStuff());
        Task.WaitAll(task1, task2, task3);
        Console.WriteLine("All threads complete");
    }

    static void doStuff()
    {
        //do stuff here
    }
}

จาก: https://stackoverflow.com/a/4190969/1676736


8
คุณไม่ได้พูดถึงหัวข้อใดในคำตอบของคุณ คำถามเกี่ยวกับเธรดไม่ใช่งาน ทั้งสองไม่เหมือนกัน
Suamere

7
ฉันมาพร้อมกับคำถามที่คล้ายกัน (และระดับความรู้) กับโปสเตอร์ต้นฉบับและคำตอบนี้มีค่ามากสำหรับฉัน - งานมีวิธีที่เหมาะสมกว่าสำหรับสิ่งที่ฉันทำและถ้าฉันไม่พบคำตอบนี้ฉันจะเขียน สระว่ายน้ำด้ายของตัวเองที่น่ากลัว
Chris Rae

1
@ChrisRae ดังนั้นนี่ควรเป็นความคิดเห็นในคำถามเดิมไม่ใช่คำตอบเช่นนี้
Jaime Hablutzel

เกี่ยวกับคำตอบเฉพาะนี้ฉันคิดว่ามันน่าจะสมเหตุสมผลมากกว่านี้
Chris Rae

1
ตามที่ @Samamere กล่าวว่าคำตอบนี้ไม่เกี่ยวข้องกับคำถาม OP ทั้งหมด
Glenn Slayden


4

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


0

ลองสิ่งนี้:

List<Thread> myThreads = new List<Thread>();

foreach (Thread curThread in myThreads)
{
    curThread.Start();
}

foreach (Thread curThread in myThreads)
{
    curThread.Join();
}

0

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

while (threadCounter > 0)
{
    Thread.Sleep(500); //Make it pause for half second so that we don’t spin the cpu out of control.
}

บันทึกไว้ในบล็อกของฉัน http://www.adamthings.com/post/2012/07/11/ensure-threads-have-finished-before-method-continues-in-c/


4
สิ่งนี้เรียกว่าการรอที่ไม่ว่าง ใช่มันใช้งานได้และบางครั้งก็เป็นทางออกที่ดีที่สุด แต่คุณต้องการหลีกเลี่ยงถ้าเป็นไปได้เพราะทำให้เสียเวลา CPU
MobileMon

@MobileMon เป็นแนวทางมากกว่ากฎ ในกรณีนี้เนื่องจากลูปนั้นอาจเสีย CPU 0.00000001% และ OP กำลังเข้ารหัสใน C # การแทนที่สิ่งนี้ด้วยบางสิ่งที่ 'มีประสิทธิภาพมากขึ้น' จะเป็นการเสียเวลาโดยสิ้นเชิง กฎการเพิ่มประสิทธิภาพข้อแรกคือ - ไม่ วัดก่อน
Spike0xff

0

เมื่อฉันต้องการให้ UI สามารถอัปเดตจอแสดงผลในขณะที่รองานให้เสร็จฉันใช้ห่วงวนขณะทดสอบ IsAlive บนเธรด:

    Thread t = new Thread(() => someMethod(parameters));
    t.Start();
    while (t.IsAlive)
    {
        Thread.Sleep(500);
        Application.DoEvents();
    }

-1

นี่คือตัวอย่างง่ายๆที่รอให้ดอกยางเสร็จภายในชั้นเดียวกัน นอกจากนี้ยังทำการเรียกคลาสอื่นในเนมสเปซเดียวกัน ฉันรวมคำสั่ง "using" เพื่อให้สามารถใช้งานเป็น Winform ได้ตราบใดที่คุณสร้าง button1

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Threading;

namespace ClassCrossCall
{

 public partial class Form1 : Form
 {
  int number = 0; // this is an intentional problem, included for demonstration purposes
  public Form1()
  {
   InitializeComponent();
  }
  private void Form1_Load(object sender, EventArgs e)
  {
   button1.Text="Initialized";
  }
  private void button1_Click(object sender, EventArgs e)
  {
   button1.Text="Clicked";
   button1.Refresh();
   Thread.Sleep(400);
   List<Task> taskList = new List<Task>();
   taskList.Add(Task.Factory.StartNew(() => update_thread(2000)));
   taskList.Add(Task.Factory.StartNew(() => update_thread(4000)));
   Task.WaitAll(taskList.ToArray());
   worker.update_button(this,number);
  }
  public void update_thread(int ms)
  {
   // It's important to check the scope of all variables
   number=ms; // this could be either 2000 or 4000.  Race condition.
   Thread.Sleep(ms);
  }
 }

 class worker
 {
  public static void update_button(Form1 form, int number)
  {
   form.button1.Text=$"{number}";
  }
 }
}
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.