Tại sao Thread. Ngủ rất có hại


128

Tôi thường thấy nó được đề cập rằng Thread.Sleep();không nên sử dụng, nhưng tôi không thể hiểu tại sao nó lại như vậy. Nếu Thread.Sleep();có thể gây rắc rối, có giải pháp thay thế nào có cùng kết quả sẽ an toàn không?

ví dụ.

while(true)
{
    doSomework();
    i++;
    Thread.Sleep(5000);
}

một số khác là:

while (true)
{
    string[] images = Directory.GetFiles(@"C:\Dir", "*.png");

    foreach (string image in images)
    {
        this.Invoke(() => this.Enabled = true);
        pictureBox1.Image = new Bitmap(image);
        Thread.Sleep(1000);
    }
}

3
Một bản tóm tắt của blog có thể là 'đừng lạm dụng Thread.s ngủ ()'.
Martin James

5
Tôi sẽ không nói nó có hại. Tôi muốn nói rằng nó giống như goto:có nghĩa là có một giải pháp tốt hơn cho các vấn đề của bạn hơn Sleep.
Mặc định

9
Nó không hoàn toàn giống như goto, giống như mùi mã hơn là mùi thiết kế. Không có gì sai với trình biên dịch chèn gotos vào mã của bạn: máy tính không bị lẫn lộn. Nhưng Thread.Sleepkhông hoàn toàn giống nhau; trình biên dịch không chèn cuộc gọi đó và nó có những hậu quả tiêu cực khác. Nhưng vâng, quan điểm chung rằng sử dụng nó là sai bởi vì hầu như luôn luôn có một giải pháp tốt hơn chắc chắn là chính xác.
Cody Grey

31
Mọi người đều đưa ra ý kiến ​​về lý do tại sao các ví dụ trên là xấu, nhưng không ai cung cấp phiên bản viết lại mà không sử dụng Thread.S ngủ () vẫn hoàn thành mục tiêu của các ví dụ đã cho.
StingyJack

3
Mỗi khi bạn nhập sleep()mã (hoặc kiểm tra), một con chó con sẽ chết
Reza S

Câu trả lời:


163

Các vấn đề với cuộc gọi Thread.Sleepđược giải thích khá ngắn gọn ở đây :

Thread.Sleepcó công dụng của nó: mô phỏng các hoạt động dài trong khi thử nghiệm / gỡ lỗi trên một luồng MTA. Trong .NET không có lý do nào khác để sử dụng nó.

Thread.Sleep(n)có nghĩa là chặn luồng hiện tại ít nhất là số lượng thời gian (hoặc lượng tử luồng) có thể xảy ra trong vòng một n phần nghìn giây. Độ dài của thời gian là khác nhau trên các phiên bản / loại Windows khác nhau và các bộ xử lý khác nhau và thường dao động từ 15 đến 30 mili giây. Điều này có nghĩa là luồng gần như được đảm bảo để chặn trong hơn một nphần nghìn giây. Khả năng chủ đề của bạn sẽ được đánh thức lại chính xác sau nmili giây là điều không thể nhất có thể. Vì vậy, Thread.Sleeplà vô nghĩa cho thời gian .

Chủ đề là một nguồn tài nguyên hạn chế, họ mất khoảng 200.000 chu kỳ để tạo và khoảng 100.000 chu kỳ để phá hủy. Theo mặc định, họ dự trữ 1 megabyte bộ nhớ ảo cho ngăn xếp của nó và sử dụng 2.000-8.000 chu kỳ cho mỗi chuyển đổi ngữ cảnh. Điều này làm cho bất kỳ chủ đề chờ đợi là một sự lãng phí rất lớn .

Giải pháp ưa thích: WaitHandles

Lỗi nhiều nhất là sử dụng Thread.Sleepvới một cấu trúc while ( bản demo và câu trả lời , mục blog đẹp )

EDIT:
Tôi muốn nâng cao câu trả lời của tôi:

Chúng tôi có 2 trường hợp sử dụng khác nhau:

  1. Chúng tôi đang chờ đợi bởi vì chúng ta đều biết một khoảng thời gian cụ thể khi chúng ta nên tiếp tục (sử dụng Thread.Sleep, System.Threading.Timerhoặc alikes)

  2. Chúng tôi đang chờ đợi vì một số điều kiện thay đổi một lúc nào đó ... từ khóa là / là một thời gian ! nếu kiểm tra điều kiện nằm trong miền mã của chúng ta, chúng ta nên sử dụng WaitHandles - nếu không thì thành phần bên ngoài sẽ cung cấp một số loại móc nối ... nếu thiết kế của nó không tệ!

