ความแตกต่างระหว่างการเขียนโปรแกรมแบบอะซิงโครนัสและมัลติเธรดคืออะไร?


234

ฉันคิดว่ามันเหมือนกัน - เขียนโปรแกรมที่แบ่งงานระหว่างโปรเซสเซอร์ (บนเครื่องที่มีโปรเซสเซอร์ 2+) จากนั้นฉันก็อ่านสิ่งนี้ซึ่งพูดว่า:

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

คำหลักแบบ async และ waitit จะไม่ทำให้เกิดการสร้างเธรดเพิ่มเติม เมธอด Async ไม่ต้องการมัลติเธรดเนื่องจากเมธอด async ไม่ทำงานบนเธรดของตัวเอง วิธีการทำงานกับบริบทการซิงโครไนซ์ปัจจุบันและใช้เวลาในเธรดเฉพาะเมื่อวิธีการใช้งานอยู่ คุณสามารถใช้ Task.Run เพื่อย้ายงานที่เชื่อมโยงกับ CPU ไปยังเธรดพื้นหลังได้ แต่เธรดพื้นหลังไม่ได้ช่วยกระบวนการที่รอให้ผลลัพธ์พร้อมใช้งาน

และฉันสงสัยว่ามีคนสามารถแปลเป็นภาษาอังกฤษให้ฉันได้หรือไม่ ดูเหมือนว่าจะแยกความแตกต่างระหว่าง asyncronicity (นั่นคือคำ?) และการทำเกลียวและบอกเป็นนัยว่าคุณสามารถมีโปรแกรมที่มีงานแบบอะซิงโครนัส แต่ไม่มีมัลติเธรด

ตอนนี้ฉันเข้าใจความคิดของงานอะซิงโครนัสเช่นตัวอย่างใน pg 467 ของC #ของ Jon Skeet ในความลึก, รุ่นที่สาม

async void DisplayWebsiteLength ( object sender, EventArgs e )
{
    label.Text = "Fetching ...";
    using ( HttpClient client = new HttpClient() )
    {
        Task<string> task = client.GetStringAsync("http://csharpindepth.com");
        string text = await task;
        label.Text = text.Length.ToString();
    }
}

asyncหมายถึงคำว่า " ฟังก์ชั่นนี้เมื่อใดก็ตามที่เรียกว่าจะไม่ถูกเรียกในบริบทที่เสร็จสมบูรณ์เป็นสิ่งจำเป็นสำหรับทุกอย่างหลังจากเรียกจะเรียกว่า."

กล่าวอีกนัยหนึ่งเขียนไว้ตรงกลางงาน

int x = 5; 
DisplayWebsiteLength();
double y = Math.Pow((double)x,2000.0);

เนื่องจากDisplayWebsiteLength()ไม่มีส่วนเกี่ยวข้องกับxหรือyจะทำให้DisplayWebsiteLength()ถูกดำเนินการ "ในพื้นหลัง" เช่น

                processor 1                |      processor 2
-------------------------------------------------------------------
int x = 5;                                 |  DisplayWebsiteLength()
double y = Math.Pow((double)x,2000.0);     |

เห็นได้ชัดว่าเป็นตัวอย่างที่โง่ แต่ฉันถูกต้องหรือฉันสับสนทั้งหมดหรืออะไร

(นอกจากนี้ฉันสับสนเกี่ยวกับสาเหตุsenderและeไม่เคยใช้ในเนื้อหาของฟังก์ชั่นด้านบน)


13
นี่เป็นคำอธิบายที่ดี: blog.stephencleary.com/2013/11/there-is-no-thread.html
Jakub Lortz

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


3
หมายเหตุสำคัญมากเกี่ยวกับDisplayWebsiteLengthตัวอย่างรหัส: คุณไม่ควรใช้HttpClientในusingคำสั่ง - ภายใต้การโหลดจำนวนมากรหัสสามารถใช้จำนวนซ็อกเก็ตที่มีอยู่หมดซึ่งส่งผลให้เกิดข้อผิดพลาด SocketException ข้อมูลเพิ่มเติมเกี่ยวกับการเริ่มที่ไม่เหมาะสม
กาน

1
@JakubLortz ฉันไม่รู้ว่าบทความนี้มีไว้เพื่อใครจริงๆ ไม่ใช่สำหรับผู้เริ่มต้นเนื่องจากต้องการความรู้ที่ดีเกี่ยวกับเธรดการขัดจังหวะสิ่งที่เกี่ยวข้องกับ CPU ฯลฯ ไม่ใช่สำหรับผู้ใช้ขั้นสูงเนื่องจากสำหรับพวกเขาทุกอย่างชัดเจนแล้ว ฉันแน่ใจว่ามันจะไม่ช่วยให้ใครเข้าใจสิ่งที่เกี่ยวกับ - ระดับสูงของนามธรรม
Loreno

