เป็นไปได้ไหมที่จะรอการดำเนินการ IO ที่ไม่ได้ประกาศเป็นแบบอะซิงก์ ถ้าไม่ฉันควรทำอย่างไร?

ฉันยังใหม่กับการเขียนโปรแกรมแบบอะซิงโครนัสใน C# และฉันยังคงสับสนเกี่ยวกับบางสิ่ง ฉันได้อ่านแล้วว่าหลังจาก .NET 4.5 ไม่แนะนำ APM และ EAP สำหรับการพัฒนาใหม่อีกต่อไป เนื่องจาก TAP ควรจะแทนที่ (แหล่งที่มา)

ฉันคิดว่าฉันเข้าใจวิธีการทำงานของ async/await และสามารถใช้เพื่อดำเนินการ IO ที่มีวิธีการ async ได้ ตัวอย่างเช่น ฉันสามารถเขียนวิธี async ที่รอผลลัพธ์ GetStringAsync ของ HttpWebClient เนื่องจากมีการประกาศเป็นวิธี async นั่นเยี่ยมมาก

คำถามของฉันคือ จะเกิดอะไรขึ้นถ้าเรามีการดำเนินการ IO ที่เกิดขึ้นในวิธีการที่ไม่ได้ประกาศเป็นอะซิงก์ เช่นนี้: สมมติว่าฉันมี API ที่มีวิธีการ

string GetResultFromWeb()

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

Task<string> getResultTask = GetResultFromWeb(myUrl); 
// Do whatever I need to do that doesn't need the query result
string result = await getResultTask;
Process(result);

แต่เนื่องจากไม่ใช่ ฉันจึงไม่สามารถรอได้ -- มันบอกฉันว่า string is not awaitable ดังนั้นคำถามของฉันคือ: มีวิธีใดในการดำเนินการ IO เหล่านี้แบบอะซิงโครนัสโดยไม่ต้องสร้างหนึ่งเธรดสำหรับแต่ละแบบสอบถามหรือไม่ หากทำได้ ฉันอยากจะสร้างเธรดให้น้อยที่สุดเท่าที่จะเป็นไปได้ โดยไม่ต้องบล็อกเธรดใดๆ

วิธีหนึ่งที่ฉันพบว่าทำได้คือการใช้ APM โดยทำตามบทความนี้ จาก Jeffrey Richter จากนั้นในวิธี Begin ของฉัน ฉันเรียก ThreadPool.QueueWorkItem(GetResultFromWeb, asyncResult) แบบนี้:

public class A {
    private void DoQuery(Object ar){
        AsyncResult<string> asyncResult = (AsyncResult<string>) ar;
        string result = GetResultFromWeb();
        asyncResult.SetAsCompleted(result, false);
    }

    public IAsyncResult BeginQuery(AsyncCallback){
        AsyncResult<string> asyncResult = new AsyncResult<string>(callback, this);
        ThreadPool.QueueUserWorkItem(DoQuery, asyncResult);
        return asyncResult;
    }

    public string EndQuery(IAsyncResult ar){
        AsyncResult<string> asyncResult = (AsyncResult<string>)ar;
        return asyncResult.EndInvoke();
    }
}

จากนั้นฉันใช้ AsyncEnumerator และเริ่มต้น (BeginQuery) หลาย ๆ แบบสอบถามและประมวลผลผลลัพธ์เมื่อแต่ละรายการเสร็จสิ้น (โดยใช้ผลตอบแทนผลตอบแทน / EndQuery) ดูเหมือนว่าจะทำงานได้ดี แต่หลังจากอ่านมามากจน APM ล้าสมัย ฉันสงสัยว่าฉันจะทำอย่างไรโดยใช้ TAP นอกจากนี้ มีปัญหาใดๆ กับแนวทาง APM นี้หรือไม่

ขอบคุณ!


person Derek Patton    schedule 06.01.2015    source แหล่งที่มา


คำตอบ (3)


จะเกิดอะไรขึ้นถ้าเรามีการดำเนินการ IO ที่เกิดขึ้นในวิธีการที่ไม่ได้ประกาศเป็น async?

ในกรณีนี้ การดำเนินการ I/O กำลังบล็อก กล่าวอีกนัยหนึ่ง GetResultFromWeb บล็อกเธรดการโทร จำไว้ในขณะที่เราผ่านส่วนที่เหลือ...

ฉันต้องใช้วิธีนี้จึงจะทำเช่นนั้นได้

