Як би я синхронно запустив метод асинхронної задачі <T>?


628

Я дізнаюся про асинхронізацію / очікування і наткнувся на ситуацію, коли мені потрібно синхронно викликати метод асинхронізації. Як я можу це зробити?

Метод асинхронізації:

public async Task<Customers> GetCustomers()
{
    return await Service.GetCustomersAsync();
}

Нормальне використання:

public async void GetCustomers()
{
    customerList = await GetCustomers();
}

Я намагався використовувати наступне:

Task<Customer> task = GetCustomers();
task.Wait()

Task<Customer> task = GetCustomers();
task.RunSynchronously();

Task<Customer> task = GetCustomers();
while(task.Status != TaskStatus.RanToCompletion)

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

public static void WaitWithPumping(this Task task) 
{
        if (task == null) throw new ArgumentNullException(“task”);
        var nestedFrame = new DispatcherFrame();
        task.ContinueWith(_ => nestedFrame.Continue = false);
        Dispatcher.PushFrame(nestedFrame);
        task.Wait();
}

Ось виняток і слід стека від виклику RunSynchronously:

System.InvalidOperationException

Повідомлення : RunSynchronrous не може бути викликаний у завданні, яке не пов'язане з делегатом.

InnerException : null

Джерело : mscorlib

StackTrace :

          at System.Threading.Tasks.Task.InternalRunSynchronously(TaskScheduler scheduler)
   at System.Threading.Tasks.Task.RunSynchronously()
   at MyApplication.CustomControls.Controls.MyCustomControl.CreateAvailablePanelList() in C:\Documents and Settings\...\MyApplication.CustomControls\Controls\MyCustomControl.xaml.cs:line 638
   at MyApplication.CustomControls.Controls.MyCustomControl.get_AvailablePanels() in C:\Documents and Settings\...\MyApplication.CustomControls\Controls\MyCustomControl.xaml.cs:line 233
   at MyApplication.CustomControls.Controls.MyCustomControl.<CreateOpenPanelList>b__36(DesktopPanel panel) in C:\Documents and Settings\...\MyApplication.CustomControls\Controls\MyCustomControl.xaml.cs:line 597
   at System.Collections.Generic.List`1.ForEach(Action`1 action)
   at MyApplication.CustomControls.Controls.MyCustomControl.<CreateOpenPanelList>d__3b.MoveNext() in C:\Documents and Settings\...\MyApplication.CustomControls\Controls\MyCustomControl.xaml.cs:line 625
   at System.Runtime.CompilerServices.TaskAwaiter.<>c__DisplayClass7.<TrySetContinuationForAwait>b__1(Object state)
   at System.Windows.Threading.ExceptionWrapper.InternalRealCall(Delegate callback, Object args, Int32 numArgs)
   at MS.Internal.Threading.ExceptionFilterHelper.TryCatchWhen(Object source, Delegate method, Object args, Int32 numArgs, Delegate catchHandler)
   at System.Windows.Threading.DispatcherOperation.InvokeImpl()
   at System.Windows.Threading.DispatcherOperation.InvokeInSecurityContext(Object state)
   at System.Threading.ExecutionContext.runTryCode(Object userData)
   at System.Runtime.CompilerServices.RuntimeHelpers.ExecuteCodeWithGuaranteedCleanup(TryCode code, CleanupCode backoutCode, Object userData)
   at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state)
   at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean ignoreSyncCtx)
   at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
   at System.Windows.Threading.DispatcherOperation.Invoke()
   at System.Windows.Threading.Dispatcher.ProcessQueue()
   at System.Windows.Threading.Dispatcher.WndProcHook(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam, Boolean& handled)
   at MS.Win32.HwndWrapper.WndProc(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam, Boolean& handled)
   at MS.Win32.HwndSubclass.DispatcherCallbackOperation(Object o)
   at System.Windows.Threading.ExceptionWrapper.InternalRealCall(Delegate callback, Object args, Int32 numArgs)
   at MS.Internal.Threading.ExceptionFilterHelper.TryCatchWhen(Object source, Delegate method, Object args, Int32 numArgs, Delegate catchHandler)
   at System.Windows.Threading.Dispatcher.InvokeImpl(DispatcherPriority priority, TimeSpan timeout, Delegate method, Object args, Int32 numArgs)
   at MS.Win32.HwndSubclass.SubclassWndProc(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam)
   at MS.Win32.UnsafeNativeMethods.DispatchMessage(MSG& msg)
   at System.Windows.Threading.Dispatcher.PushFrameImpl(DispatcherFrame frame)
   at System.Windows.Threading.Dispatcher.PushFrame(DispatcherFrame frame)
   at System.Windows.Threading.Dispatcher.Run()
   at System.Windows.Application.RunDispatcher(Object ignore)
   at System.Windows.Application.RunInternal(Window window)
   at System.Windows.Application.Run(Window window)
   at System.Windows.Application.Run()
   at MyApplication.App.Main() in C:\Documents and Settings\...\MyApplication\obj\Debug\App.g.cs:line 50
   at System.AppDomain._nExecuteAssembly(RuntimeAssembly assembly, String[] args)
   at System.AppDomain.ExecuteAssembly(String assemblyFile, Evidence assemblySecurity, String[] args)
   at Microsoft.VisualStudio.HostingProcess.HostProc.RunUsersAssembly()
   at System.Threading.ThreadHelper.ThreadStart_Context(Object state)
   at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean ignoreSyncCtx)
   at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
   at System.Threading.ThreadHelper.ThreadStart()

46
Найкраща відповідь на питання "Як я можу синхронно викликати метод асинхронізації", - це "не робити". Є хаки, щоб спробувати змусити його працювати, але всі вони мають дуже тонкі підводні камені. Натомість створити резервну копію та виправити код, який робить це "необхідним" для цього.
Стівен Клірі

