Làm thế nào để chờ đợi chủ đề kết thúc với .NET?


178

Tôi chưa bao giờ thực sự sử dụng luồng trước đây trong C # khi tôi cần có hai luồng, cũng như luồng UI chính. Về cơ bản, tôi có những điều sau đây.

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();
}

Vì vậy, về cơ bản, câu hỏi của tôi là làm thế nào để có một chủ đề chờ đợi một chủ đề khác kết thúc. Cách tốt nhất để làm việc này là gì?


4
Nếu bạn "... chưa bao giờ sử dụng luồng trước đây ...", bạn có thể muốn xem xét một số lựa chọn thay thế đơn giản hơn như mẫu * Async ().
Ðаn

4
Nếu bạn chỉ chờ đợi chủ đề 1 hoàn thành dù sao đi nữa, tại sao bạn không chỉ gọi phương thức đó một cách đồng bộ?
Svish

13
Điểm quan trọng trong việc sử dụng các luồng khi bạn xử lý theo kiểu tuyến tính là gì?
Giăng

1
@ John, nó hoàn toàn có ý nghĩa với tôi rằng có nhiều cách sử dụng để quay ra một luồng nền hoạt động trong khi người dùng làm việc. Ngoài ra, không phải câu hỏi của bạn giống như câu hỏi trước?
dùng34660

Câu trả lời của Rotem , sử dụng công cụ nền để sử dụng dễ dàng, rất đơn giản.
Kamil

Câu trả lời:


264

Tôi có thể thấy 5 tùy chọn có sẵn:

1. Chủ đề.

Như với câu trả lời của Mitch. Nhưng điều này sẽ chặn luồng UI của bạn, tuy nhiên bạn sẽ có Thời gian chờ tích hợp sẵn cho bạn.


2. Sử dụng một WaitHandle

ManualResetEventlà một WaitHandlenhư jrista đề nghị.

Một điều cần lưu ý là nếu bạn muốn đợi nhiều luồng, WaitHandle.WaitAll()sẽ không hoạt động theo mặc định, vì nó cần một luồng MTA. Bạn có thể khắc phục điều này bằng cách đánh dấu Main()phương pháp của mình bằng MTAThread- tuy nhiên điều này chặn bơm tin nhắn của bạn và không được đề xuất từ ​​những gì tôi đã đọc.


3. Bắn một sự kiện

Xem trang này của Jon Skeet về các sự kiện và đa luồng, có thể một sự kiện có thể trở nên không được mô tả giữa ifEventName(this,EventArgs.Empty)- nó đã xảy ra với tôi trước đây.

(Hy vọng những biên dịch này, tôi chưa thử)

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. Sử dụng một đại biểu

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();
        }
    }
}

Nếu bạn sử dụng phương thức _count, có thể là một ý tưởng (để an toàn) để tăng nó bằng cách sử dụng

Interlocked.Increment(ref _count)

Tôi muốn biết sự khác biệt giữa việc sử dụng các đại biểu và sự kiện cho thông báo luồng, sự khác biệt duy nhất tôi biết là các sự kiện được gọi là đồng bộ.


5. Thay vào đó hãy thực hiện không đồng bộ

Câu trả lời cho câu hỏi này có một mô tả rất rõ ràng về các lựa chọn của bạn với phương pháp này.


Đại biểu / Sự kiện trên chuỗi sai

Cách làm việc của sự kiện / ủy nhiệm sẽ có nghĩa là phương thức xử lý sự kiện của bạn nằm trên thread1 / thread2 không phải là luồng UI chính , vì vậy bạn sẽ cần phải quay lại ngay ở đầu các phương thức HandleThreadDone:

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

61

Thêm vào

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

sau khi bạn khởi động nó, nhưng điều đó sẽ không thực hiện được nhiều vì nó là kết quả tương tự như chạy trên luồng chính!

Tôi rất có thể khuyên bạn nên đọc Threading của Joe Albahari trong sách điện tử miễn phí C # , nếu bạn muốn hiểu về luồng trong .NET.


4
Mặc dù theo Joinnghĩa đen là những gì người hỏi dường như đã yêu cầu, nói chung điều này có thể cực kỳ tồi tệ. Một cuộc gọi đến Joinsẽ treo chuỗi mà từ đó việc này được thực hiện. Nếu đó là chủ đề GUI chính, thì đây là BAD ! Là một người dùng, tôi chủ động ghét các ứng dụng dường như hoạt động theo cách này. Vì vậy, vui lòng xem tất cả các câu trả lời khác cho câu hỏi này và stackoverflow.com/questions/1221374/ trên
peSHIr

1
Tôi đồng ý rằng nói chung Tham gia () là xấu. Tôi có lẽ đã không làm cho điều đó đủ rõ ràng trong câu trả lời của tôi.
Mitch Wheat

2
Các bạn, một kích thước không phù hợp với tất cả . Có những tình huống, khi một người thực sự cần chắc chắn, luồng đó đã hoàn thành công việc của nó: xem xét, rằng luồng xử lý dữ liệu, sắp bị thay đổi. Trong trường hợp như vậy, thông báo cho chủ đề hủy bỏ một cách duyên dáng và chờ cho đến khi nó kết thúc (đặc biệt, khi một bước được xử lý rất nhanh) thì IMO hoàn toàn hợp lý. Tôi muốn nói rằng, Tham gia là xấu xa (theo thuật ngữ C ++ FAQ), nghĩa là. nó sẽ không được sử dụng trừ khi thực sự cần thiết
Spook

