Как я могу запустить код, который имеет только асинхронные API без блокировки, в среде, где я знаю, что Пул потоков будет насыщен? - PullRequest
0 голосов
/ 01 июля 2019

Я пытаюсь написать вспомогательные методы для вызова из BizTalk. BizTalk не понимает Tasks или async / await, поэтому вспомогательные методы должны возвращать нормальный тип возврата .NET, не поднятый в Task<T>.

Вспомогательные методы используют библиотеку IdentityModel.Clients.ActiveDirectory , а затем HttpClient , чтобы вызвать API-интерфейс на основе HTTP и затем кэшировать этот результат. Эти классы имеют только асинхронный API, то есть все методы, которые выполняют работу, возвращают Task<T> и заканчиваются на -Async.

Способ, которым BizTalk управляет своим пулом потоков, по существу гарантирует, что пул потоков будет насыщен (по умолчанию 25 рабочих потоков), если он имеет высокую нагрузку на сообщения; например, большое количество файлов было удалено одновременно - это выполнимый сценарий при обычном использовании и на самом деле не проблема. Я наблюдал это через отладку.

Когда вспомогательный код выполняет вызов API, это довольно дорого, поскольку он возвращает много данных, и я хочу, чтобы одновременно выполнялся только один вызов. Если бы вся реализация была синхронной, я бы просто использовал оператор lock для обновления кэша, поскольку задержка при обработке сообщений приемлема для обеспечения синхронизации. Блокировка оказалась тупиковой, что для меня имеет смысл, поскольку архитектура приложения по существу гарантирует, что для выполнения асинхронных методов не будет доступно ни одного потока. Это более экстремальный случай, когда обычно дается совет не блокировать в контексте асинхронного кода, так как он не просто вероятен, но уверен в тупиковой ситуации.

Я пытался использовать SemaphoreSlim.WaitAsync, чтобы сделать эквивалент блокировки, но неблокирующим способом, то есть по-прежнему препятствовать тому, чтобы более чем один поток входил в блок, но заставляя их уступать вместо блокировки. Это не решает проблему, так как вспомогательные методы верхнего уровня все еще должны блокироваться, чтобы дождаться завершения обновления кэша. Моя гипотеза состоит в том, что в момент, когда это ожидание приводит к потоку, он поглощается обработкой нового сообщения, которое затем блокирует его, предотвращая продолжение потока, который вошел в семафор.

Следующий псевдокод (т.е., пожалуйста, не пытайтесь исправить меня по вопросам стиля кодирования, не относящимся к проблеме. Это также не может быть MCVE, если у вас не установлена ​​установка BizTalk), иллюстрирующая Проблема, которую я пытаюсь решить:

public class Helper
{
    public static string GetCachedValue(string key)
    {
        // need to wait until the cache is updated, but blocking here makes all worker threads unavailable
        CacheData().Wait();

        return _cache.GetValue(key);
    }

    private static DateTime _lastRead;

    private static readonly Dictionary<string, string> Cache = new Dictionary<string, string>();

    private static readonly SemaphoreSlim throttle = new SemaphoreSlim(1);

    private static async Task CacheData()
    {
        try{
            // stop more than one thread from entering this block at a time
            await throttle.WaitAsync();

            if(_lastRead < DateTime.Now.AddMinutes(-10))
            {
                var context = new AuthenticationContext(/* uninteresting parameters*/); 
                var token = await context.GetTokenAsync();

                // can't use HttpClientFactory here because the .NET 4.5.2 implementation doesn't supply any way of setting the web proxy
                var client = new HttpClient();

                var data =  await client.GetAsync("api/Data");

                // unimportant cache update code

                _lastRead = DateTime.Now;

            }
        }
        finally 
        {
            throttle.Release();
        }
    }
}
  • Правильно ли мое понимание того, в чем заключается основная проблема?
  • Как мне это решить?

Ответы [ 3 ]

2 голосов
/ 01 июля 2019

Ваш вопрос на самом деле «как мне выполнить синхронизацию по асинхронности, когда пул потоков заполнен», и единственный реальный ответ - «вы не можете».Проблема с синхронизацией по асинхронности заключается в том, что она блокирует поток, а затем может потребоваться другой поток, чтобы разблокировать этот поток.