57
@Stephen Cleary Абсолютно згоден, але іноді його просто неминуче, наприклад, коли ваш код залежить від якогось стороннього API, який не використовує async / wait. Крім того, якщо прив'язка до властивостей WPF при використанні MVVM, буквально неможливо використовувати async / wait, оскільки це не підтримується у властивостях.
Контанго

3
@StephenCleary Не завжди. Я будую DLL, який буде імпортований у GeneXus . Він не підтримує ключові слова async / wait, тому я повинен використовувати лише синхронні методи.
Діней

5
@StephenCleary 1) GeneXus - це інструмент 3-го птаха, і я не маю доступу до його вихідного коду; 2) GeneXus навіть не має реалізацій "функцій", тому я не можу усвідомити, як я міг реалізувати "зворотний виклик" з цим типом речі. Звичайно, це було б важче вирішення, ніж використання Taskсинхронно; 3) Я інтегрую GeneXus з драйвером MongoDB C # , який розкриває деякі методи лише асинхронно
Dinei

1
@ygoe: Використовуйте сумісний з асинхронним замком, наприклад SemaphoreSlim.
Стівен Клірі

Відповіді:


456

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

Його можна назвати за допомогою:

customerList = AsyncHelpers.RunSync<List<Customer>>(() => GetCustomers());

Код звідси

public static class AsyncHelpers
{
    /// <summary>
    /// Execute's an async Task<T> method which has a void return value synchronously
    /// </summary>
    /// <param name="task">Task<T> method to execute</param>
    public static void RunSync(Func<Task> task)
    {
        var oldContext = SynchronizationContext.Current;
        var synch = new ExclusiveSynchronizationContext();
        SynchronizationContext.SetSynchronizationContext(synch);
        synch.Post(async _ =>
        {
            try
            {
                await task();
            }
            catch (Exception e)
            {
                synch.InnerException = e;
                throw;
            }
            finally
            {
                synch.EndMessageLoop();
            }
        }, null);
        synch.BeginMessageLoop();

        SynchronizationContext.SetSynchronizationContext(oldContext);
    }

    /// <summary>
    /// Execute's an async Task<T> method which has a T return type synchronously
    /// </summary>
    /// <typeparam name="T">Return Type</typeparam>
    /// <param name="task">Task<T> method to execute</param>
    /// <returns></returns>
    public static T RunSync<T>(Func<Task<T>> task)
    {
        var oldContext = SynchronizationContext.Current;
        var synch = new ExclusiveSynchronizationContext();
        SynchronizationContext.SetSynchronizationContext(synch);
        T ret = default(T);
        synch.Post(async _ =>
        {
            try
            {
                ret = await task();
            }
            catch (Exception e)
            {
                synch.InnerException = e;
                throw;
            }
            finally
            {
                synch.EndMessageLoop();
            }
        }, null);
        synch.BeginMessageLoop();
        SynchronizationContext.SetSynchronizationContext(oldContext);
        return ret;
    }

    private class ExclusiveSynchronizationContext : SynchronizationContext
    {
        private bool done;
        public Exception InnerException { get; set; }
        readonly AutoResetEvent workItemsWaiting = new AutoResetEvent(false);
        readonly Queue<Tuple<SendOrPostCallback, object>> items =
            new Queue<Tuple<SendOrPostCallback, object>>();

        public override void Send(SendOrPostCallback d, object state)
        {
            throw new NotSupportedException("We cannot send to our same thread");
        }

        public override void Post(SendOrPostCallback d, object state)
        {
            lock (items)
            {
                items.Enqueue(Tuple.Create(d, state));
            }
            workItemsWaiting.Set();
        }

        public void EndMessageLoop()
        {
            Post(_ => done = true, null);
        }

        public void BeginMessageLoop()
        {
            while (!done)
            {
                Tuple<SendOrPostCallback, object> task = null;
                lock (items)
                {
                    if (items.Count > 0)
                    {
                        task = items.Dequeue();
                    }
                }
                if (task != null)
                {
                    task.Item1(task.Item2);
                    if (InnerException != null) // the method threw an exeption
                    {
                        throw new AggregateException("AsyncHelpers.Run method threw an exception.", InnerException);
                    }
                }
                else
                {
                    workItemsWaiting.WaitOne();
                }
            }
        }

        public override SynchronizationContext CreateCopy()
        {
            return this;
        }
    }
}

28
Для ознайомлення з тим, як це працює, Стівен Туб (містер Паралель) написав про це ряд публікацій. Частина 1 Частина 2 Частина 3
Камерон Макфарланд

18
Я оновив код Джона, щоб він працював без обгортання завдань у лямбдах: github.com/tejacques/AsyncBridge . По суті, ви працюєте з блоками асинхронізації з використанням оператора. Все, що використовується всередині використовуваного блоку, відбувається асинхронно, із зачеканням у кінці. Мінус полягає в тому, що вам потрібно розгортати завдання самостійно в зворотному дзвінку, але це все ще досить елегантно, особливо якщо вам потрібно викликати відразу кілька функцій асинхронізації.
Том Жак

17
@StephenCleary Хоча я, як правило, погоджуюся з вами, що код повинен бути асинхронізований до кінця, іноді ви опиняєтесь у важкій ситуації, коли доводиться змушувати це робити як синхронний дзвінок. В основному, моя ситуація полягає в тому, що весь код доступу до даних асинхронний. Мені потрібно було створити мапу сайту на основі мапи сайту, і третя сторона бібліотеки, якою я користувався, була MvcSitemap. Тепер, коли хтось розширює його через DynamicNodeProviderBaseбазовий клас, не можна оголосити його як asyncметод. Або мені довелося замінити нову бібліотеку, або просто викликати синхронний оп.
justin.lovell