จากนี้ฉันอนุมานได้ว่าคุณไม่สามารถเขียนเมธอด GetResultFromWebAsync ที่ไม่ตรงกันได้ ดังนั้นเธรดใดๆ ที่ทำการร้องขอเว็บ ต้อง จะถูกบล็อก

มีวิธีใดในการดำเนินการ IO เหล่านี้แบบอะซิงโครนัสโดยไม่ต้องสร้างหนึ่งเธรดสำหรับแต่ละแบบสอบถามหรือไม่

แนวทางที่เป็นธรรมชาติที่สุดคือการเขียนวิธี GetResultFromWebAsync เนื่องจากเป็นไปไม่ได้ ตัวเลือกของคุณคือ: บล็อกเธรดที่เรียก หรือบล็อกเธรดอื่น (เช่น เธรดพูลเธรด) การบล็อกเธรดพูลเธรดเป็นเทคนิคที่ฉันเรียกว่า "อะซิงโครนัสปลอม" - เนื่องจากปรากฏแบบอะซิงโครนัส (เช่น ไม่ได้บล็อกเธรด UI) แต่จริงๆ แล้วไม่ได้เป็นเช่นนั้น (เช่น เพียงบล็อกเธรดพูลเธรดแทน)

หากทำได้ ฉันอยากจะสร้างเธรดให้น้อยที่สุดเท่าที่จะเป็นไปได้ โดยไม่ต้องบล็อกเธรดใดๆ

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

วิธีหนึ่งที่ฉันพบว่าทำได้คือการใช้ APM ตามบทความนี้จาก Jeffrey Richter จากนั้นในวิธี Begin ฉันเรียก ThreadPool.QueueWorkItem(GetResultFromWeb, asyncResult)

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

ดูเหมือนว่าจะทำงานได้ดี

มันใช้งานได้ แต่มันไม่อะซิงโครนัสอย่างแท้จริง

แต่หลังจากอ่านมามากจน APM ล้าสมัย ฉันสงสัยว่าฉันจะทำอย่างไรโดยใช้ TAP

ดังที่คนอื่นๆ ระบุไว้ มีวิธีที่ง่ายกว่ามากในการกำหนดเวลางานให้กับกลุ่มเธรด: Task.Run

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

Task<string> getResultTask = Task.Run(() => GetResultFromWeb(myUrl)); 
// Do whatever I need to do that doesn't need the query result
string result = await getResultTask;
Process(result);

(โค้ดที่สะอาดกว่า APM และ AsyncEnumerator มาก)

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

กล่าวอีกนัยหนึ่ง ตามที่ฉันได้อธิบายในรายละเอียดเพิ่มเติมในบล็อกของฉัน ใช้ Task.Run เพื่อเรียกใช้เมธอด ไม่ใช่เพื่อใช้เมธอด

person Stephen Cleary    schedule 06.01.2015
comment
คำตอบที่ดี! ขอบคุณ! - person Derek Patton; 06.01.2015

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

Task.Factory.FromAsync<string>(BeginQuery, EndQuery)

ซึ่งส่งคืน Task<string> ซึ่งคุณสามารถ await