Одна вещь, которую вы можете попробовать, - это установить собственный контекст (временно) в вашей теме.У меня есть AsyncContext тип в моей AsyncEx библиотеке , которая может это сделать.Таким образом, в вашей точке входа в BizTalk вы можете использовать это вместо прямой блокировки:

// Old code:
//   var result = MyLogicAsync().GetAwaiter().GetResult();
var result = AsyncContext.Run(() => MyLogicAsync());

Это позволит await продолжениям работать в вашем собственном потоке по умолчанию.Он ведет себя подобно циклу сообщений в пользовательском интерфейсе (только без пользовательского интерфейса).

К сожалению, вы не можете гарантировать, что это всегда будет работать, потому что продолжения захватывают только этот контекст по умолчанию ,А для библиотек общего назначения, таких как ActiveDirectory и HttpClient, захват контекста считается плохой практикой;большая часть библиотечного кода старается изо всех сил использовать пул потоков, всегда используя ConfigureAwait(false).

Таким образом, единственный способ избежать взаимных блокировок в коде синхронизации по асинхронности - убедиться, что пул потоков не насыщен,Если бы был какой-то способ ограничить BizTalk каким-либо значением и иметь пул потоков больше этого, тогда это сработало бы.

Гораздо более идеальным решением было бы пойти "полностью синхронизировать".Вы хотели бы заменить HttpClient на WebClient, но из вашего описания это звучит так: ActiveDirectory не поддерживает API синхронизации.

Возможно, вам не подходит гибридное решение: используйте WebClientсделать синхронный вызов API и обернуть все ActiveDirectory вызовы в AsyncContext.Run.Я думаю, что может работать, потому что команда ASP.NET Core удалила большинство / все ConfigureAwait(false) вызовов в своем коде.Таким образом, HTTP API будет синхронным, а ActiveDirectory будет асинхронным, но его продолжения будут выполняться в ожидающем его потоке (не требуя потока пула потоков).

Но даже если вы получите этоработает, это не гарантировано в будущем.Отсутствие ConfigureAwait(false) можно считать «ошибкой», и если они «исправят» ошибку, добавив ConfigureAwait(false) обратно, то ваш код снова будет подвержен взаимоблокировкам.

Единственное действительно гарантированное решение будетчтобы заставить асинхронные API-интерфейсы быть синхронными, написав собственный прокси-сервер WebAPI, который упаковывает вызовы ActiveDirectory.Тогда ваш код BizTalk будет взаимодействовать с этим API (и другим API), используя WebClient, и весь код BizTalk будет синхронным в этот момент.

0 голосов
/ 02 июля 2019

Если количество потоков является основной проблемой, вы можете уменьшить или увеличить количество потоков, добавив адрес и параметр maxconnection в раздел connectionMangement файла BTSNTSvc64.exe.config или BTSNTSvc.exe.config, если это 32 бит. Или, как сказал Джонс в своем ответе, если вы хотите, чтобы он был однопоточным, установите флажок Заказанная доставка в порту отправки.

    <system.net>
        <connectionManagement>
            <add address="*" maxconnection="25"   />

            <add address="https://LIMITEDSERVER.com*" maxconnection="5"  />
            <add address="https://GRUNTYSERVER.com*" maxconnection="50"  />
       </connectionManagement>
    </system.net>

Если вы хотите использовать Async, вам нужно иметь односторонний порт отправки в BizTalk для связи с вашей целевой системой и пункт приема в BizTalk, на который другая система может отправить ответ.

0 голосов
/ 01 июля 2019

Прежде всего, я должен сказать, что создается впечатление, что вы просто создаете много ненужных проблем, пытаясь делать все эти причудливые вещи в классе помощников. Потому что A) действительно нет практического случая для асинхронных операций в классе помощника, так как он не собирается изменять время обработки сообщения, B) вы должны не вызывать конечные точки http в классе помощника .

Таким образом, правильное решение - вызвать конечную точку http, используя Заказанный порт отправки доставки .

Я говорю «заказанная доставка» из-за этого: «и я хочу, чтобы одновременно выполнялся только один звонок», но ... это действительно не имеет значения, что вы хотите , вы должны сделать последовательные вызовы, только если служба требует этого или имеет проблемы с пропускной способностью.

Если вы хотите внутренне кэшировать результаты, есть много примеров этого, например, здесь (применяются все правила .Net): Реализация кэширования для ваших приложений BizTalk с использованием «статических» классов и методов

...