6
@ justin.lovell: Так, обмеження бібліотеки можуть змусити нас брати хакі, принаймні, поки бібліотека не буде оновлена. Здається, MvcSitemap - одна з таких ситуацій, коли потрібен злом (фільтри MVC та дочірні дії теж); Я просто відвертаю людей від цього взагалі, тому що такі хаки використовуються занадто часто, коли вони не потрібні. Зокрема, з MVC деякі API ASP.NET/MVC припускають, що вони є AspNetSynchronizationContext, тому цей конкретний хак не працюватиме, якщо ви викликаєте ці API.
Стівен Клірі

5
Цей код не працюватиме. Якщо його викликають з потоку пулу, він може викликати глухий кут голоду. Ваш абонент заблокує очікування завершення операції, що ніколи не станеться, якщо він вичерпав пул потоків. Дивіться цю статтю .
ZunTzu

318

Зауважте, що ця відповідь три роки. Я написав це, в основному, на досвіді роботи з .Net 4.0, і дуже мало з 4.5, особливо з async-await. Взагалі кажучи, це приємне просте рішення, але воно іноді ламає речі. Будь ласка, прочитайте обговорення в коментарях.

.Net 4.5

Просто скористайтеся цим:

// For Task<T>: will block until the task is completed...
var result = task.Result; 

// For Task (not Task<T>): will block until the task is completed...
task2.RunSynchronously();

Див .: TaskAwaiter , Task.Result , Task.RunSynchronous


.Net 4.0

Використовуй це:

var x = (IAsyncResult)task;
task.Start();

x.AsyncWaitHandle.WaitOne();

... або це:

task.Start();
task.Wait();

67
.Resultможе призвести до глухого кута в певних сценаріях
Джорді Ланген

122
Resultможе легко викликати тупик у asyncкоді , як я описую в своєму блозі.
Стівен Клірі

8
@StephenCleary Я прочитав ваш пост і спробував сам. Я чесно думаю, що хтось із Microsoft був справді п'яний ... Це те саме питання, що і winforms та фонові теми ....
AK_

9
Питання стосується завдання, яке повертається методом async. Такий вид завдань, можливо, вже був запущений, виконаний або скасований, тому використання методу Task.RunSynchronically може призвести до InvalidOperationException . Див. Сторінку MSDN: Метод Task.RunSynchronically . Крім того, ця задача, ймовірно, створена методами Task.Factory.StartNew або Task.Run (всередині методу async), тому небезпечно спробувати запустити її заново. Деякі умови перегонів можуть виникати під час виконання. З іншого боку, Task.Wait і Task.Result може призвести до тупикової ситуації.
sgnsajgon

4
Запуск Синхронно працював на мене ... Я не знаю, чи щось мені не вистачає, але це здається кращим перед жахами позначеної відповіді - я просто шукав спосіб вимкнення асинхронізації для тестування коду, який просто там зупинився the ui from
vis

121

Здивований, що про це ніхто не згадав:

public Task<int> BlahAsync()
{
    // ...
}

int result = BlahAsync().GetAwaiter().GetResult();

Не настільки гарний, як деякі інші методи тут, але він має такі переваги:

  • він не ковтає винятків (як Wait)
  • він не буде містити будь-яких винятків, кинутих у AggregateException(як Result)
  • працює для обох Taskі Task<T>( спробуйте самі! )

Крім того, оскільки GetAwaiterвведено качка, це повинно працювати для будь-якого об'єкта, який повертається з методу асинхронізації (наприклад, ConfiguredAwaitableабо YieldAwaitable), а не лише Завдання.


редагувати: Зверніть увагу, що такий підхід (або використання .Result) можливий у глухому куточку, якщо ви не обов'язково додаєте .ConfigureAwait(false)щоразу, коли ви очікуєте, для всіх методів асинхронізації, до яких можливо отримати доступ BlahAsync()(не лише тих, до яких він дзвонить безпосередньо). Пояснення .

// In BlahAsync() body
await FooAsync(); // BAD!
await FooAsync().ConfigureAwait(false); // Good... but make sure FooAsync() and
                                        // all its descendants use ConfigureAwait(false)
                                        // too. Then you can be sure that
                                        // BlahAsync().GetAwaiter().GetResult()
                                        // won't deadlock.

Якщо ви лінуєтеся додавати .ConfigureAwait(false)всюди, і вам не важлива ефективність, яку ви можете зробити

Task.Run(() => BlahAsync()).GetAwaiter().GetResult()

1
Для мене працює простий матеріал. Крім того, якщо метод повертає IAsyncOperation, я повинен був спершу перетворити його в завдання: BlahAsync (). AsTask (). GetAwaiter (). GetResult ();
Лі Макферсон

3
Це спричинило тупик всередині веб-методу asmx. Тим не менш, загортання виклику методу в Task.Run () змусило його працювати: Task.Run (() => BlahAsync ()). GetAwaiter (). GetResult ()
Augusto Barreto

Мені найкраще синтаксично подобається такий підхід, оскільки він не передбачає лямбдів.
Дітим

25
Не редагуйте відповіді інших людей, щоб вставити посилання на своє. Якщо ви вважаєте, що ваша відповідь краща, залиште її замість коментаря.
Рейчел

1
docs.microsoft.com/en-us/dotnet/api/… говорить про GetAwaiter()"Цей метод призначений для користувача компілятора, а не безпосередньо в коді".
Теофіл

75

Набагато простіше запустити завдання на пул потоків, ніж намагатися обдурити планувальник, щоб запустити його синхронно. Таким чином, ви можете бути впевнені, що це не буде тупиком. На продуктивність впливає зміна контексту.

Task<MyResult> DoSomethingAsync() { ... }

// Starts the asynchronous task on a thread-pool thread.
// Returns a proxy to the original task.
Task<MyResult> task = Task.Run(() => DoSomethingAsync());

// Will block until the task is completed...
MyResult result = task.Result; 

3
Потім ви викликаєте task.Wait (). Тип даних - це просто завдання.
Майкл Л Перрі

