เหตุใด Thread.Sleep จึงเป็นอันตราย


128

ฉันมักจะเห็นมันพูดถึงว่าThread.Sleep();ไม่ควรใช้ แต่ฉันไม่เข้าใจว่าทำไมถึงเป็นเช่นนั้น หากThread.Sleep();สามารถก่อให้เกิดปัญหามีทางเลือกอื่นใดที่ให้ผลลัพธ์เหมือนกันซึ่งจะปลอดภัยหรือไม่?

เช่น.

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

อีกอันคือ:

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
ข้อมูลสรุปของบล็อกอาจเป็น "อย่าใช้ Thread.sleep ()" ในทางที่ผิด
Martin James

5
ฉันจะไม่บอกว่ามันเป็นอันตราย ผมค่อนข้างจะบอกว่ามันก็เหมือนเช่นอาจจะมีทางออกที่ดีกว่าการแก้ปัญหาของคุณมากกว่าgoto: Sleep
ค่าเริ่มต้น

9
มันไม่เหมือนกับgotoกลิ่นรหัสมากกว่ากลิ่นดีไซน์ ไม่มีอะไรผิดปกติกับคอมไพเลอร์ที่ใส่gotoรหัสของคุณ: คอมพิวเตอร์ไม่สับสน แต่Thread.Sleepไม่เหมือนกันทุกประการ คอมไพเลอร์ไม่แทรกการเรียกนั้นและมีผลเสียอื่น ๆ แต่ใช่ว่าความรู้สึกทั่วไปที่ใช้มันไม่ถูกต้องเพราะมักจะมีทางออกที่ดีกว่านั้นถูกต้องแน่นอน
โคดี้เกรย์

32
ทุกคนกำลังให้ความเห็นว่าเหตุใดตัวอย่างข้างต้นจึงไม่ดี แต่ไม่มีใครจัดเตรียมเวอร์ชันที่เขียนซ้ำซึ่งไม่ใช้ Thread.Sleep () ที่ยังคงบรรลุเป้าหมายของตัวอย่างที่กำหนด
StingyJack

3
ทุกครั้งที่คุณใส่sleep()รหัส (หรือทดสอบ) ลูกสุนัขตาย
Reza S

คำตอบ:


163

Thread.Sleepมีการอธิบายปัญหาเกี่ยวกับการโทรอย่างรวบรัดไว้ที่นี่ :

Thread.Sleepมีการใช้งาน: จำลองการทำงานที่ยาวนานในขณะทดสอบ / แก้ไขข้อบกพร่องบนเธรด MTA ใน. NET ไม่มีเหตุผลอื่นที่จะใช้

Thread.Sleep(n)หมายถึงบล็อกเธรดปัจจุบันเป็นเวลาอย่างน้อยจำนวนครั้ง (หรือเธรดควอนตัม) ที่สามารถเกิดขึ้นได้ภายในn มิลลิวินาที ความยาวของไทม์สไลซ์จะแตกต่างกันไปตามรุ่น / ประเภทของ Windows และโปรเซสเซอร์ที่แตกต่างกันและโดยทั่วไปอยู่ในช่วง 15 ถึง 30 มิลลิวินาที ซึ่งหมายความว่าเธรดเกือบจะรับประกันได้ว่าจะบล็อกได้นานกว่าnมิลลิวินาที ความเป็นไปได้ที่เธรดของคุณจะกลับมาทำงานอีกครั้งหลังจากnมิลลิวินาทีนั้นเป็นไปไม่ได้เลย ดังนั้นThread.Sleepจะไม่มีจุดหมายสำหรับการกำหนดเวลา

เธรดเป็นทรัพยากรที่ จำกัด ใช้เวลาประมาณ 200,000 รอบในการสร้างและประมาณ 100,000 รอบในการทำลาย โดยค่าเริ่มต้นจะสงวนหน่วยความจำเสมือน 1 เมกะไบต์สำหรับสแต็กและใช้ 2,000-8,000 รอบสำหรับสวิตช์บริบทแต่ละรายการ นี้จะทำให้รอใด ๆ ด้าย ขนาดใหญ่เสีย

ทางออกที่ต้องการ: WaitHandles

ในที่สุดที่ทำผิดพลาดคือการใช้Thread.Sleepด้วยในขณะที่-สร้าง ( สาธิตและคำตอบ , ดีบล็อกรายการ )

แก้ไข:
ฉันต้องการปรับปรุงคำตอบของฉัน:

