Чому я повинен створювати операції async WebAPI замість синхронізувати?


109

У веб-API я створив таку операцію:

// GET api/<controller>
[HttpGet]
[Route("pharmacies/{pharmacyId}/page/{page}/{filter?}")]
public CartTotalsDTO GetProductsWithHistory(Guid pharmacyId, int page, string filter = null ,[FromUri] bool refresh = false)
{
    return delegateHelper.GetProductsWithHistory(CustomerContext.Current.GetContactById(pharmacyId), refresh);
}

Виклик до цієї веб-служби здійснюється через дзвінок Jquery Ajax таким чином:

$.ajax({
      url: "/api/products/pharmacies/<%# Farmacia.PrimaryKeyId.Value.ToString() %>/page/" + vm.currentPage() + "/" + filter,
      type: "GET",
      dataType: "json",
      success: function (result) {
          vm.items([]);
          var data = result.Products;
          vm.totalUnits(result.TotalUnits);
      }          
  });

Я бачив деяких розробників, які реалізують попередню операцію таким чином:

// GET api/<controller>
[HttpGet]
[Route("pharmacies/{pharmacyId}/page/{page}/{filter?}")]
public async Task<CartTotalsDTO> GetProductsWithHistory(Guid pharmacyId, int page, string filter = null ,[FromUri] bool refresh = false)
{
    return await Task.Factory.StartNew(() => delegateHelper.GetProductsWithHistory(CustomerContext.Current.GetContactById(pharmacyId), refresh));
}

Треба сказати, що GetProductsWithHistory () - досить тривала операція. З огляду на мою проблему та контекст, як користь для мене асинхронної роботи webAPI?


1
Сторона клієнта використовує AJAX, який вже є асинхронним. Вам не потрібна послуга, яка також може бути записана як async Task<T>. Пам'ятайте, AJAX був втілений ще до того, як TPL навіть існував :)
Домінік Зукевич

65
Вам потрібно зрозуміти, чому ви реалізуєте контролери асинхронізації, багато хто не робить. IIS має обмежену кількість потоків, і коли всі використовуються, сервер не може обробляти нові запити. Якщо контролери асинхронізують процес, коли процес очікує завершення вводу / виводу, його поток звільняється від сервера для обробки інших запитів.
Матія Грчич

3
Які розробники ви бачили, що роблять це? Якщо є повідомлення в блозі або стаття, яка рекомендує цю техніку, будь ласка, опублікуйте посилання.
Стівен Клірі

3
Ви отримуєте повну користь від асинхронізації лише в тому випадку, якщо ваш процес знає про асинхронізацію зверху (включаючи саму веб-програму та ваші контролери) до будь-яких очікуваних дій, що виходять за межі вашого процесу (включаючи затримки таймера, введення / виведення файлів, доступ до БД, і веб-запити, які він робить). У цьому випадку ваш помічник-делегат потребує GetProductsWithHistoryAsync()повернення Task<CartTotalsDTO>. У написанні асинхронного контролера може бути користь, якщо ви також маєте намір перенести виклики, які він робить для асинхронізації; тоді ви починаєте отримувати користь від частин асинхронізації під час міграції решти.
Кіт Робертсон

1
Якщо процес, який ви виконуєте, закінчується і потрапляє в базу даних, то ваша веб-нитка просто чекає, коли вона повернеться і утримує цю нитку. Якщо ви досягли максимальної кількості потоків, і надійде ще один запит, його потрібно почекати. Навіщо це робити? Натомість ви хочете звільнити цей потік від свого контролера, щоб інший запит міг його використовувати, і займати інший веб-потік лише тоді, коли ваш вихідний запит із бази даних повернеться. msdn.microsoft.com/en-us/magazine/dn802603.aspx
user441521

Відповіді:


98

У вашому конкретному прикладі операція зовсім не асинхронна, тому те, що ви робите, - це асинхронізація через синхронізацію. Ви просто випускаєте одну нитку і блокуєте іншу. Причин тому немає, тому що всі потоки є потоками пулу потоків (на відміну від програми GUI).