1
Припустимо, що DoSomethingAsync () - це довготривалий метод асинхронізації в цілому (внутрішньо він очікує тривалого завдання), але він повертає керування потоком для свого абонента швидко, таким чином робота лямбда-аргументу також швидко закінчується. Результат Tusk.Run () може мати завдання <Завдання> або Завдання <Завдання <>> , тому ви очікуєте результату зовнішнього завдання, яке швидко виконується, але внутрішнього завдання (через очікування тривалої роботи в методі асинхронізації) все ще працює. Висновки полягають у тому, що, ймовірно, нам потрібно використовувати підхід Unwrap () (як це було зроблено в публікації @ J.Lennon) для досягнення синхронної поведінки методу async.
sgnsajgon

5
@sgnsajgon Ви неправі. Task.Run відрізняється від Task.Factory.StartNew тим, що автоматично автоматично розгортає результат. Дивіться цю статтю .
ZunTzu

1
Чи можу я просто написати Task.Run(DoSomethingAsync)замість цього? Це видаляє один рівень делегатів.
ygoe

1
Так. Хоча в зворотному напрямку, як в Task<MyResult> task = Task.Run(async () => await DoSomethingAsync());більш явному, і вирішує занепокоєння @sgnsajgon, що це може бути повернення Завдання <Завдання <MyResult>>. Правильне перевантаження Task.Run вибирається будь-яким способом, але делегат асинхронізації робить ваш намір очевидним.
Майкл Л Перрі

57

Я дізнаюся про асинхронізацію / очікування і наткнувся на ситуацію, коли мені потрібно синхронно викликати метод асинхронізації. Як я можу це зробити?

Найкраща відповідь - це ти не робиш , деталі залежать від того, яка "ситуація".

Це власник нерухомості / сетер? У більшості випадків краще мати асинхронні методи, ніж "асинхронні властивості". (Для отримання додаткової інформації дивіться мою публікацію в блозі про асинхронні властивості ).

Це додаток MVVM, і ви хочете зробити асинхронну прив'язку даних? Потім використовуйте щось на зразок мого NotifyTask, як описано в моїй статті MSDN про асинхронне прив'язування даних .

Це конструктор? Тоді ви, мабуть, хочете розглянути асинхронний заводський метод. (Для отримання додаткової інформації дивіться мою публікацію в блозі про асинхронні конструктори ).

Майже завжди є краща відповідь, ніж синхронізація над асинхронізацією.

Якщо це неможливо для вашої ситуації (а ви це знаєте, задавши тут питання, що описує ситуацію ), то я б рекомендував просто використовувати синхронний код. Асинхронізувати все найкраще; синхронізація весь шлях є другою найкращою. Не рекомендується синхронізувати асинхронізацію.

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

У такому випадку вам потрібно буде скористатись одним із хаків, описаних у моїй статті про розробку коричневогоasync поля , зокрема:

  • Блокування (наприклад, GetAwaiter().GetResult()). Зауважте, що це може призвести до тупиків (як я описую в своєму блозі).
  • Запуск коду на потоці пулу потоків (наприклад, Task.Run(..).GetAwaiter().GetResult()). Зауважте, що це буде працювати лише в тому випадку, якщо асинхронний код може працювати на потоці пулу потоків (тобто не залежить від контексту інтерфейсу або ASP.NET).
  • Вкладені петлі повідомлень. Зауважте, що це буде працювати лише в тому випадку, якщо асинхронний код передбачає лише однопотоковий контекст, а не конкретний тип контексту (багато UI та ASP.NET код очікує конкретного контексту).

Вкладені петлі повідомлень є найнебезпечнішими з усіх хаків, оскільки це викликає повторне враження . Повторне вступництво вкрай складне, і (IMO) є причиною більшості помилок додатків у Windows. Зокрема, якщо ви знаходитесь у потоці користувальницького інтерфейсу та блокуєте на черзі роботи (очікуючи завершення роботи асинхронізації), CLR насправді виконує певне повідомлення для вас - воно фактично обробляє деякі повідомлення Win32 зсередини вашого код . О, і ви не знаєте, які повідомлення - коли Кріс Брумм каже: "Хіба не було б чудово знати, що саме накачане ? На жаль, накачування - це чорне мистецтво, яке не виходить із смертного розуміння". , то ми насправді не маємо надії знати.

Отже, коли ви блокуєте подібне на потоці інтерфейсу, ви задаєте собі проблеми. Ще одна цитата cbrumme з тієї ж статті: "Час від часу клієнти всередині або за межами компанії виявляють, що ми перекачуємо повідомлення під час керованого блокування на STA [потоці інтерфейсу користувача]. Це законна проблема, оскільки вони знають, що це дуже важко написати код, який є надійним перед реструктуризацією ".

Так. Дуже важко написати код, який є надійним в умовах реструктуризації. І вкладені петлі повідомлень змушують вас писати код, який є надійним перед обличчям. Ось чому прийнята (і найвигідніша) відповідь на це питання є надзвичайно небезпечною на практиці.

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

Якщо ви опинитесь у цьому куточку, я б рекомендував використовувати щось на зразок Dispatcher.PushFrameдля WPF-додатків , циклічного перегляду Application.DoEventsдля додатків WinForm та для загального випадку - власного AsyncContext.Run.


Стівен, є ще одне дуже схоже запитання, на яке ви також дали чудову відповідь. Чи вважаєте ви, що один із них може бути закритим як дублікат або, можливо, запит на об'єднання або спочатку відкритий мета (оскільки кожен q має ~ 200K переглядів 200+ голосів)? Пропозиції?
Олексій Левенков

1
@AlexeiLevenkov: Я не вважаю правильним це робити з кількох причин: 1) Відповідь на пов’язане питання досить застаріла. 2) Я написав цілу статтю на тему, яку, на мою думку, є більш повною, ніж будь-яка існуюча SO Q / A. 3) Прийнята відповідь на це питання надзвичайно популярна. 4) Я жорстоко проти цієї прийнятої відповіді. Отже, закрити це як копію, це було б зловживанням владою; закриваючи, що як копія цього (або злиття) ще більше посилює небезпечну відповідь. Я нехай це і залишає громаді.
Стівен Клірі