Câu trả lời của tôi chủ yếu bao gồm trường hợp sử dụng 2


29
Tôi sẽ không gọi 1 MB bộ nhớ là một sự lãng phí lớn khi xem xét phần cứng ngày nay
Mặc định

14
@Default này, thảo luận với tác giả gốc :) và, nó luôn phụ thuộc vào mã của bạn - hoặc tốt hơn: yếu tố ... và vấn đề chính là "Chủ đề là tài nguyên hạn chế" - ngày nay trẻ em không biết nhiều về hiệu quả và chi phí của một số triển khai nhất định, bởi vì "phần cứng là rẻ" ... nhưng đôi khi bạn cần mã hóa rất optd
Andreas Niedermair

11
'Điều này làm cho bất kỳ chủ đề chờ đợi là một sự lãng phí rất lớn' eh? Nếu một số thông số giao thức yêu cầu tạm dừng một giây trước khi tiếp tục, điều gì sẽ chờ trong 1 giây? Một số chủ đề, ở đâu đó, sẽ phải chờ đợi! Chi phí cho việc tạo / hủy luồng thường không liên quan vì dù sao thì một luồng phải được nâng lên vì những lý do khác và nó chạy trong suốt vòng đời của quá trình. Tôi sẽ quan tâm để xem bất kỳ cách nào để tránh chuyển đổi ngữ cảnh khi thông số kỹ thuật nói 'sau khi bật máy bơm, đợi ít nhất mười giây để áp suất ổn định trước khi mở van nạp'.
Martin James

9
@CodyGray - Tôi đọc lại bài viết. Tôi thấy không có con cá nào màu sắc trong bình luận của tôi. Andreas rút ra khỏi web: 'Thread.S ngủ có sử dụng: mô phỏng các hoạt động dài trong khi thử nghiệm / gỡ lỗi trên một luồng MTA. Trong .NET không có lý do nào khác để sử dụng nó '. Tôi lập luận rằng có rất nhiều ứng dụng trong đó một cuộc gọi ngủ () là tốt, chỉ là những gì được yêu cầu. Nếu các nhà phát triển, (có rất nhiều), khăng khăng sử dụng các vòng lặp ngủ () như các trình theo dõi điều kiện nên được thay thế bằng các sự kiện / condvars / semas / bất cứ điều gì, thì đó không phải là lý do để khẳng định rằng 'không có lý do nào khác để sử dụng nó '.
Martin James

8
Trong 30 năm phát triển ứng dụng nhiều lần, (chủ yếu là C ++ / Delphi / Windows), tôi chưa bao giờ thấy bất kỳ nhu cầu nào về vòng lặp ngủ (0) hoặc ngủ (1) trong bất kỳ mã có thể phân phối nào. Thỉnh thoảng, tôi đã sử dụng mã như vậy cho mục đích gỡ lỗi, nhưng nó không bao giờ đến được với khách hàng. 'Nếu bạn đang viết một cái gì đó không hoàn toàn kiểm soát mọi luồng' - quản lý vi mô của các luồng cũng là một sai lầm lớn như quản lý vi mô của nhân viên phát triển. Quản lý luồng là những gì hệ điều hành dành cho - các công cụ mà nó cung cấp nên được sử dụng.
Martin James

34

SCENARIO 1 - chờ hoàn thành tác vụ không đồng bộ: Tôi đồng ý rằng WaitHandle / Auto | ManualResetEvent nên được sử dụng trong trường hợp trong đó một luồng đang chờ tác vụ trên luồng khác hoàn thành.

SCENARIO 2 - vòng lặp thời gian trong khi: Tuy nhiên, như một cơ chế thời gian thô (while + Thread.S ngủ) hoàn toàn tốt cho 99% ứng dụng KHÔNG yêu cầu biết chính xác khi nào Thread bị chặn sẽ "đánh thức *. Chu kỳ 200k để tạo luồng cũng không hợp lệ - dù sao thì luồng vòng lặp thời gian cũng cần được tạo và chu kỳ 200k chỉ là một số lớn khác (cho tôi biết có bao nhiêu chu kỳ để mở một cuộc gọi tệp / socket / db?).

Vì vậy, nếu while + Thread.S ngủ hoạt động, tại sao mọi thứ lại phức tạp? Chỉ có luật sư cú pháp sẽ, được thực tế !


Rất may, bây giờ chúng tôi có TaskCompletionSource để chờ kết quả một cách hiệu quả.
Austin Salgat 27/03/18

13