5
Tôi muốn nói rõ rằng, Tham gia là một công cụ, có thể hữu ích, mặc dù thực tế, nó rất thường bị sử dụng sai. Có một số tình huống, khi nó sẽ chỉ hoạt động mà không có bất kỳ tác dụng phụ không cần thiết nào (chẳng hạn như xử lý chủ đề GUI chính trong một khoảng thời gian đáng chú ý).
Spook

2
Tham gia là một công cụ? Tôi nghĩ bạn sẽ thấy đó là một phương pháp.
Mitch Wheat

32

Hai câu trả lời trước là tuyệt vời, và sẽ hoạt động cho các kịch bản đơn giản. Tuy nhiên, có nhiều cách khác để đồng bộ hóa chủ đề. Sau đây cũng sẽ làm việc:

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 là một trong những WaitHandle khác nhau mà .NET framework phải cung cấp. Chúng có thể cung cấp khả năng đồng bộ hóa luồng phong phú hơn nhiều so với các công cụ đơn giản nhưng rất phổ biến như lock () / Monitor, Thread.Join, v.v. Chúng cũng có thể được sử dụng để đồng bộ hóa nhiều hơn hai luồng, cho phép các kịch bản phức tạp như luồng 'chính' điều phối nhiều luồng 'con', nhiều tiến trình đồng thời phụ thuộc vào một số giai đoạn của nhau để được đồng bộ hóa, v.v.


30

Nếu sử dụng từ .NET 4, mẫu này có thể giúp bạn:

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
    }
}

từ: https://stackoverflow.com/a/4190969/1676736


8
Bạn đã không đề cập bất cứ điều gì về Chủ đề trong câu trả lời của bạn. Câu hỏi là về Chủ đề, không phải Nhiệm vụ. Hai là không giống nhau.
Suamere

7
Tôi đã đưa ra một câu hỏi tương tự (và trình độ kiến ​​thức) cho người đăng ban đầu và câu trả lời này rất có giá trị đối với tôi - nhiệm vụ phù hợp hơn với những gì tôi đang làm và nếu tôi không tìm thấy câu trả lời này, tôi đã viết sở hữu chủ đề khủng khiếp.
Chris Rae

1
@ChrisRae, vì vậy đây nên là một nhận xét trong câu hỏi ban đầu, không phải là một câu trả lời như câu hỏi này.
Jaime Hablutzel

Vì đó là về câu trả lời cụ thể này, tôi nghĩ rằng nó có thể có ý nghĩa hơn ở đây.
Chris Rae

1
như @Suamere đã nói, câu trả lời này hoàn toàn không liên quan đến câu hỏi của OP.
Glenn Slayden


4

Tôi sẽ có chủ đề chính của bạn chuyển một phương thức gọi lại cho chủ đề đầu tiên của bạn và khi hoàn thành, nó sẽ gọi phương thức gọi lại trên chương trình bảo trì, có thể khởi chạy chủ đề thứ hai. Điều này giữ cho chủ đề chính của bạn không bị treo trong khi nó chờ tham gia hoặc tham gia. Truyền các phương thức làm đại biểu là một điều hữu ích để học với C #.


0

Thử cái này:

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

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

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

0

Đăng bài để có thể giúp đỡ một số người khác, dành khá nhiều thời gian để tìm kiếm một giải pháp như những gì tôi nghĩ ra. Vì vậy, tôi đã có một cách tiếp cận khác nhau. Có một tùy chọn truy cập ở trên, tôi chỉ áp dụng nó một chút khác nhau. Tôi đã quay cuồng với nhiều luồng và tăng một bộ đếm và giảm một bộ đếm khi một chuỗi bắt đầu và dừng lại. Sau đó, trong phương thức chính tôi muốn tạm dừng và chờ đợi các chủ đề hoàn thành tôi đã làm.

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

Tài liệu trên blog của tôi. http://www.adamthings.com/post/2012/07/11/ensure-threads-have-finished-b Before-method-content-in-c /


4
Điều này được gọi là chờ đợi bận rộn. Có, nó hoạt động và đôi khi là giải pháp tốt nhất, nhưng bạn muốn tránh nó nếu có thể vì nó làm lãng phí thời gian của CPU
MobileMon

@MobileMon đó là một hướng dẫn nhiều hơn là một quy tắc. Trong trường hợp này vì vòng lặp đó có thể lãng phí 0,00000001% CPU và OP đang mã hóa bằng C #, thay thế điều này bằng thứ gì đó 'hiệu quả hơn' sẽ hoàn toàn lãng phí thời gian. Nguyên tắc tối ưu hóa đầu tiên là - không. Đo lường trước.
Spike0xff

0

Khi tôi muốn UI có thể cập nhật màn hình của nó trong khi chờ tác vụ hoàn thành, tôi sử dụng vòng lặp while để kiểm tra IsAlive trên luồng:

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

-1

Đây là một ví dụ đơn giản chờ đợi một bước đi kết thúc, trong cùng một lớp. Nó cũng thực hiện một cuộc gọi đến một lớp khác trong cùng một không gian tên. Tôi đã bao gồm các câu lệnh "bằng cách sử dụng" để nó có thể thực thi dưới dạng Winform miễn là bạn tạo nút1.

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}";
  }
 }
}
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.