Добре. Я розглядаю, як викласти це на мета, ніж якимось чином.
Олексій Левенков

9
Ця відповідь довгий шлях над моєю головою. "Використовувати асинхронізацію до кінця вниз" - це заплутана порада, оскільки явно неможливо дотримуватися. Програма з Main()методом асинхронізації не компілюється; в якому - то момент ви отримали , щоб подолати розрив між синхронними і асинхронними світами. Це не " дуже рідкісна ситуація" , це доводиться буквально в кожній програмі, яка викликає метод асинхронізації. Немає можливості не "виконувати синхронізацію-над-асинхронізацією" , лише параметр перемикати цей тягар до методу виклику, а не зводити його в той, про який ви зараз пишете.
Марк Амері

1
Чудово. asyncЗараз я збираюся застосувати всі методи в моєму додатку. І це багато. Чи не може це бути просто за замовчуванням?
ygoe

25

Якщо я читаю ваше запитання правильно - код, який бажає синхронного виклику методу асинхронізації, виконується в підвішеній диспетчерській потоці. І ви хочете фактично синхронно заблокувати цей потік, поки не буде завершено метод асинхронізації.

Методи асинхронізації на C # 5 використовуються за рахунок ефективного подрібнення методу на шматки під кришкою та повернення, Taskщо дозволяє відстежувати загальне завершення роботи всього шабангу. Однак від того, як виконується подрібнений метод виконання, може залежати від типу виразу, переданого awaitоператору.

Більшу частину часу ви будете використовувати awaitдля виразу типу Task. Реалізація awaitшаблону задачі є "розумною", оскільки вона відкладається до SynchronizationContext, що в основному спричиняє таке:

  1. Якщо потік, що входить до, awaitзнаходиться в потоці циклу повідомлень диспетчера або WinForms, він забезпечує, що фрагменти методу асинхронізації відбуваються як частина обробки черги повідомлень.
  2. Якщо потік, що входить до, awaitє на потоці пулу потоків, то інші фрагменти методу асинхронізації трапляються в будь-якому місці пулу потоків.

Ось чому ви, мабуть, стикаєтеся з проблемами - реалізація методу асинхрон намагається запустити решту на диспетчері - навіть якщо вона призупинена.

.... резервне копіювання! ….

Я маю задати питання, чому ви намагаєтеся синхронно блокувати метод асинхронізації? Це може перемогти мету того, чому метод хотів бути названий асинхронним. Загалом, коли ви почнете використовувати awaitметод диспетчера чи інтерфейсу користувача, вам потрібно буде перетворити весь асинхронний потік потоку інтерфейсу. Наприклад, якщо ваша ставка виклику була приблизно такою:

  1. [Вгору] WebRequest.GetResponse()
  2. YourCode.HelperMethod()
  3. YourCode.AnotherMethod()
  4. YourCode.EventHandlerMethod()
  5. [UI Code].Plumbing()- WPFабо WinFormsКодекс
  6. [Цикл повідомлень] - WPFабо WinFormsцикл повідомлень

Потім, коли код буде перетворений на використання асинхронізації, ти зазвичай закінчуєш

  1. [Вгору] WebRequest.GetResponseAsync()
  2. YourCode.HelperMethodAsync()
  3. YourCode.AnotherMethodAsync()
  4. YourCode.EventHandlerMethodAsync()
  5. [UI Code].Plumbing()- WPFабо WinFormsКодекс
  6. [Цикл повідомлень] - WPFабо WinFormsцикл повідомлень

Фактично відповідаючи

Клас AsyncHelpers вище справді працює, тому що він поводиться як вкладений цикл повідомлень, але він встановлює власну паралельну механіку диспетчеру, а не намагається виконувати на самому диспетчері. Це одне вирішення вашої проблеми.

Іншим вирішенням є виконання методу асинхронізації на потоці нитки, а потім дочекатися її завершення. Це легко - ви можете зробити це за допомогою наступного фрагмента:

var customerList = TaskEx.RunEx(GetCustomers).Result;

Остаточний API буде Task.Run (...), але з CTP вам знадобляться суфікси Ex ( пояснення тут ).


+1 для детального пояснення, однак TaskEx.RunEx(GetCustomers).Resultвисить додаток, коли він запускається на підвішеній диспетчерській потоці. Також метод GetCustomers () зазвичай працює за допомогою асинхронізації, проте в одній ситуації він повинен запускатися синхронно, тому я шукав спосіб це зробити, не будуючи версію методу синхронізації.
Рейчел

+1 для "чому ви намагаєтеся синхронно блокувати метод асинхронізації?" Завжди є спосіб правильно використовувати asyncметоди; вкладених петель неодмінно слід уникати.
Стівен Клірі

24

Це добре працює для мене

public static class TaskHelper
{
    public static void RunTaskSynchronously(this Task t)
    {
        var task = Task.Run(async () => await t);
        task.Wait();
    }

    public static T RunTaskSynchronously<T>(this Task<T> t)
    {
        T res = default(T);
        var task = Task.Run(async () => res = await t);
        task.Wait();
        return res;
    }
}

Вам також потрібно використовувати метод Task.Unwrap , тому що ваш оператор Task.Wait викликає очікування зовнішньої задачі (створеної Task.Run ), а не для внутрішнього очікування. Завдання, передане як параметр методу розширення. Ваш метод Task.Run повертає не Завдання <T>, а Завдання <Завдання <T>>. У деяких простих сценаріях ваше рішення може працювати завдяки оптимізаціям TaskScheduler, наприклад, використовуючи метод TryExecuteTaskInline для виконання завдань у поточному потоці під час операції « Зачекайте». Погляньте на мій коментар до цієї відповіді.
sgnsajgon

1
Це не правильно. Завдання.Run поверне завдання <T>. Дивіться це перевантаження msdn.microsoft.com/en-us/library/hh194918(v=vs.110).aspx
Климент

