Apakah mungkin menunggu operasi IO yang tidak dinyatakan sebagai async? Jika tidak, apa yang harus saya lakukan?

Saya baru mengenal pemrograman asinkron di C# dan saya masih bingung tentang beberapa hal. Saya telah membaca bahwa setelah .NET 4.5, APM dan EAP tidak lagi direkomendasikan untuk pengembangan baru karena TAP seharusnya menggantikannya (sumber).

Saya rasa saya memahami cara kerja async/await dan saya dapat menggunakannya untuk melakukan operasi IO yang memiliki metode async. Misalnya, saya dapat menulis metode async yang menunggu hasil GetStringAsync HttpWebClient, karena metode tersebut dinyatakan sebagai metode async. Itu hebat.

Pertanyaan saya adalah: bagaimana jika kita memiliki operasi IO yang terjadi pada metode yang tidak dinyatakan sebagai async? Seperti ini: misalkan saya memiliki API yang memiliki metode

string GetResultFromWeb()

yang menanyakan sesuatu dari Web. Dan saya mempunyai banyak pertanyaan berbeda yang harus dilakukan dan saya harus menggunakan metode ini untuk melakukannya. Dan kemudian saya perlu memproses setiap hasil kueri. Saya memahami bahwa saya akan melakukan ini jika itu adalah metode 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);

Namun karena tidak, saya tidak dapat menunggunya -- ini memberi tahu saya bahwa string tidak dapat ditunggu. Jadi pertanyaan saya adalah: apakah ada cara untuk melakukan operasi IO ini secara asinkron tanpa harus membuat satu utas untuk setiap kueri? Jika saya bisa, saya ingin membuat thread sesedikit mungkin, tanpa harus memblokir thread mana pun.

Salah satu cara yang saya temukan untuk melakukannya adalah dengan menerapkan APM, dengan mengikuti artikel ini dari Jeffrey Richter dan kemudian, dalam metode Begin saya, saya memanggil ThreadPool.QueueWorkItem(GetResultFromWeb, asyncResult). Seperti ini:

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

Kemudian saya menggunakan AsyncEnumerator dan memulai (BeginQuery) beberapa kueri dan memproses hasilnya saat masing-masing kueri selesai (menggunakan pengembalian hasil/EndQuery). Hal ini tampaknya bekerja dengan baik. Tapi setelah membaca begitu banyak bahwa APM sudah usang, saya bertanya-tanya bagaimana saya bisa melakukan ini menggunakan TAP. Juga, apakah ada masalah dengan pendekatan APM ini?

Terima kasih!


person Derek Patton    schedule 06.01.2015    source sumber


Jawaban (3)


bagaimana jika kita memiliki operasi IO yang terjadi pada metode yang tidak dideklarasikan sebagai async?

Dalam hal ini, operasi I/O diblokir. Dengan kata lain, GetResultFromWeb memblokir thread pemanggil. Ingatlah hal itu saat kita membahas sisanya...

Saya harus menggunakan metode ini untuk melakukannya.

Dengan ini saya menyimpulkan bahwa Anda tidak dapat menulis metode GetResultFromWebAsync yang tidak sinkron. Jadi setiap thread yang melakukan permintaan web harus diblokir.

apakah ada cara untuk melakukan operasi IO ini secara asinkron tanpa harus membuat satu utas untuk setiap kueri?

Pendekatan paling alami adalah dengan menulis metode GetResultFromWebAsync. Karena itu tidak mungkin, pilihan Anda adalah: memblokir thread pemanggil, atau memblokir beberapa thread lain (yaitu, thread pool thread). Memblokir thread pool thread adalah teknik yang saya sebut "asinkroni palsu" - karena tampaknya tidak sinkron (yaitu, tidak memblokir thread UI) tetapi sebenarnya tidak (yaitu, hanya memblokir thread pool thread saja).

Jika saya bisa, saya ingin membuat thread sesedikit mungkin, tanpa harus memblokir thread mana pun.

Hal ini tidak mungkin dilakukan mengingat adanya kendala. Jika Anda harus menggunakan metode GetResultFromWeb, dan metode tersebut memblokir thread pemanggil, maka thread harus diblokir.

Salah satu cara yang saya temukan untuk melakukannya adalah dengan menerapkan APM, mengikuti artikel dari Jeffrey Richter ini dan kemudian, dalam metode Begin saya, saya memanggil ThreadPool.QueueWorkItem(GetResultFromWeb, asyncResult).

Dalam hal ini, kode Anda mengekspos API asinkron (mulai/akhir), tetapi dalam implementasinya hanya memanggil GetResultFromWeb pada thread pool thread. Yaitu, ini adalah asinkroni palsu.

Hal ini tampaknya bekerja dengan baik.

Ini berfungsi, tetapi tidak benar-benar asinkron.

Tapi setelah membaca begitu banyak bahwa APM sudah usang, saya bertanya-tanya bagaimana saya bisa melakukan ini menggunakan TAP.

Seperti yang telah dicatat orang lain, ada cara yang lebih mudah untuk menjadwalkan pekerjaan ke kumpulan thread: Task.Run.

Asinkroni yang sebenarnya tidak mungkin dilakukan, karena Anda memiliki metode pemblokiran yang harus Anda gunakan. Jadi, yang bisa Anda lakukan hanyalah solusi - asinkron palsu, alias memblokir thread pool thread. Cara termudah untuk melakukannya adalah:

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