person Ben Voigt    schedule 06.01.2015
comment
นอกเหนือจากการทำให้โค้ดง่ายขึ้นแล้ว มีข้อดี (ประสิทธิภาพ) ในการทำเช่นนี้แทนที่จะยึดติดกับวิธี Begin/End แบบเก่าหรือไม่ - person Derek Patton; 06.01.2015
comment
@DerekPatton: การแทรกแซงการโทรแบบ async อื่น ๆ นั้นง่ายกว่ามาก - person Ben Voigt; 06.01.2015
comment
@DerekPatton: นอกจากนี้ฉันไม่รู้ว่าทำไมคุณถึงคิดว่าอาจมีความได้เปรียบด้านประสิทธิภาพ กระดาษห่อไม่เคยเร็วกว่ารองพื้นที่วางไว้ - person Ben Voigt; 06.01.2015
comment
คุณบอกว่า API ของฉันใช้โมเดล Begin/End มันไม่ใช่. โปรดทราบว่าฉันสร้างวิธีการเริ่มต้น/สิ้นสุดด้วยตัวเอง นี่หมายความว่าฉันต้องสร้างเมธอด Begin/End แล้วใช้การเรียก Task.Factory.FromAsync นี้ทุกครั้งที่ฉันต้องการรอเมธอดที่ไม่ async ใช่หรือไม่ นอกจากนี้ สิ่งนี้จะสร้างเธรดใหม่ต่อการโทรหรือไม่ - person Derek Patton; 06.01.2015
comment
@DerekPatton: ตราบใดที่ API ของคุณสอดคล้องกับลายเซ็นมาตรฐาน คุณสามารถใช้คำแนะนำของ Ben ได้ โปรดทราบว่าไม่มีการโอเวอร์โหลดที่ตรงกับวิธี BeginQuery ของคุณทุกประการ (ใกล้ที่สุดเท่าที่ฉันจะบอกได้...มีเยอะมากและฉันอาจพลาดอะไรบางอย่างไป :) ) แต่คุณสามารถ โทร วิธีการและส่งผลลัพธ์ไปที่ FromAsync: Task.Factory.FromAsync<string>(BeginQuery(MyQueryCallback), EndQuery); (โดยที่ MyQueryCallback คือวิธีการติดต่อกลับของคุณ ซึ่งเป็นวิธีที่คุณจะส่งต่อไปยัง BeginQuery() โดยไม่คำนึงถึง) - person Peter Duniho; 06.01.2015
comment
@DerekPatton: ไม่ คุณคงไม่อยากเขียนเมธอด Begin/End เพียงเพื่อจุดประสงค์ในการล้อมมัน แต่มี API ที่มีอยู่จำนวนมากที่ใช้ Begin/End เพื่อให้มีประสิทธิภาพที่ดี คุณต้องเขียน GetResultFromWeb ใหม่เพื่อใช้ async API บางประเภทสำหรับ I/O เครือข่าย ไม่ว่าจะเป็น Begin/End, TPL async หรืออะไรก็ตาม ทั้งหมดนี้สามารถเปลี่ยนเป็น async ได้ ในกรณีเฉพาะของคุณ อาจดีกว่าถ้าส่งพร็อกซีคำขอโดยใช้ async API และเรียก API ของบริษัทอื่นด้วย URL ในเครื่องที่ตอบสนองทันที - person Ben Voigt; 06.01.2015

วิธีที่ง่ายกว่าในการทำสิ่งที่คุณกำลังมองหาคือการเรียกเมธอดโดยใช้คลาส Task ในกรณีของคุณมันจะมีลักษณะดังนี้:

Task<string> getResultTask = Task.Run<string>(()=>GetResultFromWeb(myUrl));
// Do whatever I need to do that doesn't need the query result
string result = await getResultTask;
Process(result);

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

person Jacob Lambert    schedule 06.01.2015
comment
คำตอบที่ดี แต่ไม่แนะนำให้มีเมธอด async ที่เพิ่งมอบหมายให้ Task.Run เนื่องจากเมธอด async มักจะไม่เริ่มเธรดใหม่ - person NeddySpaghetti; 06.01.2015
comment
ดี แต่นี่ไม่ใช่การสร้างเธรดใหม่สำหรับการเรียก GetResultFromWebAsync แต่ละครั้งใช่ไหม ฉันคิดว่า APM น่าจะมีประสิทธิภาพมากกว่าเนื่องจากจะไม่สร้างเธรดที่แตกต่างกันสำหรับการเรียกแต่ละครั้ง แต่จะจัดคิวงานและสลับเธรดในจำนวนที่น้อยกว่ามากสำหรับงานเหล่านั้น ดังนั้นจึงเป็นการขจัดค่าใช้จ่ายในการสร้างเธรดใหม่ ฉันผิดหรือเปล่า? - person Derek Patton; 06.01.2015
comment
@DerekPatton ฉันเชื่อว่าคลาส Task ใช้ ThreadPool ภายในหากฉันจำได้ว่าสิ่งที่ฉันอ่านนั้นถูกต้อง - person Jacob Lambert; 06.01.2015
comment
@NedStoyanov อะไรจะเป็นวิธีที่ดีกว่าในการดำเนินการนี้? นี่เป็นวิธีที่ดีที่สุดที่ฉันรู้จักในปัจจุบันเพื่อทำให้วิธีแบบไม่อะซิงก์ทำหน้าที่เป็นวิธีอะซิงก์ แต่ฉันจะพยายามปรับปรุงอยู่เสมอ - person Jacob Lambert; 06.01.2015
comment
ฉันคิดว่าคำแนะนำคืออย่าเพิ่มวิธีการลงท้ายด้วย xxxxAsync เพียงใช้ Task.Run โดยตรง - person NeddySpaghetti; 06.01.2015
comment
@DerekPatton ฉันถูกต้องในความคิดของฉันว่า Task ใช้เธรดพูล CLR ภายใน - person Jacob Lambert; 06.01.2015