Як це слід використовувати? Це тупики у WPF:MyAsyncMethod().RunTaskSynchronously();
ygoe

18

Найпростіший спосіб, який я знайшов, щоб запустити завдання синхронно і без блокування потоку інтерфейсу, це використовувати RunSynchronically (), наприклад:

Task t = new Task(() => 
{ 
   //.... YOUR CODE ....
});
t.RunSynchronously();

У моєму випадку у мене є подія, яка спрацьовує, коли щось відбувається. Я не знаю, скільки разів це відбудеться. Отже, я використовую код у своєму випадку, тому щоразу, коли він спрацьовує, він створює завдання. Завдання виконуються синхронно, і це чудово працює для мене. Я просто був здивований, що мені знадобилося так довго, щоб дізнатися про це, враховуючи, наскільки це просто. Зазвичай рекомендації набагато складніші та схильні до помилок. Це було просто і чисто.


1
Але як ми могли використовувати цей метод, коли код асинхронізації повертає щось необхідне?
S.Serpooshan

16

Я стикався з цим кілька разів, в основному при тестуванні одиниць або в розробці сервісу Windows. В даний час я завжди використовую цю функцію:

        var runSync = Task.Factory.StartNew(new Func<Task>(async () =>
        {
            Trace.WriteLine("Task runSync Start");
            await TaskEx.Delay(2000); // Simulates a method that returns a task and
                                      // inside it is possible that there
                                      // async keywords or anothers tasks
            Trace.WriteLine("Task runSync Completed");
        })).Unwrap();
        Trace.WriteLine("Before runSync Wait");
        runSync.Wait();
        Trace.WriteLine("After runSync Waited");

Це просто, легко і у мене не було проблем.


Це єдиний, який не зупинився для мене.
AndreFeijo

15

Я знайшов цей код у компоненті Microsoft.AspNet.Identity.Core, і він працює.

private static readonly TaskFactory _myTaskFactory = new 
     TaskFactory(CancellationToken.None, TaskCreationOptions.None, 
     TaskContinuationOptions.None, TaskScheduler.Default);

// Microsoft.AspNet.Identity.AsyncHelper
public static TResult RunSync<TResult>(Func<Task<TResult>> func)
{
    CultureInfo cultureUi = CultureInfo.CurrentUICulture;
    CultureInfo culture = CultureInfo.CurrentCulture;
    return AsyncHelper._myTaskFactory.StartNew<Task<TResult>>(delegate
    {
        Thread.CurrentThread.CurrentCulture = culture;
        Thread.CurrentThread.CurrentUICulture = cultureUi;
        return func();
    }).Unwrap<TResult>().GetAwaiter().GetResult();
}


13

Лише невелика примітка - такий підхід:

Task<Customer> task = GetCustomers();
task.Wait()

працює для WinRT.

Дозволь пояснити:

private void TestMethod()
{
    Task<Customer> task = GetCustomers(); // call async method as sync and get task as result
    task.Wait(); // wait executing the method
    var customer = task.Result; // get's result.
    Debug.WriteLine(customer.Name); //print customer name
}
public class Customer
{
    public Customer()
    {
        new ManualResetEvent(false).WaitOne(TimeSpan.FromSeconds(5));//wait 5 second (long term operation)
    }
    public string Name { get; set; }
}
private Task<Customer> GetCustomers()
{
    return Task.Run(() => new Customer
    {
        Name = "MyName"
    });
}

Більше того, цей підхід працює лише для рішень Windows Store!

Примітка. Цей спосіб не є безпечним для потоків, якщо ви називаєте свій метод всередині іншого методу асинхронізації (відповідно до коментарів @Servy)


Я пояснив це рішення, перевірте розділ EDIT.
RredCat

2
Це може дуже легко призвести до тупикових ситуацій, коли їх викликають в асинхронних ситуаціях.
Сервіс

@ Сервіс має сенс. Тож як я правильно виправдаю, Wait (timeOut) може допомогти, правда?
RredCat

1
Тоді вам потрібно потурбуватися про те, чи буде досягнутий час очікування, коли операція насправді не виконана, що дуже погано, а також час, витрачений на очікування до закінчення часу, у випадках, коли він замикає (і в такому випадку ви все ще продовжуєте роботу коли це не зроблено). Так ні, це не вирішує проблему.
Сервіс

@Servy Схоже, я маю реалізувати CancellationTokenсвоє рішення.
RredCat

10

У вашому коді ваше перше очікування виконання завдання, але ви його не запускали, тому воно чекає нескінченно. Спробуйте це:

Task<Customer> task = GetCustomers();
task.RunSynchronously();

Редагувати:

Ви кажете, що отримуєте виняток. Будь ласка, опублікуйте більше деталей, включаючи трасування стека
Моно містить такий тестовий випадок:

[Test]
public void ExecuteSynchronouslyTest ()
{
        var val = 0;
        Task t = new Task (() => { Thread.Sleep (100); val = 1; });
        t.RunSynchronously ();

        Assert.AreEqual (1, val);
}

Перевірте, чи працює це для вас. Якщо це не так, хоча це дуже малоймовірно, у вас може бути якась дивна збірка Async CTP. Якщо це все-таки працює, ви можете вивчити, що саме створює компілятор і чим Taskінстанція відрізняється від цього зразка.

Редагувати №2:

Я перевірив за допомогою Reflector, що виняток, який ви описали, виникає, коли m_actionє null. Це щось дивно, але я не є експертом з Async CTP. Як я вже сказав, ви повинні декомпілювати свій код і подивитися, як саме Taskінстанціюється, як це m_actionвідбувається null.


PS Яка угода з випадковими потоками? Хочете допрацювати?


Я підкоригував своє запитання, щоб зробити код, який я намагався трохи зрозуміліше. RunSynchronrous повертає помилку RunSynchronously may not be called on a task unbound to a delegate. Google не допомагає, оскільки всі результати для цього є на китайській мові ...
Рейчел