(kode jauh lebih bersih daripada APM dan AsyncEnumerator)

Perhatikan bahwa saya tidak merekomendasikan pembuatan metode GetResultFromWebAsync yang diimplementasikan menggunakan asinkron palsu. Metode Pengembalian Tugas, akhiran Async seharusnya mengikuti Pola Asinkron Berbasis Tugas pedoman, yang menyiratkan ketidaksinkronan yang benar.

Dengan kata lain, seperti yang saya jelaskan lebih detail di blog saya, gunakan Task.Run untuk memanggil suatu metode, bukan untuk mengimplementasikan suatu metode.

person Stephen Cleary    schedule 06.01.2015
comment
Jawaban yang bagus! Terima kasih! - person Derek Patton; 06.01.2015

API Anda tidak sinkron menggunakan model Mulai/Akhir yang lebih lama. Ini cocok dengan TPL via

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

yang mengembalikan Task<string> yang Anda dapat await.

person Ben Voigt    schedule 06.01.2015
comment
Selain membuat kode lebih sederhana, apakah ada keuntungan (kinerja) melakukan hal ini daripada hanya menggunakan metode Begin/End yang lama? - person Derek Patton; 06.01.2015
comment
@DerekPatton: Jauh lebih mudah untuk melakukan interleave dengan panggilan async lainnya. - person Ben Voigt; 06.01.2015
comment
@DerekPatton: Selain itu, saya tidak tahu mengapa menurut Anda mungkin ada keunggulan kinerja. Pembungkus tidak pernah lebih cepat dari fondasi tempat ia bertumpu. - person Ben Voigt; 06.01.2015
comment
Anda mengatakan bahwa API saya menggunakan model Begin/End. Bukan itu. Harap dicatat bahwa saya sendiri yang membuat metode Mulai/Akhir. Apakah ini berarti saya harus membuat metode Begin/End dan kemudian menggunakan panggilan Task.Factory.FromAsync ini setiap kali saya ingin menunggu metode non-async? Juga, apakah ini membuat thread baru per panggilan? - person Derek Patton; 06.01.2015
comment
@DerekPatton: selama API Anda sesuai dengan tanda tangan standar, Anda dapat menggunakan saran Ben. Perhatikan bahwa tidak ada kelebihan yang sama persis dengan metode BeginQuery Anda (sejauh yang saya tahu...ada banyak dan saya mungkin melewatkan sesuatu :)), tetapi Anda dapat menelepon metode dan teruskan hasilnya ke FromAsync: Task.Factory.FromAsync<string>(BeginQuery(MyQueryCallback), EndQuery); (dengan MyQueryCallback adalah metode panggilan balik Anda, metode tersebut akan tetap Anda teruskan ke BeginQuery()). - person Peter Duniho; 06.01.2015
comment
@DerekPatton: Tidak, Anda tidak ingin menulis metode Begin/End hanya untuk tujuan membungkusnya. Namun ada banyak API yang menggunakan Begin/End. Untuk mendapatkan kinerja yang baik, Anda perlu menulis ulang GetResultFromWeb Anda untuk menggunakan semacam API asinkron untuk I/O jaringan. Baik Mulai/Akhir, TPL async, atau apa pun, semuanya dapat diubah menjadi async. Dalam kasus khusus Anda, mungkin lebih baik untuk mem-proxy permintaan menggunakan API async, dan memanggil API pihak ketiga dengan URL lokal yang segera merespons. - person Ben Voigt; 06.01.2015

Cara yang lebih sederhana untuk melakukan apa yang Anda cari adalah dengan memanggil metode menggunakan kelas Task. Dalam kasus Anda, tampilannya akan seperti ini:

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

Meskipun ini akan membuat thread lain seperti yang dilakukan opsi IAsyncResult Anda, ini sangat menyederhanakan prosesnya.

person Jacob Lambert    schedule 06.01.2015
comment
Jawaban yang bagus, namun memiliki metode async yang hanya didelegasikan ke Task.Run tidak disarankan karena metode async biasanya tidak memulai thread baru. - person NeddySpaghetti; 06.01.2015
comment
Bagus, tapi bukankah ini membuat utas baru untuk setiap panggilan GetResultFromWebAsync? Saya pikir APM akan lebih efisien karena tidak akan membuat thread yang berbeda untuk setiap panggilan, melainkan mengantrekan tugas dan mengganti jumlah thread yang jauh lebih kecil untuk tugas-tugas tersebut, sehingga menghilangkan biaya tambahan untuk membuat thread baru. Apakah aku salah? - person Derek Patton; 06.01.2015
comment
@DerekPatton Saya yakin kelas Task menggunakan ThreadPool secara internal jika saya ingat apa yang saya baca itu benar. - person Jacob Lambert; 06.01.2015
comment
@NedStoyanov, lalu apa cara yang lebih baik untuk mengimplementasikan ini? Ini adalah cara terbaik yang saat ini saya ketahui untuk menjadikan metode non async bertindak sebagai metode async, tetapi saya selalu mencari cara untuk meningkatkannya. - person Jacob Lambert; 06.01.2015
comment
Saya pikir rekomendasinya adalah untuk tidak menambahkan metode yang berakhiran xxxxAsync cukup gunakan Task.Run secara langsung - person NeddySpaghetti; 06.01.2015
comment
@DerekPatton Saya benar dalam pemikiran saya bahwa Task menggunakan kumpulan thread CLR secara internal. - person Jacob Lambert; 06.01.2015