เรามี 2 กรณีการใช้งานที่แตกต่างกัน:

  1. เรากำลังรอเพราะเรารู้ว่าช่วงเวลาที่เฉพาะเจาะจงเมื่อเราควรดำเนินการต่อ (การใช้งานThread.Sleep, System.Threading.Timerหรือลุค)

  2. เรากำลังรอเพราะเงื่อนไขบางอย่างมีการเปลี่ยนแปลงในบางครั้ง ... คำหลักคือ / บางเวลา ! หากการตรวจสอบเงื่อนไขอยู่ในโค้ดโดเมนของเราเราควรใช้ WaitHandles - มิฉะนั้นส่วนประกอบภายนอกควรมีตะขอบางประเภท ... ถ้ามันไม่ได้ออกแบบมาก็ไม่ดี!

คำตอบของฉันส่วนใหญ่ครอบคลุมกรณีการใช้งาน 2


30
ฉันจะไม่เรียกหน่วยความจำ 1 MB ซึ่งเป็นการสิ้นเปลืองอย่างมากเมื่อพิจารณาถึงฮาร์ดแวร์ในปัจจุบัน
ค่าเริ่มต้น

14
@ ค่าเริ่มต้นเดี๋ยวก่อนพูดคุยกับผู้เขียนต้นฉบับ :) และมันขึ้นอยู่กับรหัสของคุณเสมอหรือดีกว่า: ปัจจัย ... และปัญหาหลักคือ "เธรดเป็นทรัพยากรที่ จำกัด " - เด็ก ๆ ในปัจจุบันไม่รู้มากนัก เกี่ยวกับประสิทธิภาพและค่าใช้จ่ายของการใช้งานบางอย่างเนื่องจาก "ฮาร์ดแวร์มีราคาถูก" ... แต่บางครั้งคุณต้องเลือกรหัสมาก
Andreas Niedermair

11
'สิ่งนี้ทำให้การรอคอยเป็นเรื่องสิ้นเปลืองมาก' ใช่มั้ย? หากข้อกำหนดของโปรโตคอลบางอย่างต้องการการหยุดชั่วคราวหนึ่งวินาทีก่อนดำเนินการต่อจะต้องรออะไรเป็นเวลา 1 วินาที บางกระทู้บางแห่งคงต้องรอ! ค่าใช้จ่ายสำหรับการสร้าง / ทำลายเธรดมักไม่เกี่ยวข้องเนื่องจากเธรดจะต้องถูกยกขึ้นด้วยเหตุผลอื่น ๆ และจะทำงานตลอดอายุของกระบวนการ ฉันสนใจที่จะเห็นวิธีการหลีกเลี่ยงสวิตช์บริบทเมื่อข้อมูลจำเพาะระบุว่า 'หลังจากเปิดปั๊มรออย่างน้อยสิบวินาทีเพื่อให้แรงดันคงที่ก่อนเปิดวาล์วป้อน'
Martin James

9
@CodyGray - ฉันอ่านโพสต์อีกครั้ง ฉันไม่เห็นปลาที่มีสีใด ๆ ในความคิดเห็นของฉัน Andreas ดึงออกมาจากเว็บ: 'Thread.Sleep มีการใช้งาน: จำลองการทำงานที่ยาวนานขณะทดสอบ / แก้ไขข้อบกพร่องบนเธรด MTA ใน. NET ไม่มีเหตุผลอื่นที่จะใช้ ' ฉันยืนยันว่ามีหลายแอพที่การโทร sleep () เป็นสิ่งที่จำเป็น หากกลุ่มนักพัฒนา (สำหรับมีหลายคน) ยืนยันที่จะใช้ sleep () ลูปเป็นตัวตรวจสอบเงื่อนไขที่ควรถูกแทนที่ด้วยเหตุการณ์ / condvars / semas / อะไรก็ตามนั่นไม่ใช่เหตุผลสำหรับการยืนยันว่า 'ไม่มีเหตุผลอื่นที่จะใช้มัน '
Martin James

8
ใน 30 ปีของการพัฒนาแอพ multiThreaded (ส่วนใหญ่เป็น C ++ / Delphi / Windows) ฉันไม่เคยเห็นความจำเป็นในการนอนหลับ (0) หรือโหมดสลีป (1) ใด ๆ ในโค้ดที่ส่งมอบได้ ในบางครั้งฉันได้ใส่รหัสดังกล่าวเพื่อจุดประสงค์ในการดีบัก แต่ก็ไม่เคยส่งถึงลูกค้า 'ถ้าคุณกำลังเขียนบางสิ่งที่ไม่ได้ควบคุมทุกเธรดอย่างสมบูรณ์' - การจัดการเธรดขนาดเล็กถือเป็นความผิดพลาดที่ยิ่งใหญ่พอ ๆ กับการจัดการขนาดเล็กของเจ้าหน้าที่พัฒนา การจัดการเธรดคือสิ่งที่ระบบปฏิบัติการมีไว้ - ควรใช้เครื่องมือที่มีให้
Martin James

