Cara membuat dan mengimplementasikan antarmuka untuk operasi yang terkadang tidak sinkron

Katakanlah saya memiliki 100 kelas yang mengimplementasikan antarmuka umum dengan metode "menghitung". Beberapa kelas akan menjalankan async (misalnya membaca file), dan kelas lain yang mengimplementasikan antarmuka yang sama akan mengeksekusi kode yang disinkronkan (misalnya menambahkan dua angka). Apa cara yang baik untuk mengkodekan ini, untuk pemeliharaan dan kinerja?

Postingan yang saya baca sejauh ini, selalu merekomendasikan untuk membuat metode async/menunggu muncul di hadapan penelepon. Jadi, jika Anda memiliki satu operasi yang async, jadikan pemanggilnya async, lalu pemanggilnya async, dan seterusnya. Jadi ini membuat saya berpikir bahwa antarmukanya harus berupa antarmuka async. Namun, hal ini menimbulkan masalah ketika mengimplementasikan antarmuka dengan kode yang sinkron.

Salah satu ide yang saya pikirkan adalah mengekspos 2 metode antarmuka, satu async dan satu sinkronisasi, dan satu properti boolean untuk memberi tahu pemanggil metode mana yang harus dipanggil. Ini akan terlihat sangat jelek.

Kode yang saya miliki saat ini hanya satu metode antarmuka yaitu async. Kemudian untuk implementasi yang sinkron, mereka membungkus kode di dalam objek Task:

using System.IO;
using System.Threading.Tasks;

namespace TestApp
{
    interface IBlackBox
    {
        Task<string> PullText();
    }

    sealed class MyAsyncBlackBox : IBlackBox
    {
        public async Task<string> PullText()
        {
            using (var reader = File.OpenText("Words.txt"))
            {
                return await reader.ReadToEndAsync();
            }
        }
    }

    sealed class MyCachedBlackBox : IBlackBox
    {
        public Task<string> PullText()
        {
            return Task.Run(() => "hello world");
        }
    }
}

Apakah ini pendekatan yang tepat untuk membuat dan mengimplementasikan antarmuka yang terkadang tidak sinkron? Saya memiliki banyak kelas yang mengimplementasikan operasi sinkron pendek, dan khawatir hal ini dapat menambah banyak overhead. Apakah ada cara lain untuk melakukan ini yang saya lewatkan?


person cat_in_hat    schedule 01.04.2019    source sumber
comment
Saya rasa Anda memiliki ide yang tepat dengan membuat antarmuka mengembalikan Tugas, tetapi implementasi sinkron Anda salah. Jangan gunakan Task.Run(...), gunakan Task.FromResult(...);. Perpustakaan Anda tidak boleh menggunakan Task.Run(...) karena itu akan menggunakan thread baru (semacamnya, ini lebih rumit dari itu) daripada hanya berjalan di thread yang sedang dijalankan.   -  person Nelson    schedule 01.04.2019
comment
Menurut Anda mengapa Anda perlu memiliki satu properti boolean untuk memberi tahu pemanggil metode mana yang harus dipanggil?   -  person Enigmativity    schedule 01.04.2019
comment
Saya tidak memerlukan boolean, itu adalah salah satu ide untuk menghindari membungkus kode di dalam Task.   -  person cat_in_hat    schedule 01.04.2019
comment
Komentar dari @Nelson sepertinya menjadi jawaban terbaik   -  person cat_in_hat    schedule 02.04.2019


Jawaban (3)


Ini adalah situasi umum dengan antarmuka. Jika Anda memiliki kontrak yang perlu menentukan task untuk Pola Tunggu Async dan kami harus menerapkan Task tersebut di antarmuka.

Dengan asumsi penelepon akan menggunakan await Anda cukup membuang async dan mengembalikan Task.

Namun, Anda harus berhati-hati dengan pengecualian Anda. Diasumsikan bahwa pengecualian ditempatkan pada tugas. Jadi untuk menjaga pipa ledeng ini, penelepon berharap Anda harus menanganinya sedikit berbeda.

Penggunaan umum

Standar async

public async Task<string> PullText()
{
   using (var reader = File.OpenText("Words.txt"))
   {
      return await reader.ReadToEndAsync();
   }
}

Mengembalikan Task untuk pekerjaan terikat CPU (menangkap pengecualian dan menempatkannya di Task)

public Task<string> PullText()
{
   try
   {
      return Task.Run(() => DoCpuWork());
   }
   catch (Exception e)
   {
      return Task.FromException<string>(e);
   }
}

Sedikit kurang efisien karena kami memasang pipa IAsyncStateMachine

public async Task<string> PullText()
{
    return await Task.Run(() => DoCpuWork());
}

Mengembalikan selesai Task dengan hasil sederhana (menangkap pengecualian dan menempatkannya di Task)

public Task<string> PullText()
{
   try
   {
      // simplified example
      return Task.FromResult("someString");
   }
   catch (Exception e)
   {
      return Task.FromException<string>(e);
   }
}

Ada juga pendekatan ke-3, Anda dapat menggunakan kata kunci async, dan pragma mengeluarkan peringatan, ini menangani semantik kesalahan untuk Anda. Ini terasa sedikit kotor bagi saya, hanya karena terlihat berantakan dan perlu pragma mengeluarkan peringatan, meskipun sekarang saya telah melihat ini digunakan di perpustakaan produksi yang dipesan lebih dahulu

#pragma warning disable 1998
public async Task<string> PullText()()
#pragma warning restore 1998
{
    return Task.Run(() => "hello world");
}

Dan

#pragma warning disable 1998
public async Task<string> PullText()()
#pragma warning restore 1998
{
    return Task.FromResult("someString");
}

Perhatikan semua hal di atas berkaitan dengan pengembalian Task<T> dari metode. Jika seseorang hanya ingin mengembalikan Task Anda dapat memanfaatkan Task.CompletedTask; dengan semantik kesalahan yang sama seperti di atas.

person TheGeneral    schedule 01.04.2019
comment
Setuju dengan jawaban ini secara umum, tetapi tidak dengan penggunaan Task.Run. IMO Task.Run harus digunakan hanya jika diperlukan oleh kode yang memakan, yakni ketika dipanggil dari thread UI tetapi tidak dari thread pool thread. - person Stephen Cleary; 01.04.2019
comment
Terima kasih @StephenCleary, saya membeli buku Anda! - person cat_in_hat; 23.10.2019

Biasanya dalam kasus ini, Anda memiliki sesuatu di depan panggilan yang menangani permintaan dan meneruskannya ke kelas "pekerja" (mis. TestApp). Jika ini masalahnya, saya tidak mengerti mengapa memiliki antarmuka "IAsyncable" di mana Anda dapat menguji apakah kelas tersebut berkemampuan async tidak akan berfungsi.

if(thisObject is IAscyncAble) {
  ... call the ansync request.
}
person alwayslearning    schedule 01.04.2019

Saya akhirnya menggunakan kode berikut:

using System.IO;
using System.Threading.Tasks;

namespace TestApp
{
    interface IBlackBox // interface for both sync and async execution
    {
        Task<string> PullText();
    }

    sealed class MyAsyncBlackBox : IBlackBox
    {
        public async Task<string> PullText()
        {
            using (var reader = File.OpenText("Words.txt"))
            {
                return await reader.ReadToEndAsync();
            }
        }
    }

    sealed class MyCachedBlackBox : IBlackBox
    {
        public Task<string> PullText() // notice no 'async' keyword
        {
            return Task.FromResult("hello world");
        }
    }
}
person cat_in_hat    schedule 02.04.2019