Най-добра практика по отношение на задействане и обработка на .NET събития

Очевидно е, че задействането на събития вътре в ключалка (т.е. критична секция) е предразположено към блокиране поради възможността манипулаторът на събития да блокира някои асинхронни операции, които също трябва да придобият същата ключалка. Сега, по отношение на дизайна, има две възможни решения, които ми идват на ум:

  1. Ако е необходимо да се задейства събитие вътре в ключалка, тогава винаги задействайте събитието асинхронно. Това може да се извърши с помощта на ThreadPool, например, ако редът на задействане на събитията няма значение. Ако редът на събитията трябва да бъде запазен, тогава може да се използва единична нишка за задействане на събития за задействане на събитията по ред, но асинхронно.

  2. Класът/библиотеката, която задейства събитията, не трябва да предприема необходимите предпазни мерки, за да предотврати блокировката и просто задейства събитието вътре в заключването. В този случай отговорност на манипулатора на събитието е да обработи събитието асинхронно, ако извършва заключване (или друга блокираща операция) вътре в манипулатора на събитие. Ако манипулаторът на събития не отговаря на това правило, тогава той трябва да понесе последствията в случай, че възникне блокиране.

Честно казано смятам, че вторият вариант е по-добър от гледна точка на принципа за разделяне на проблемите, тъй като кодът за задействане на събитие не трябва да предполага какво може или не може да направи манипулаторът на събитието.

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

Интересувам се да видя дали има други алтернативни решения, които може да съм пренебрегнал и какви възможни предимства/недостатъци и клопки могат да бъдат приписани на всяко възможно решение.

Има ли най-добра практика за този вид ситуация?


person exc    schedule 09.07.2014    source източник


Отговори (2)


Има и трета опция: Отлагане на повдигане на събитието, докато заключването бъде освободено. Обикновено бравите се вземат за кратко време. Обикновено е възможно да се забави повдигането на събитието до след заключване (но в същата нишка).

BCL почти никога не извиква потребителски код под ключалка. Това е изричен техен принцип на проектиране. Например ConcurrentDictionary.AddOrUpdate не се обажда на фабриката, докато е под ключ. Това контраинтуитивно поведение предизвиква много въпроси за препълване на стека, тъй като може да доведе до множество фабрични извиквания за един и същ ключ.

person usr    schedule 09.07.2014
comment
Определено съм съгласен, това е друго възможно решение. Въпреки че е много лесно просто да се види дали методът, който задейства събитието, има заключване в него, обикновено е много трудно или невъзможно да се знае дали един от извикващите методи (във веригата за извикване, водеща до метода за задействане на събитие) вече има поставена ключалка. В такива случаи е много трудно да се задейства събитието извън контекста на заключване. BCL е подходящ пример, но не съм сигурен дали е приложим за код на приложение със сложна бизнес логика, тъй като BCL е просто колекция от независими (обикновено малки и небезопасни за нишки) класове. - person exc; 10.07.2014
comment
@exc прав си: Ако кодът използва силно заключване и дълбоки стекове за извиквания в заключен регион, това е голям проблем. Бих искал да предложа, че може би трябва да преоцените дали такъв сложен код за нишки наистина е най-доброто решение?! Особено като се има предвид, че блокиранията са най-фаталните грешки, тъй като приложението никога не се възстановява без ръчна намеса.; Във вашия случай вероятно е безопасно и лесно да изпратите извикването за обратно извикване към пула от нишки. Това е моят нов отговор. - person usr; 10.07.2014
comment
Искрено ви благодаря за изпратения отговор и бих искал да отбележа, че по принцип съм съгласен с вас и се старая много да не пиша такъв неуправляем код. Моля, имайте предвид обаче, че такъв идеалистичен подход обикновено е малко приложим, когато имате голяма кодова база (милиони редове код) и по-голямата част от нея е наследен код, който е написан от десетки хора и в момента се поддържа от група от минимум 15 човека. Опитвам се постепенно да подобря кода, доколкото мога, и оттам и въпросът ми. - person exc; 10.07.2014
comment
@exc разбирам, разбирам го напълно. Подходът на забавяне вероятно не е приложим. Нито пък пренаписване на кодовата база. - person usr; 10.07.2014

Честно казано смятам, че вторият вариант е по-добър от гледна точка на принципа за разделяне на проблемите, тъй като кодът за задействане на събитие не трябва да предполага какво може или не може да направи манипулаторът на събитието.

Не мисля, че първият вариант нарушава разделянето на загрижеността. Извлечете AsyncEventNotifier клас и делегирайте вашия генериращ събитие обект към него, нещо като (очевидно непълно):

class AsyncEventNotifier
{
    private List<EventListener> _listeners;

    public void AddEventListener(EventListener listener) { _listeners.add(listener); }
    public void NotifyListeners(EventArgs args)
    {
        // spawn a new thread to call the listener methods
    }
    ....
}

class EventGeneratingClass
{
    private AsyncEventHandler _asyncEventHandler;

    public void AddEventListener(EventListener listener) { _asyncEventHandler.AddEventListener(listener); }

    private void FireSomeEvent()
    {
        var eventArgs = new EventArgs();
        ...
        _asyncEventhandler.NotifyListeners(eventArgs);
    }
}

Сега вашият оригинален клас не е отговорен за нищо, за което не е бил отговорен преди. Той знае как да добави слушатели към себе си и знае как да каже на своите слушатели, че е настъпило събитие. Новият клас знае подробностите за изпълнението на „кажи на своите слушатели, че е настъпило събитие“. Редът на слушателите също се запазва, ако е важен. Вероятно обаче не трябва да е така. Ако listenerB не може да обработи събитие, докато listenerA вече не го е обработил, тогава listenerB вероятно се интересува повече от събитие, генерирано от listenerA, отколкото от обекта, който е слушал. Или това, или трябва да имате друг клас, чиято отговорност е да знае реда, в който дадено събитие трябва да бъде обработено.

person Mike B    schedule 09.07.2014
comment
Благодаря за отговора и съжалявам, ако не съм бил достатъчно ясен, но все пак точно това се опитах да опиша в първия вариант. Причината, поради която споменах SoC, е, че кодът трябва да има причина да задейства събитието асинхронно на първо място. Защо да активирате събитие асинхронно? Тъй като манипулаторите могат да блокират. Наистина ли методът на изпичане трябва да вземе предвид това? Ето защо споменах SoC. Между другото, с реда на обработка на събития имах предвид реда на изстрелване на различни събития, съотнесени едно към друго, а не реда на обработка на едно събитие от различни манипулатори. - person exc; 10.07.2014