34

SCENARIO 1 - รอให้งาน async เสร็จสิ้น: ฉันยอมรับว่าควรใช้ WaitHandle / Auto | ManualResetEvent ในสถานการณ์ที่เธรดกำลังรอให้งานบนเธรดอื่นเสร็จสมบูรณ์

กรณีที่ 2 - ระยะเวลาในขณะที่วง: แต่เป็นน้ำมันดิบกลไกการกำหนดเวลา (ในขณะที่ + Thread.Sleep) เป็นอย่างดีดีสำหรับ 99% ของการใช้งานซึ่งไม่จำเป็นต้องรู้ว่าเมื่อกระทู้บล็อกควรจะ "ตื่นขึ้นมา * ข้อโต้แย้งว่าจะใช้เวลา. 200k รอบในการสร้างเธรดก็ไม่ถูกต้องเช่นกัน - จำเป็นต้องสร้างเธรดลูปไทม์มิ่งต่อไปและ 200k รอบเป็นเพียงตัวเลขขนาดใหญ่อีกจำนวนหนึ่ง (บอกฉันว่าจะเปิดการเรียกไฟล์ / ซ็อกเก็ต / ฐานข้อมูลกี่รอบ)

ดังนั้นถ้าในขณะที่ + Thread.Sleep ทำงานทำไมสิ่งต่าง ๆ ซับซ้อน? มีเพียงทนายความด้านไวยากรณ์เท่านั้นที่สามารถใช้ได้จริง !


โชคดีที่ตอนนี้เรามี TaskCompletionSource สำหรับการรอผลอย่างมีประสิทธิภาพ
Austin Salgat

14

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

รายการโปรดอีกสองสามรายการของฉันในโลก C # คือพวกเขาบอกให้คุณ "ไม่โทรล็อค (นี้)" หรือ "ไม่โทรหา GC.Collect ()" มีการประกาศอย่างจริงจังในบล็อกและเอกสารทางการจำนวนมากและ IMO เป็นข้อมูลที่ผิดอย่างสมบูรณ์ ในบางระดับข้อมูลที่ผิดนี้มีจุดประสงค์เพื่อให้ผู้เริ่มต้นไม่ทำสิ่งที่พวกเขาไม่เข้าใจก่อนที่จะค้นคว้าทางเลือกอย่างเต็มที่ แต่ในขณะเดียวกันก็ทำให้ยากที่จะค้นหาข้อมูลจริงผ่านทางเครื่องมือค้นหาทั้งหมด ดูเหมือนจะชี้ไปที่บทความที่บอกให้คุณอย่าทำอะไรบางอย่างในขณะที่ไม่มีคำตอบสำหรับคำถาม "ทำไมไม่"

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

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


5

มันคือ 1) .spinning และ 2) .polling loop ของตัวอย่างของคุณที่ผู้คนเตือนไม่ใช่ส่วน Thread.Sleep () ฉันคิดว่า Thread.Sleep () มักถูกเพิ่มเพื่อปรับปรุงโค้ดที่หมุนหรืออยู่ในลูปการสำรวจได้อย่างง่ายดายดังนั้นจึงมีความเกี่ยวข้องกับโค้ด "ไม่ดี" เท่านั้น

นอกจากนี้ผู้คนยังทำสิ่งต่างๆเช่น:

while(inWait)Thread.Sleep(5000); 

โดยที่ตัวแปร inWait ไม่ถูกเข้าถึงในลักษณะที่ปลอดภัยของเธรดซึ่งทำให้เกิดปัญหาเช่นกัน

สิ่งที่โปรแกรมเมอร์ต้องการเห็นคือเธรดที่ควบคุมโดยเหตุการณ์และโครงสร้างการส่งสัญญาณและการล็อกและเมื่อคุณทำเช่นนั้นคุณไม่จำเป็นต้องใช้ Thread.Sleep () และความกังวลเกี่ยวกับการเข้าถึงตัวแปรที่ปลอดภัยของเธรดจะถูกตัดออกไปด้วย ตัวอย่างเช่นคุณสามารถสร้างตัวจัดการเหตุการณ์ที่เชื่อมโยงกับคลาส FileSystemWatcher และใช้เหตุการณ์เพื่อทริกเกอร์ตัวอย่างที่ 2 ของคุณแทนการวนซ้ำได้หรือไม่