У своєму обговоренні "асинхронізації через синхронізацію" я настійно запропонував, що якщо у вас є API, який внутрішньо реалізується синхронно, ви не повинні виставляти асинхронний аналог, який просто завершує синхронний метод Task.Run.

З Чи слід виставляти синхронні обгортки для асинхронних методів?

Однак при здійсненні викликів WebAPI asyncтам, де є фактична асинхронна операція (як правило, введення / виведення) замість блокування потоку, який сидить і чекає результату, нитка повертається до пулу потоків і таким чином може виконувати якусь іншу операцію. Крім того, це означає, що ваша програма може більше працювати з меншими ресурсами, а це покращує масштабованість.


3
@efaruk всі теми є робочими нитками. Звільнення однієї теми ThreadPool та блокування іншого безглуздо.
i3arnon

1
@efaruk Я не впевнений, що ти намагаєшся сказати .. але поки ти згоден, немає ніяких причин використовувати асинхронізацію над синхронізацією в WebAPI, то це добре.
i3arnon

@efaruk "async over sync" (тобто await Task.Run(() => CPUIntensive())) марно в asp.net. Ви нічого не отримуєте від цього. Ви просто випускаєте одну нитку ThreadPool, щоб займати іншу. Це менш ефективно, ніж просто викликати синхронний метод.
i3arnon

1
@efaruk Ні, це не розумно. Ви, наприклад, виконуєте самостійні завдання послідовно. Вам справді потрібно прочитати на asyc / дочекатися, перш ніж давати рекомендації. Вам потрібно буде використовувати await Task.WhenAllдля виконання паралельно.
Søren Boisen

1
@efaruk Як пояснює Бойсен, ваш приклад не додає ніякого значення, крім простого виклику цих синхронних методів послідовно. Ви можете використовувати, Task.Runякщо ви хочете паралелізувати навантаження на декілька потоків, але це не означає "асинхронізація через синхронізацію". посилання "асинхронізація над синхронізацією", створюючи метод асинхронізації як обгортка над синхронним. Ви можете побачити цитату в моїй відповіді.
i3arnon

1

Одним із підходів може бути (я це успішно використовував у клієнтських додатках), щоб служба Windows виконувала тривалі операції з робочими потоками, а потім зробіть це в IIS, щоб звільнити потоки до завершення операції блокування. Примітка. Це передбачає результати зберігаються в таблиці (рядки, визначені jobId), і чистіший процес їх очищення через кілька годин після використання.

Щоб відповісти на запитання: "З огляду на мою проблему та контекст, як мені допоможе асинхронна робота webAPI?" враховуючи, що це "досить довга операція", я думаю більше секунд, а не мс, такий підхід звільняє потоки IIS. Очевидно, вам також потрібно запустити службу Windows, яка сама забирає ресурси, але такий підхід може запобігти затопленню повільних запитів від крадіжки потоків з інших частин системи.

// GET api/<controller>
[HttpGet]
[Route("pharmacies/{pharmacyId}/page/{page}/{filter?}")]
public async Task<CartTotalsDTO> GetProductsWithHistory(Guid pharmacyId, int page, string filter = null ,[FromUri] bool refresh = false)
{
        var jobID = Guid.NewGuid().ToString()
        var job = new Job
        {
            Id = jobId,
            jobType = "GetProductsWithHistory",
            pharmacyId = pharmacyId,
            page = page,
            filter = filter,
            Created = DateTime.UtcNow,
            Started = null,
            Finished = null,
            User =  {{extract user id in the normal way}}
        };
        jobService.CreateJob(job);

        var timeout = 10*60*1000; //10 minutes
        Stopwatch sw = new Stopwatch();
        sw.Start();
        bool responseReceived = false;
        do
        {
            //wait for the windows service to process the job and build the results in the results table
            if (jobService.GetJob(jobId).Finished == null)
            {
                if (sw.ElapsedMilliseconds > timeout ) throw new TimeoutException();
                await Task.Delay(2000);
            }
            else
            {
                responseReceived = true;
            }
        } while (responseReceived == false);

    //this fetches the results from the temporary results table
    return jobService.GetProductsWithHistory(jobId);
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.