Tôi muốn trả lời câu hỏi này từ góc độ chính trị mã hóa, có thể có hoặc không hữu ích cho bất kỳ ai. Nhưng đặc biệt khi bạn đang xử lý các công cụ dành cho 9-5 lập trình viên của công ty, những người viết tài liệu có xu hướng sử dụng các từ như "không nên" và "không bao giờ" có nghĩa là "đừng làm điều này trừ khi bạn thực sự biết bạn là gì đang làm và tại sao ".

Một vài mục yêu thích khác của tôi trong thế giới C # là họ bảo bạn "không bao giờ gọi khóa (cái này)" hoặc "không bao giờ gọi GC.Collect ()". Hai cái này được tuyên bố mạnh mẽ trong nhiều blog và tài liệu chính thức, và IMO là thông tin sai lệch hoàn toàn. Ở một mức độ nào đó, thông tin sai lệch này phục vụ mục đích của nó, ở chỗ nó khiến người mới bắt đầu không làm những việc họ không hiểu trước khi nghiên cứu đầy đủ các lựa chọn thay thế, nhưng đồng thời, rất khó tìm thấy thông tin THỰC SỰ thông qua các công cụ tìm kiếm dường như chỉ ra những bài báo nói với bạn rằng đừng làm điều gì đó trong khi không đưa ra câu trả lời cho câu hỏi "tại sao không?"

Về mặt chính trị, nó tập trung vào những gì mọi người cho là "thiết kế tốt" hay "thiết kế xấu". Tài liệu chính thức không nên ra lệnh cho thiết kế ứng dụng của tôi. Nếu thực sự có một lý do kỹ thuật mà bạn không nên gọi là ngủ (), thì IMO tài liệu sẽ nói rằng việc gọi nó theo các tình huống cụ thể là hoàn toàn ổn, nhưng có thể đưa ra một số giải pháp thay thế độc lập hoặc phù hợp hơn với kịch bản khác kịch bản.

Gọi rõ ràng "ngủ ()" rất hữu ích trong nhiều tình huống khi thời hạn được xác định rõ ràng theo thuật ngữ thời gian thực, tuy nhiên, có nhiều hệ thống tinh vi hơn để chờ đợi và báo hiệu các luồng cần được xem xét và hiểu trước khi bạn bắt đầu ném ngủ ( ) vào mã của bạn và ném các câu lệnh ngủ () không cần thiết vào mã của bạn thường được coi là chiến thuật của người mới bắt đầu.


5

Đó là vòng lặp 1) .spinning và 2) .polling các ví dụ của bạn mà mọi người thận trọng , không phải là phần Thread.S ngủ (). Tôi nghĩ Thread.S ngủ () thường được thêm vào để dễ dàng cải thiện mã đang quay hoặc trong vòng lặp bỏ phiếu, vì vậy nó chỉ được liên kết với mã "xấu".

Ngoài ra, mọi người làm những việc như:

while(inWait)Thread.Sleep(5000); 

trong đó biến inWait không được truy cập theo cách an toàn luồng, điều này cũng gây ra sự cố.

Những gì lập trình viên muốn thấy là các luồng được điều khiển bởi các cấu trúc Sự kiện và Báo hiệu và Khóa, và khi bạn làm điều đó, bạn sẽ không cần Thread.S ngủ () và những lo ngại về truy cập biến an toàn của luồng cũng bị loại bỏ. Ví dụ, bạn có thể tạo một trình xử lý sự kiện được liên kết với lớp FileSystemWatcher và sử dụng một sự kiện để kích hoạt ví dụ thứ 2 của bạn thay vì lặp không?

Như Andreas N. đã đề cập, đọc Threading trong C #, bởi Joe Albahari , nó thực sự rất tốt.


Vì vậy, sự thay thế để làm những gì trong ví dụ mã của bạn là gì?
shinzou

kuhaku - Vâng, câu hỏi hay. Rõ ràng Thread.S ngủ () là một phím tắt, và đôi khi là một phím tắt lớn. Đối với ví dụ trên, phần còn lại của mã trong hàm bên dưới vòng bỏ phiếu "while (inWait) Thread.S ngủ (5000);" là một hàm mới. Chức năng mới đó là một đại biểu (chức năng gọi lại) và bạn chuyển nó cho bất cứ điều gì đang đặt cờ "inWait" và thay vì thay đổi cờ "inWait", cuộc gọi lại được gọi. Đây là ví dụ ngắn nhất tôi có thể tìm thấy: myelin.co.nz/notes/callbacks/cs-delegates.html
mike

Chỉ để chắc chắn rằng tôi đã nhận nó, bạn có nghĩa là bọc Thread.Sleep()với một chức năng khác và gọi nó trong vòng lặp while?
shinzou

5