คำตอบ:


589

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

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

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

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

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

ลองดูตัวอย่างของ Jon ในรายละเอียดเพิ่มเติม เกิดอะไรขึ้น?

  • มีคนเรียกใช้ DisplayWebSiteLength Who? เราไม่สนใจ
  • มันตั้งฉลากสร้างลูกค้าและขอให้ลูกค้าดึงบางสิ่งบางอย่าง ลูกค้าส่งคืนวัตถุที่เป็นตัวแทนของงานในการดึงบางสิ่งบางอย่าง งานนั้นกำลังดำเนินการอยู่
  • กำลังดำเนินการกับเธรดอื่นอยู่หรือไม่ อาจจะไม่. อ่านบทความของสตีเฟ่นว่าทำไมไม่มีเธรด
  • ตอนนี้เรารองาน เกิดอะไรขึ้น? เราตรวจสอบเพื่อดูว่างานนั้นเสร็จระหว่างเวลาที่เราสร้างมันขึ้นมาหรือไม่และเรารออยู่ ถ้าใช่เราจะดึงผลลัพธ์และดำเนินการต่อ สมมติว่ามันยังไม่เสร็จ เราลงทะเบียนที่เหลือของวิธีการนี้เป็นความต่อเนื่องของงานที่และผลตอบแทน
  • ตอนนี้การควบคุมได้กลับไปที่ผู้โทร มันทำอะไร? สิ่งที่มันต้องการ
  • ตอนนี้สมมติว่างานเสร็จสมบูรณ์ มันทำอย่างนั้นได้อย่างไร? บางทีมันอาจจะทำงานบนเธรดอื่นหรือบางทีผู้เรียกที่เราเพิ่งกลับไปอนุญาตให้มันรันให้เสร็จสมบูรณ์บนเธรดปัจจุบัน ไม่ว่าตอนนี้เรามีงานที่เสร็จสมบูรณ์แล้ว
  • งานที่เสร็จสมบูรณ์จะถามเธรดที่ถูกต้อง - อีกครั้งอาจเป็นเธรดเท่านั้น - เพื่อรันความต่อเนื่องของงาน
  • การควบคุมผ่านทันทีกลับเข้าสู่วิธีการที่เราเพิ่งออกไป ณ จุดรอ ขณะนี้มีเป็นผลที่มีอยู่เพื่อให้เราสามารถกำหนดtextและเรียกส่วนที่เหลือของวิธีการ

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


8
@ user5648283: ฮาร์ดแวร์อยู่ในระดับที่ไม่ถูกต้องที่จะคิดเกี่ยวกับงาน งานเป็นเพียงวัตถุที่ (1) แสดงให้เห็นว่ามูลค่าที่จะกลายเป็นใช้ได้ในอนาคตและ (2) สามารถเรียกใช้รหัส (ในหัวข้อที่ถูกต้อง) เมื่อค่าที่ใช้ได้ งานใด ๆ ของบุคคลที่ได้รับผลลัพธ์ในอนาคตนั้นขึ้นอยู่กับมัน บางคนจะใช้ฮาร์ดแวร์พิเศษเช่น "ดิสก์" และ "การ์ดเครือข่าย" เพื่อทำเช่นนั้น บางอย่างจะใช้ฮาร์ดแวร์เช่น CPU
Eric Lippert

13
@ user5648283: คิดอีกครั้งเกี่ยวกับการเปรียบเทียบของฉัน เมื่อมีคนขอให้คุณปรุงไข่และขนมปังคุณใช้ฮาร์ดแวร์พิเศษ - เตาและเครื่องปิ้งขนมปัง - และคุณสามารถทำความสะอาดครัวในขณะที่ฮาร์ดแวร์ทำงานอยู่ หากมีคนขอไข่ขนมปังและคำวิจารณ์ดั้งเดิมของภาพยนตร์ฮอบบิทครั้งล่าสุดคุณสามารถเขียนบทวิจารณ์ของคุณในขณะที่ไข่และขนมปังปิ้งกำลังทำอาหาร แต่คุณไม่จำเป็นต้องใช้ฮาร์ดแวร์สำหรับเรื่องนั้น
Eric Lippert

9
@ user5648283: ตอนนี้สำหรับคำถามของคุณเกี่ยวกับ "การจัดเรียงรหัสใหม่" ให้พิจารณาสิ่งนี้ สมมติว่าคุณมีวิธี P ซึ่งมีผลตอบแทนผลตอบแทนและวิธีการ Q ซึ่งทำ foreach มากกว่าผลของ P. Step ผ่านรหัส คุณจะเห็นว่าเราใช้คิวไปนิดหน่อยแล้วก็พีเล็กน้อยแล้วก็ถามนิดหน่อย ... คุณเข้าใจจุดนั้นหรือไม่? รอคอยเป็นหลักผลตอบแทนในการแต่งกายแฟนซี ตอนนี้มันชัดเจนขึ้นหรือไม่
Eric Lippert