ดังที่ Andreas N. กล่าวถึงอ่านThreading ใน C # โดย Joe Albahari มันดีจริงๆ


แล้วทางเลือกอื่นในการทำสิ่งที่อยู่ในตัวอย่างโค้ดของคุณคืออะไร?
shinzou

kuhaku - ใช่คำถามที่ดี เห็นได้ชัดว่า Thread.Sleep () เป็นทางลัดและบางครั้งก็เป็นทางลัดขนาดใหญ่ สำหรับตัวอย่างข้างต้นส่วนที่เหลือของโค้ดในฟังก์ชันด้านล่างของลูปการสำรวจ "while (inWait) Thread.Sleep (5000);" เป็นฟังก์ชันใหม่ ฟังก์ชันใหม่นั้นคือผู้รับมอบสิทธิ์ (ฟังก์ชันการเรียกกลับ) และคุณส่งต่อไปยังสิ่งใดก็ตามที่ตั้งค่าสถานะ "inWait" และแทนที่จะเปลี่ยนแฟล็ก "inWait" การเรียกกลับจะถูกเรียกใช้ นี่เป็นตัวอย่างที่สั้นที่สุดที่ฉันหาได้myelin.co.nz/notes/callbacks/cs-delegates.html
mike

เพื่อให้แน่ใจว่าฉันเข้าใจแล้วคุณหมายถึงการรวมThread.Sleep()กับฟังก์ชันอื่นและเรียกสิ่งนั้นในวง while?
shinzou

5

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


3

ฉันมีกรณีการใช้งานที่ไม่ค่อยเห็นครอบคลุมที่นี่และจะยืนยันว่านี่เป็นเหตุผลที่ถูกต้องในการใช้ Thread.Sleep ():

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

การใช้จ่าย 2,000-8,000 รอบในการสลับบริบทระหว่างการโทรที่อาจใช้เวลา 500 ms ในการดำเนินการนั้นไม่เป็นพิษเป็นภัยเช่นเดียวกับการมีสแต็ก 1 MB สำหรับเธรดซึ่งทำงานเป็นอินสแตนซ์เดียวบนเซิร์ฟเวอร์


Yeap ใช้Thread.Sleepในแอปพลิเคชันคอนโซลแบบเธรดเดียวแบบเดียวกับที่คุณอธิบายไว้นั้นใช้ได้อย่างสมบูรณ์แบบ
Theodor Zoulias

-7

ฉันเห็นด้วยกับหลาย ๆ คนที่นี่ แต่ฉันก็คิดว่ามันขึ้นอยู่กับ

ฉันเพิ่งทำรหัสนี้:

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

มันเป็นฟังก์ชั่นสร้างภาพเคลื่อนไหวที่เรียบง่ายและฉันใช้Thread.Sleepมัน

ข้อสรุปของฉันถ้ามันได้ผลให้ใช้มัน


-8

สำหรับพวกคุณที่ยังไม่เห็นอาร์กิวเมนต์ที่ถูกต้องเกี่ยวกับการใช้เธรดการนอนหลับใน SCENARIO 2 มีอยู่อย่างหนึ่ง - การออกจากแอปพลิเคชันจะถูกระงับโดยลูป while (SCENARIO 1/3 นั้นโง่ธรรมดาดังนั้นจึงไม่คุ้มค่ามากกว่านี้ กล่าวขวัญ)

หลายคนที่แสร้งทำเป็นไม่รู้ตัวและกรีดร้อง Thread การนอนหลับเป็นสิ่งชั่วร้ายล้มเหลวในการพูดถึงเหตุผลที่ถูกต้องเพียงข้อเดียวสำหรับพวกเราที่ต้องการเหตุผลที่เป็นประโยชน์ที่จะไม่ใช้มัน - แต่นี่คือสิ่งที่ต้องขอบคุณ Pete - Thread is Evil (สามารถหลีกเลี่ยงได้อย่างง่ายดายด้วยตัวจับเวลา / ตัวจัดการ)

    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.Sleepไม่ไม่ใช่เหตุผลนี้ (เธรดใหม่ที่ต่อเนื่อง while-loop คือ)! คุณสามารถลบThread.Sleep-line - et voila: โปรแกรมจะไม่ออกเช่นกัน ...
Andreas Niedermair
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.