Я думаю, що різниця полягає в тому, що я не створюю завдання, а потім намагаюся запустити його. Натомість завдання створюється методом async, коли використовується awaitключове слово. Виняток, розміщений у моїх попередніх коментарях, є виключенням, яке я отримую, хоча це одне з небагатьох, яке я не можу Google знайти і вирішити причину.
Рейчел

1
asyncа asyncключові слова - це не що інше, як синтаксичний цукор. Компілятор генерує код для створення Task<Customer>в GetCustomers()так що це , де я виглядав би першим. Що стосується винятку, ви опублікували лише повідомлення про виключення, яке є безрезультатним без винятку типу та сліду стека. Назвіть ToString()метод виключення та опублікуйте висновок у запитанні.
Дан Абрамов

@gaearon: Я розмістив деталі про винятки та стеження стека в своєму первісному запитанні.
Рейчел

2
@gaearon Я думаю, що у вас виникли голоси, оскільки ваша публікація не стосується питань. Дискусія йде про методи асинхрон-очікування, а не про прості методи повернення завдань. Більше того, на мій погляд, механізм асинхронного очікування є синтаксичним цукром, але не настільки тривіальним - є продовження, захоплення контексту, відновлення локального контексту, покращена обробка локальних винятків тощо. Тоді не слід запускати метод RunSynchronrous за результатом методу async, оскільки за визначенням асинхронний метод повинен повертати завдання, яке наразі є щонайменше запланованим, і більше одного разу знаходиться в запущеному стані.
sgnsajgon

9

Перевірено в .Net 4.6. Це також може уникнути тупикової ситуації.

Для повернення методу async Task.

Task DoSomeWork();
Task.Run(async () => await DoSomeWork()).Wait();

Для повернення методу async Task<T>

Task<T> GetSomeValue();
var result = Task.Run(() => GetSomeValue()).Result;

Редагувати :

Якщо абонент працює в потоці пулу потоків (або ж абонент також знаходиться у завданні), він все ще може спричинити тупик у певній ситуації.


1
Мій answar після майже 8 років :) Другий приклад - створить тупик у всіх запланованих контекстах, які в основному використовуються (консольний додаток / .NET core / desktop app / ...). тут у вас є докладніший огляд, про що я зараз говорю: medium.com/rubrikkgroup/…
W92

Resultідеально підходить для роботи, якщо ви хочете синхронний дзвінок, і прямо небезпечний в іншому випадку. Ні в імені, Resultні в суперечливості нічого не Resultвказує, що це дзвінок, що блокує. Це дійсно слід перейменувати.
Зодман

5

використовувати нижче фрагмент коду

Task.WaitAll(Task.Run(async () => await service.myAsyncMethod()));

4

Чому б не створити дзвінок на зразок:

Service.GetCustomers();

це не асинхроніка.


4
Це буде те, що я роблю, якщо не можу працювати так ... створіть версію Sync на додаток до версії Async
Рейчел

3

Ця відповідь призначена для всіх, хто використовує WPF для .NET 4.5.

Якщо ви намагаєтеся виконати Task.Run()на потоці графічного інтерфейсу, він task.Wait()зависне на невизначений термін, якщо у вас немає asyncключового слова у визначенні функції.

Цей метод розширення вирішує проблему, перевіряючи, чи ми знаходимось у потоці GUI, і якщо так, виконуючи завдання на потоці диспетчера WPF.

Цей клас може виступати клеєм між асинхронним / очікуваним світом та не-асинхронним / очікуваним світом у ситуаціях, коли це неминуче, наприклад, властивості MVVM або залежності від інших API, які не використовують async / очікують.

/// <summary>
///     Intent: runs an async/await task synchronously. Designed for use with WPF.
///     Normally, under WPF, if task.Wait() is executed on the GUI thread without async
///     in the function signature, it will hang with a threading deadlock, this class 
///     solves that problem.
/// </summary>
public static class TaskHelper
{
    public static void MyRunTaskSynchronously(this Task task)
    {
        if (MyIfWpfDispatcherThread)
        {
            var result = Dispatcher.CurrentDispatcher.InvokeAsync(async () => { await task; });
            result.Wait();
            if (result.Status != DispatcherOperationStatus.Completed)
            {
                throw new Exception("Error E99213. Task did not run to completion.");
            }
        }
        else
        {
            task.Wait();
            if (task.Status != TaskStatus.RanToCompletion)
            {
                throw new Exception("Error E33213. Task did not run to completion.");
            }
        }
    }

    public static T MyRunTaskSynchronously<T>(this Task<T> task)
    {       
        if (MyIfWpfDispatcherThread)
        {
            T res = default(T);
            var result = Dispatcher.CurrentDispatcher.InvokeAsync(async () => { res = await task; });
            result.Wait();
            if (result.Status != DispatcherOperationStatus.Completed)
            {
                throw new Exception("Error E89213. Task did not run to completion.");
            }
            return res;
        }
        else
        {
            T res = default(T);
            var result = Task.Run(async () => res = await task);
            result.Wait();
            if (result.Status != TaskStatus.RanToCompletion)
            {
                throw new Exception("Error E12823. Task did not run to completion.");
            }
            return res;
        }
    }

    /// <summary>
    ///     If the task is running on the WPF dispatcher thread.
    /// </summary>
    public static bool MyIfWpfDispatcherThread
    {
        get
        {
            return Application.Current.Dispatcher.CheckAccess();
        }
    }
}

3

Просто зателефонувати .Result;або .Wait()є ризиком для тупиків, як багато хто сказав у коментарях. Оскільки більшості з нас подобаються oneliners, ви можете використовувати їх для.Net 4.5<

Отримання значень за допомогою методу асинхронізації:

var result = Task.Run(() => asyncGetValue()).Result;

Синхронно викликає метод асинхронізації