Ngủ được sử dụng trong trường hợp (các) chương trình độc lập mà bạn không kiểm soát được đôi khi có thể sử dụng tài nguyên thường được sử dụng (giả sử là tệp), mà chương trình của bạn cần truy cập khi chạy và khi tài nguyên được sử dụng bởi các chương trình khác chương trình của bạn bị chặn sử dụng nó. Trong trường hợp này, khi bạn truy cập tài nguyên trong mã của mình, bạn đặt quyền truy cập tài nguyên của mình vào một lần thử (để bắt ngoại lệ khi bạn không thể truy cập tài nguyên) và bạn đặt điều này trong một vòng lặp. Nếu tài nguyên là miễn phí, giấc ngủ không bao giờ được gọi. Nhưng nếu tài nguyên bị chặn, thì bạn sẽ ngủ trong một khoảng thời gian thích hợp và cố gắng truy cập lại tài nguyên đó (đây là lý do tại sao bạn đang lặp). Tuy nhiên, hãy nhớ rằng bạn phải đặt một số loại giới hạn trên vòng lặp, vì vậy nó không phải là một vòng lặp vô hạn tiềm năng.


3

Tôi có một trường hợp sử dụng mà tôi không thấy được bảo hiểm ở đây và sẽ cho rằng đây là lý do hợp lệ để sử dụng Thread.S ngủ ():

Trong một ứng dụng giao diện điều khiển chạy các công việc dọn dẹp, tôi cần thực hiện một số lượng lớn các cuộc gọi cơ sở dữ liệu khá tốn kém, đến một DB được chia sẻ bởi hàng ngàn người dùng đồng thời. Để không làm hỏng DB và loại trừ người khác trong nhiều giờ, tôi sẽ cần tạm dừng giữa các cuộc gọi, theo thứ tự 100 ms. Điều này không liên quan đến thời gian, chỉ để mang lại quyền truy cập vào DB cho các luồng khác.

Việc dành 2000-8000 chu kỳ để chuyển đổi ngữ cảnh giữa các cuộc gọi có thể mất 500 ms để thực hiện là lành tính, cũng như có 1 MB ngăn xếp cho luồng, chạy như một thể hiện duy nhất trên máy chủ.


Yeap, sử dụng Thread.Sleeptrong một ứng dụng Bảng điều khiển đơn, một mục đích giống như ứng dụng bạn mô tả là hoàn toàn OK.
Theodor Zoulias

-7

Tôi đồng ý với nhiều người ở đây, nhưng tôi cũng nghĩ nó phụ thuộc.

Gần đây tôi đã làm mã này:

private void animate(FlowLayoutPanel element, int start, int end)
{
    bool asc = end > start;
    element.Show();
    while (start != end) {
        start += asc ? 1 : -1;
        element.Height = start;
        Thread.Sleep(1);
    }
    if (!asc)
    {
        element.Hide();
    }
    element.Focus();
}

Đó là một chức năng animate đơn giản, và tôi đã sử dụng Thread.Sleepnó.

Kết luận của tôi, nếu nó làm công việc, sử dụng nó.


-8

Đối với những người bạn chưa thấy một đối số hợp lệ chống lại việc sử dụng Thread.S ngủ trong SCENARIO 2, thực sự có một lối thoát ứng dụng được giữ bởi vòng lặp while (SCENARIO 1/3 chỉ đơn giản là ngu ngốc nên không xứng đáng hơn đề cập đến)

Nhiều người giả vờ hiểu biết, la hét Thread.S ngủ là ác không thể đề cập đến một lý do hợp lệ duy nhất cho những người trong chúng ta yêu cầu một lý do thực tế không sử dụng nó - nhưng đây là, nhờ Pete - Thread.S ngủ là Ác (có thể dễ dàng tránh được với bộ hẹn giờ / xử lý)

    static void Main(string[] args)
    {
        Thread t = new Thread(new ThreadStart(ThreadFunc));
        t.Start();

        Console.WriteLine("Hit any key to exit.");
        Console.ReadLine();

        Console.WriteLine("App exiting");
        return;
    }

    static void ThreadFunc()
    {
        int i=0;
        try
        {
            while (true)
            {
                Console.WriteLine(Thread.CurrentThread.ThreadState.ToString() + " " + i);

                Thread.Sleep(1000 * 10);
                i++;
            }
        }
        finally
        {
            Console.WriteLine("Exiting while loop");
        }
        return;
    }

9
-, Thread.Sleepkhông, không phải là lý do cho điều này (chủ đề mới là vòng lặp while liên tục)! bạn chỉ có thể xóa Thread.Sleep-line - et voila: chương trình cũng sẽ không thoát ...
Andreas Niedermair
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.