10
เครื่องปิ้งขนมปังเป็นฮาร์ดแวร์ ฮาร์ดแวร์ไม่ต้องการเธรดเพื่อให้บริการ ดิสก์และการ์ดเครือข่ายและสิ่งที่ไม่ได้ทำงานในระดับที่ต่ำกว่าเธรด OS
Eric Lippert

5
@ShivprasadKoirala: นั่นไม่จริงเลย หากคุณเชื่อว่าแล้วคุณมีความเชื่อผิด ๆ เกี่ยวกับ asynchrony จุดทั้งหมดของความไม่ตรงกันใน C # คือมันไม่ได้สร้างเธรด
Eric Lippert

27

Javascript ในเบราว์เซอร์เป็นตัวอย่างที่ยอดเยี่ยมของโปรแกรมอะซิงโครนัสที่ไม่มีเธรด

คุณไม่ต้องกังวลกับรหัสหลายชิ้นที่สัมผัสกับวัตถุเดียวกันในเวลาเดียวกัน: แต่ละฟังก์ชั่นจะเสร็จสิ้นการทำงานก่อนที่จาวาสคริปต์อื่น ๆ จะได้รับอนุญาตให้ทำงานบนหน้าเว็บ

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

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

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

หากคุณคิดว่า CPU ทำอะไรเมื่ออ่านไฟล์ที่ระดับฮาร์ดแวร์และระบบปฏิบัติการโดยทั่วไปแล้วจะมีคำสั่งให้อ่านข้อมูลจากดิสก์ไปยังหน่วยความจำและตีระบบปฏิบัติการด้วยการขัดจังหวะ " "เมื่อการอ่านเสร็จสมบูรณ์ กล่าวอีกนัยหนึ่งการอ่านจากดิสก์ (หรือ I / O ใด ๆ จริงๆ) เป็นการดำเนินการแบบอะซิงโครนัสโดยเนื้อแท้ แนวคิดของเธรดที่รอให้ I / O เสร็จสมบูรณ์เป็นนามธรรมที่นักพัฒนาไลบรารีสร้างขึ้นเพื่อให้ง่ายต่อการตั้งโปรแกรม มันไม่จำเป็น.

ตอนนี้การดำเนินงาน I / O ส่วนใหญ่ใน. NET มี...Async()วิธีการที่สอดคล้องกันซึ่งคุณสามารถเรียกใช้ได้ซึ่งจะส่งคืนTaskเกือบทันที คุณสามารถเพิ่มการเรียกกลับไปที่สิ่งนี้Taskเพื่อระบุรหัสที่คุณต้องการเรียกใช้เมื่อการดำเนินการแบบอะซิงโครนัสเสร็จสมบูรณ์ คุณยังสามารถระบุเธรดที่คุณต้องการให้โค้ดทำงานและคุณสามารถระบุโทเค็นที่การดำเนินการแบบอะซิงโครนัสสามารถตรวจสอบได้เป็นครั้งคราวเพื่อดูว่าคุณตัดสินใจที่จะยกเลิกงานอะซิงโครนัสหรือไม่ และอย่างสง่างาม

จนกว่าจะasync/awaitมีการเพิ่มคำหลัก C # นั้นชัดเจนมากขึ้นเกี่ยวกับวิธีการเรียกใช้รหัสโทรกลับเนื่องจากการเรียกกลับเหล่านั้นอยู่ในรูปแบบของตัวแทนที่คุณเชื่อมโยงกับงาน เพื่อให้คุณได้รับประโยชน์จากการใช้...Async()งานในขณะที่หลีกเลี่ยงความซับซ้อนในโค้ดให้async/awaitย่อการสร้างผู้แทนเหล่านั้น แต่พวกเขายังคงอยู่ในรหัสที่รวบรวม

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


มีเพียงหนึ่ง JavaScript "หัวข้อ" ทำงานเป็น - ไม่จริงกับเว็บแรงงาน
oleksii

6
@oleksii: นั่นเป็นเรื่องจริงทางเทคนิค แต่ฉันไม่ได้ไปที่นั่นเพราะ Web Workers API นั้นเป็นแบบอะซิงโครนัสและ Web Workers ไม่ได้รับอนุญาตให้ส่งผลกระทบโดยตรงต่อค่า javascript หรือ DOM บนหน้าเว็บที่พวกเขาเรียกใช้ จากซึ่งหมายถึงย่อหน้าที่สองที่สำคัญของคำตอบนี้ยังคงเป็นจริง จากมุมมองของโปรแกรมเมอร์มีความแตกต่างเพียงเล็กน้อยระหว่างการเรียกใช้ Web Worker กับการร้องขอ AJAX
StriplingWarrior
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.