Task.Run(() => asyncMethod()).Wait();

Жодних проблем з тупиком не виникне через використання Task.Run.

Джерело:

https://stackoverflow.com/a/32429753/3850405


1

Я думаю, що наступний допоміжний метод також може вирішити проблему.

private TResult InvokeAsyncFuncSynchronously<TResult>(Func< Task<TResult>> func)
    {
        TResult result = default(TResult);
        var autoResetEvent = new AutoResetEvent(false);

        Task.Run(async () =>
        {
            try
            {
                result = await func();
            }
            catch (Exception exc)
            {
                mErrorLogger.LogError(exc.ToString());
            }
            finally
            {
                autoResetEvent.Set();
            }
        });
        autoResetEvent.WaitOne();

        return result;
    }

Можна використовувати наступним чином:

InvokeAsyncFuncSynchronously(Service.GetCustomersAsync);

1
Поясніть, будь ласка, голосування
donttellya

2
... Мене все ще нетерпляче цікавить, чому за цю відповідь було проголосовано?
donttellya

Це не справжній "синхронний" .Y потрібно створити дві нитки і чекати перших результатів інших.
тмт

і все вбік, це дуже погана ідея.
Ден комора

1
Я просто написав майже однаковий код (рядок за рядком той самий), але натомість використовував SemaphoreSlim замість події автоматичного скидання. Бажаю, я раніше це бачив. Я вважаю такий підхід для запобігання тупикових ситуацій і зберігає ваш код асинхронізації таким же, як і у справжніх асинхронних сценаріях. Не дуже впевнений, чому це погана ідея. Здається, набагато чіткіше, ніж інші підходи, які я бачив вище.
тмрог

0

Це працює для мене

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApp2
{
    public static class AsyncHelper
    {
        private static readonly TaskFactory _myTaskFactory = new TaskFactory(CancellationToken.None, TaskCreationOptions.None, TaskContinuationOptions.None, TaskScheduler.Default);

        public static void RunSync(Func<Task> func)
        {
            _myTaskFactory.StartNew(func).Unwrap().GetAwaiter().GetResult();
        }

        public static TResult RunSync<TResult>(Func<Task<TResult>> func)
        {
            return _myTaskFactory.StartNew(func).Unwrap().GetAwaiter().GetResult();
        }
    }

    class SomeClass
    {
        public async Task<object> LoginAsync(object loginInfo)
        {
            return await Task.FromResult(0);
        }
        public object Login(object loginInfo)
        {
            return AsyncHelper.RunSync(() => LoginAsync(loginInfo));
            //return this.LoginAsync(loginInfo).Result.Content;
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            var someClass = new SomeClass();

            Console.WriteLine(someClass.Login(1));
            Console.ReadLine();
        }
    }
}

-1

Я виявив, що SpinWait працює досить добре для цього.

var task = Task.Run(()=>DoSomethingAsyncronous());

if(!SpinWait.SpinUntil(()=>task.IsComplete, TimeSpan.FromSeconds(30)))
{//Task didn't complete within 30 seconds, fail...
   return false;
}

return true;

Вищеописаний підхід не потребує використання .Result або .Wait (). Це також дозволяє вказати тайм-аут, щоб ви не затримувались назавжди у випадку, якщо завдання ніколи не завершиться.


1
Оголошення говорить, що цей метод не любить когось. Хтось може прокоментувати мінус цього?
Grax32

У разі відсутності прихильника, чому сказали ЧОМУ дано посилання, хтось може його висловити? :-)
Кертіс

1
Це опитування (обертання), делегат буде приймати нитку з пулу до 1000 разів за секунду. Він може не повернути керування відразу після завершення завдання (до 10 + мс помилка). Якщо закінчиться тайм-аут, завдання продовжить працювати, що робить тайм-аут практично марним.
Сінатр

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

-3

На wp8:

Загорніть його:

Task GetCustomersSynchronously()
{
    Task t = new Task(async () =>
    {
        myCustomers = await GetCustomers();
    }
    t.RunSynchronously();
}

Назви це:

GetCustomersSynchronously();

3
Ні, це звичайно не спрацює, тому що завдання не чекає делегата від конструктора (його делегата, а не завдання ..)
Rico Suter

-4
    private int GetSync()
    {
        try
        {
            ManualResetEvent mre = new ManualResetEvent(false);
            int result = null;

            Parallel.Invoke(async () =>
            {
                result = await SomeCalcAsync(5+5);
                mre.Set();
            });

            mre.WaitOne();
            return result;
        }
        catch (Exception)
        {
            return null;
        }
    }

-5

Або ви могли просто піти з:

customerList = Task.Run<List<Customer>>(() => { return GetCustomers(); }).Result;

Для компіляції переконайтеся, що ви посилаєтеся на збірку розширень:

System.Net.Http.Formatting

-9

Спробуйте наступний код, який він працює для мене:

public async void TaskSearchOnTaskList (SearchModel searchModel)
{
    try
    {
        List<EventsTasksModel> taskSearchList = await Task.Run(
            () => MakeasyncSearchRequest(searchModel),
            cancelTaskSearchToken.Token);

        if (cancelTaskSearchToken.IsCancellationRequested
                || string.IsNullOrEmpty(rid_agendaview_search_eventsbox.Text))
        {
            return;
        }

        if (taskSearchList == null || taskSearchList[0].result == Constants.ZERO)
        {
            RunOnUiThread(() => {
                textViewNoMembers.Visibility = ViewStates.Visible;                  
                taskListView.Visibility = ViewStates.Gone;
            });

            taskSearchRecureList = null;

            return;
        }
        else
        {
            taskSearchRecureList = TaskFooterServiceLayer
                                       .GetRecurringEvent(taskSearchList);

            this.SetOnAdapter(taskSearchRecureList);
        }
    }
    catch (Exception ex)
    {
        Console.WriteLine("ActivityTaskFooter -> TaskSearchOnTaskList:" + ex.Message);
    }
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.