Pull to refresh

Comments 94

И в чем же преимущество EventHandler<> над Action или MyDelegate?


Есть и еще способ — создать новый класс, унаследованный от Delegate или MulticastDelegate, но это уже для совсем специфичных случаев. Фактически, способ 2 это и делает за кулисами.

Как вы вообще это себе представляете?
error CS0644: 'ExampleHandler' cannot derive from special class 'MulticastDelegate'

Я ведь в конце об этом написал. Смотрите на класс AsyncEventsHelper. Такой фокус бы не удался при использовании Action или MyDelegate. А в большинстве случаев (по моим личным наблюдениям) данный способ оповещения вполне подходит.

Считайте это упрощением работы событий для частного случая. Но дело в том, что этот частный случай встречается чаще других при разработке.

При использовании Action<> с одним параметром такой фокус делается столь же просто.

Если событие что-то возвращает — то у него, во-первых, не может быть более 1 подписчика — а во-вторых, нам в любом случае надо дождаться результата. В итоге никакой AsyncEventsHelper попросту не нужен.

Может быть несколько подписчиков, просто результут вернётся только из последнего

Неявно забытое возвращаемое значение — это ошибка. Если его старались-возвращали — значит, оно кому-то было нужно.

Оно не забытое оно потерянное :)
Но конечно впринципе использовать события для возврата значения хоть и известная практика, но она плохая именно по этой причине.


Я не призывал, так делать, а говорил именно о том что это возможно, хоть и следует этого избегать.

я же исправил. принимает а не возвращает… или имеет аргументы если хотите…

Я ответил до того как вы исправили.

Ну хорошо, а если надо больше аргументов события? В этом случае вам придется либо на каждое количество аргументов писать отдельный дженерик метод, либо делать свою собственную реализацию EventArgs, но зачем, если она уже есть?

Реализация EventArgs:


[Serializable]
[System.Runtime.InteropServices.ComVisible(true)]
public class EventArgs {
    public static readonly EventArgs Empty = new EventArgs();

    public EventArgs() 
    {
    }
}

Да, действительно, написать свой собственный аналог этого класса — так сложно!


Вы понимаете, что у приведенного вами же класса MyEventArgs можно "оторвать" базовый тип — и от этого ничего не изменится (после перехода на Action)?

А зачем? Он ведь уже есть. Я ведь сказал, что в большинстве случаев будет достаточно этого. Но я не видел пока живых примеров, где не хватало бы EventHandler

Вообще-то, это был мой вопрос.


А зачем? Зачем использовать EventHandler-то?

Ну да… это как вопрос «а зачем мне использовать готовый велосипед, я изобрету свой»… Да пожалуйста

Почему стандартный класс Action<> называется велосипедом? С каким пор ключевое слово delegate стало признаком велосипеда?

Класс Action<> не является велосипедом. Только в данном случае он неуместен. Это как «string1» + «string2» + «string3».
Класс Action<> не является велосипедом. Только в данном случае он неуместен.

Почему?

Чтобы не писать EventHandler вокруг одного строкового значения.

Ну хорошо, а если надо больше аргументов события?

Берется любой класс, в чем проблема-то?

Ну опишите универсальный способ для многопоточной обработки событий. Мой способ влазит в несколько строк (я не учитываю тут правильную обработку потоков, у меня она неправильная, но я и не ставил цель сделать ее правильной)
Ну опишите универсальный способ для многопоточной обработки событий.

Задача неполна, поэтому и решения ей быть не может. Если вам надо fire-and-forget — берете очередь и пишете в нее, все остальное вас не волнует.

Так… Задача вполне конкретна. у меня есть генератор событий, который принимает хандлер типа EventHandler TEventArgs и с ним работает. А в случае с Action<> у вас будет 19 таких генераторов по количеству разновидностей Action. То есть
для Action

для Actionдля Action<T, T>
и тд до 18 аргументов
Задача вполне конкретна. у меня есть генератор событий, который принимает хандлер типа EventHandler и с ним работает. А в случае с Action<> у вас будет 19 таких генераторов по количеству разновидностей Action.

Неа. Я просто ограничу "события" Action<T> и все.


(собственно, в Rx так и сделано)

А что у вас будет в качестве T?

В смысле "будет ли он ограничен"? Нет, не будет. Зачем?

в смысле какого типа аргумент будет иметь ваше событие

Ну так от события зависит. Если у меня конкретное событие принимает строку, будет Action<string>, если http-запрос — Action<HttpRequestMessage>, ну и так далее.

Да, но экземпляров, порождающих событие может быть несколько и обработчику может захотеться узнать, какой экземпляр породил событие. А так же может быть разное количество и типы аргументов. По этому в конечном итоге у вас таки либо будет несколько универсальных асинхронных порождателя событий, либо вы придете к общему виду, типа

Action<object, MyClass>

Action — это тоже делегат. и казалось бы, почему бы тогда не использовать его?

но вы ведь не обязательно будете порождать собственные события. Что, если вы захотите породить событие, описанное где то в дебрях фрэймворка? А вот они то как раз являются чем то на подобие

SomeNetEventHandler: EventHandler<SomeNeteventArgs>

и тут мой универсальный метод будет кстати. Я ответил на ваш вопрос?
Да, но экземпляров, порождающих событие может быть несколько и обработчику может захотеться узнать, какой экземпляр породил событие.

Обработчику может хотеться что угодно, набор предоставляемых данных определяется их источником.


Action<object, MyClass>

Достаточно Action<T>, где T определяется каждым событием, либо Action<TSource,TArgs>, если я хочу передавать источник.


Вы, кстати, не представляете себе, как меня достал object sender в стандартных событиях.


Что, если вы захотите породить событие, описанное где то в дебрях фрэймворка?

Напишу адаптер: (Action<T>) (T args) => handler(this, args).


Другое дело, что не захочу.


Я ответил на ваш вопрос?

Эээ, простите, какой вопрос?

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

Достаточно Action<T>, где T определяется каждым событием, либо Action<TSource,TArgs>, если я хочу передавать источник.

Вы, кстати, не представляете себе, как меня достал object sender в стандартных событиях.


Почему же не представляю? Представляю. Но иногда за универсальность приходится платить.
Иногда один обработчик может использоваться для аналогичных событий разных типов. И если породитель события нам в данном случае не интересен, то object sender мы оставим без внимания, а вместо этого обработаем аргументы события.

Согласен, чаще всего универсальность в этом излишня.
Сейчас уберу из статьи гневные комментики по поводу Action
Но иногда за универсальность приходится платить.

Так вот я не хочу платить за ненужную мне универсальность моим же неудобством. Особенно когда альтернативные решения не страдают таким неудобством.

Вы же вкурсе что на каждый Begin<Method> должен быть свой End<Method>? ;)

Я в курсе, что есть такой функционал. Нужно это для ожидания завершения и получения результата. Но ИвентХандлеры возвращают void. Поэтому, если дожидаться выполнения работы обработчика нам не нужно, использовать EndInvoke незачем.

Если я чего то не знаю, расскажите, я сам не так давно начал копать систему работы событий и буду рад любым замечаниям.

Вообще-то, метод EndInvoke еще и освобождает ресурсы!


Если вам не нужен результат — то существует очень простое решение:


var d = (EventHandler<T>)delegates[i];
d.BeginInvoke(sender, e, d.EndInvoke, null);

А пропускать вызов End-метода не следует.

Поэтому, если дожидаться выполнения работы обработчика нам не нужно, использовать EndInvoke незачем.

Угу.


"No matter which technique you use, always call EndInvoke to complete your asynchronous call." (MSDN)

Для начала можно почитать документацию: https://msdn.microsoft.com/en-us/library/2e08f6yc(v=vs.110).aspx
Вот цитата с доки:


No matter which technique you use, always call EndInvoke to complete your asynchronous call.

Далее если спросить у гугла, то вам явно ответят на stakcoverflow, что таки да, вы ОБЯЗАНЫ вызывать EndInvoke.


Есть исключение в WinForms приложенях, там вызывать EndInvoke не обязательно, но об этом тоже явно написано в документации.

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

Круто, а теперь ответьте на свой же вопрос: в каком потоке выполняется событие?


Вот, значит, кто-то подписал на ваше событие две сотни обработчиков. Сколько будет потоков? Откуда они возьмутся? А откуда они возьмутся при втором вызове того же события?


Колбэк в данном примере нам не нужен.

Агу. А что будет, если в обработчике возникнет ошибка (exception кто-то бросит)?

Круто, а теперь ответьте на свой же вопрос: в каком потоке выполняется событие?

Тут из-за нечеткой терминологии могут возникнуть непонятки у нас. Событие не выполняется, оно генерируется. Ну или как то так. А выполняются обработчики. Так вот в моем примере каждый обработчик выполняется в отдельном потоке.

Вот, значит, кто-то подписал на ваше событие две сотни обработчиков. Сколько будет потоков? Откуда они возьмутся? А откуда они возьмутся при втором вызове того же события?
Я на старте еще написал, что речь тут не про потоки, а про события в контексте потоков. Я тут не занимался разруливанием потоков. Если вам необходимо рулить пулом, шелдером или еще чем, поищите информацию сами. Я лишь дал идею.

Агу. А что будет, если в обработчике возникнет ошибка (exception кто-то бросит)?
Во-первых, в моем примере генератора событий вообще не волнует как это событие будет обработано. поэтому, если возникнет ошибка, то обрабатывать ее надо в методе-обработчике. В случае моего примера это
        static void Raiser_CounterChanged(object sender, EventRaiserCounterChangedEventArgs e)
        {
            Console.WriteLine(string.Format("OldValue: {0}; NewValue: {1}", e.OldValue, e.NewValue));
        }
Так вот в моем примере каждый обработчик выполняется в отдельном потоке.

Так в каком же?


Я на старте еще написал, что речь тут не про потоки, а про события в контексте потоков. Я тут не занимался разруливанием потоков. Если вам необходимо рулить пулом, шелдером или еще чем, поищите информацию сами. Я лишь дал идею.

Так вот, без знания ответов на эти вопросы ваша идея опасна.


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

Так что же будет, если обработчик не обработает ошибку, и она из него вылетит?

Так в каком же?
В новом
Так вот, без знания ответов на эти вопросы ваша идея опасна.
Идея тут не может быть опасна. Опасна реализация. Но я же предупредил, что все примеры чисто тестовые. Я тут рассматриваю абстрактного коня в вакууме. Вот если бы я написал библиотеку и выложил на гитхаб, то данное замечание было бы уместно.

Так что же будет, если обработчик не обработает ошибку, и она из него вылетит?
Тут все зависит от того, пытается ли обработчик лезть в другие потоки. Если он тихонечко работает в своем конткесте, то с больше долей вероятности он просто упадет и никому об этом не расскажет :)
Если вам все таки хочется об этом узнать, оборачивайте операции обработчика в try catch
В новом

Откуда он взялся? Какой у него оверхед?


Есть такое правило большого пальца, что постоянное создание новых потоков — это большое зло.


Идея тут не может быть опасна.

Может, может (я даже не буду приводить очевидные примеры). В вашем случае идея выглядит как "а давайте запустим все обработчики каждый в своем потоке". Первый же вопрос к это идее — это что делать, если обработчиков очень много.


Тут все зависит от того, пытается ли обработчик лезть в другие потоки.

Почему? Какая разница?


Если он тихонечко работает в своем конткесте, то с больше долей вероятности он просто упадет и никому об этом не расскажет

Так с большой долей вероятности или гарантированно? А не может так случиться, что он упадет и погребет все приложение?

Есть такое правило большого пальца, что постоянное создание новых потоков — это большое зло.
Я с вами согласен, но к теме данной статьи это не относится

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

Почему? Какая разница?
Потому что это не обсервер

Так с большой долей вероятности или гарантированно? А не может так случиться, что он упадет и погребет все приложение?
Не имеет значение… обработка ошибок обработчика событий — дело обработчика событий а не их генератора.

Относится напрямую, потому что продвигаемый в статье подход — давайте создавать новый поток на каждый обработчик при каждом вызове события.

Да не продвигаю я в статье никакой подход. Я только на конкретном примере показываю, как все это работает. Прочитав данную статью, человек, который никогда не задумывался о том, где выполняются обработчики событий, должен это понять. А как использовать это знание — уже его дело.
Прочитав данную статью, человек, который никогда не задумывался о том, где выполняются обработчики событий, должен это понять.

Для этого достаточно двух фраз: "Обработчики событий — это делегаты. Делегаты выполняются там, откуда их вызвали".


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

Не лучшая идея запускать по потоку на каждый обработчики эвентов. ThreadPool.QueueWorkItem предпочтительней. И не забываем про SynchronizationContext, когда работаем с голыми потоками или тред плулом.

BeginInvoke на делегате тоже использует поток пула, нет никакого отличия от ThreadPool.QueueWorkItem


Запоминание SynchronizationContext в событиях — отдельная проблема, решение которой больше этого поста раза в два :)

Я уже ответил на прошлый коммент по этому поводу. Пул потоков не относится к теме данной статьи. Вы можете рулить задачами с шелдером… да чем угодно. Я только предложил идею, как универсализировать систему событий и объяснил где и как работают обработчики
Использовать механизм event в многопоточной среде… Скажем так, один из лучших способов стрельнуть себе даже не в ногу, а в голову. На мой взгляд, event — введенный в язык сахар для обслуживания ui. Более безопасный и управляемый способ — IObservable (+ Reactive Extensions FromEventPattern как переходник). После этого имеем все радости управляемой многонитевой подписки с реактивным потоком событий.
События и обсерверы имеют разные области применения. Использовать паттерн «наблюдатель», на мой взгляд, имеет смысл, когда генератору событий нужно знать, кто ожидает оповещений и/или выбирать, кому отправить данное оповещение, а кому нет. Мне вот сбербанк отправляет информацию о моих операциях, а вам о моих не отправляет… зато отправляет о ваших… Вот хороший пример обсервера.
Использовать паттерн «наблюдатель», на мой взгляд, имеет смысл, когда генератору событий нужно знать, кто ожидает оповещений и/или выбирать, кому отправить данное оповещение, а кому нет.

Вообще-то в Observables источник данных понятия не имеет, кто на него подписан.

Вообще-то в Observables источник данных понятия не имеет, кто на него подписан.
Не в случае с .net

https://msdn.microsoft.com/ru-ru/library/dd990377(v=vs.110).aspx

И что вы хотели этим сказать? Да, разумеется, поставщик имеет ссылки на всех подписчиков. Но он не может отличать их друг от друга. Точно так же как и в делегатах. Отличие-то в чем?

Может. Точнее имеет возможность. Это уже вопрос реализации. У IObserver нет метода Subscribe. А это значит что способ подписки остается на усмотрение программиста.
У IObserver нет метода Subscribe. А это значит что способ подписки остается на усмотрение программиста.

… способ подписки в Rx вполне известен: observable.Subscribe(observer). Никакого "усмотрения программиста".


Но какое это имеет отношение к тому, знает ли источник данных о том, кто на него подписан?

… и чем это отличается от обычных событий? Тем, что делегатов три, а не один?


(Не говоря уже о том, что типовой случай работы с Observable — это функциональное преобразование, где финальные подписчики никому никогда видны не будут.)


Вот, значит, мой код (который источник данных):


    private Subject<string> _messageReceived = new Subject<string>();

    public IObservable<string> MessageReceived => _messageReceived.AsObservable();

    public void ReceiveMessage(string message)
    {
        _messageReceived.OnNext(message);
    }

А вот потребитель:


messager.MessageReceived.Where(s => s.Length > threshold).Throttle(..)...

И много у источника данных возможностей узнать, что с сообщением происходит?

Зачем вы привели пример, в котором не дали эту возможность? :)

Затем, что это один из стандартных способ работы с observables в .net. И, что характерно, все остальные тоже устроены так, что источник данных (observable) отвязан от получателя (observer) и знает о нем только и исключительно интерфейс.


Так что ну да, попробуйте привести другой пример, где observable будет как-то различать своих подписчиков.

Вы путаете шаблоны "наблюдатель" и "публикация/подписка".

Вы путаете шаблоны «наблюдатель» и «публикация/подписка»
Я как раз их не путаю
А зачем по потоку на события? Обычно обработка события в одном потоке — наоборот ожидаемое поведение.
Единственное логичное исключение — UI. Но тогда всегда можно явно запустить событие в диспатчере от UI потока.
Незачем. В универсальном методе, который я предложил, для этого можно не BeginInvoke вызывать, а создать новый поток, в котором вызывать Invoke. Я же написал, статья не про работу с потоками, а про то, в контексте каких потоков выполняются события. К сожалению не все знают ответ на этот вопрос
Я же написал, статья не про работу с потоками, а про то, в контексте каких потоков выполняются события.

Вообще-то, на этот вопрос нет формально верного ответа. События выполняются в том потоке, в котором вызывающая сторона сочла нужным их вызвать. Или вы спрашиваете, в каком потоке выполняется делегат во время вызова? Ну так это, в общем-то, очевидно: при вызове Invoke — в вызывающем потоке. При вызове BeginInvoke… на этот вопрос и вы ответа не дали пока.

Вообще-то, на этот вопрос нет формально верного ответа. События выполняются в том потоке, в котором вызывающая сторона сочла нужным их вызвать.

В статье сказано:
В общем обработчики события в примере выше выполняются в потоке, порожденном в методе DoWork(). То есть при генерации события, поток, который его сгенерировал таким образом, дожидается выполнения всех обработчиков.

То есть я указал на приведенный мною пример. Я не стал обобщать, потому что именно различные реализации этого примера я чаще всего вижу в чужом коде. И вот это обобщение, которое вы привели — верное, но без разбора конкретного примера не всем будет понятно. Джуниоров намного больше, чем мидлов и сеньеров. И кода в общем объеме они выдают больше.
Или вы спрашиваете, в каком потоке выполняется делегат во время вызова? Ну так это, в общем-то, очевидно
К сожалению не для всех это очевидно.
При вызове BeginInvoke… на этот вопрос и вы ответа не дали пока.
Как не дал то? написал же что порождаются новые потоки.
К сожалению не для всех это очевидно.

Знаете, людям, которым неочевидно, в каком потоке вызывается делегат, надо идти обратно учиться. Простите за прямоту.


Как не дал то? написал же что порождаются новые потоки.

Вот только это неправильный ответ.


Я повторю свой пример: вот у нас есть событие, на него подписано двести обработчиков. Мы дважды вызвали это событие. Сколько потоков будет создано?

Знаете, людям, которым неочевидно, в каком потоке вызывается делегат, надо идти обратно учиться. Простите за прямоту.
Согласен. Нас вот в универе не учили работе с потоками. Выходит, приходится заниматься самообразованием. Для этого есть курсы, книги и подобные статьи.

Вот только это неправильный ответ.

Я повторю свой пример: вот у нас есть событие, на него подписано двести обработчиков. Мы дважды вызвали это событие. Сколько потоков будет создано?

Почему не правильный то?

Количество потоков будет равно Количеству подписчиков умноженному на количество вызовов. В случае с вашими цифрами их будет 400.

Но к чему вообще этот вопрос?
Нас вот в универе не учили работе с потоками.

Так это не вопрос работы с потоками, это вопрос работы с делегатами. И это описано в любой книжке про C# и/или CLR.


Почему не правильный то?

Потому что BeginInvoke на делегате выбрасывает делегат в тредпул, а не в новый поток. Со всеми занятными вытекающими.


Количество потоков будет равно Количеству подписчиков умноженному на количество вызовов. В случае с вашими цифрами их будет 400.

… и вы серьезно считаете, что создавать 200 потоков во время вызова события — это нормально? То, что в этот момент вы съели (при настройках по умолчанию) 200мб памяти, вас не смущает?


Но, к счастью, вы не правы. Будет создано не больше потоков, чем задано в тредпуле. А если приложение давно используется, и тредпул уже разогрет — то ни одного.


Но к чему вообще этот вопрос?

К тому, что вы сам, похоже, не понимаете, как работает предлагаемое вами решение.

Так это не вопрос работы с потоками, это вопрос работы с делегатами. И это описано в любой книжке про C# и/или CLR.

Но статья не про потоки или делегаты!
Потому что BeginInvoke на делегате выбрасывает делегат в тредпул, а не в новый поток. Со всеми занятными вытекающими.
Вы лезете в дебри. Возьмите 200 ядер и ваши потоки выполнятся в раз.

… и вы серьезно считаете, что создавать 200 потоков во время вызова события — это нормально? То, что в этот момент вы съели (при настройках по умолчанию) 200мб памяти, вас не смущает?
Нет я так не считаю, я уже написал об этом раз 10.

К тому, что вы сам, похоже, не понимаете, как работает предлагаемое вами решение.
В работе с потоками я прикрылся за абстракциями. Мне не важно, как отработает пул. Мне важны 2 вещи:
1. Обработчики будут выполняться параллельно (да, на самом деле все не совсем так и одновременных потоков будет по количеству ядер, свободной памяти и тд, но я не раскрываю данную абстракцию в этой статье. Именно поэтому я и говорю, что для вашего примера потоков будет 400, ведь я потребовал от системы генерацию 400 потоков)
2. Породитель событий не будет дожидаться их выполнение и продолжит свою работу.

Зачем вы пытаетесь мне сказать, что я неправильно работаю с потоками, если я в начале статьи сказал, что я неправильно с ними работаю и статья не о них?
Но статья не про потоки или делегаты!

А про что же она, если события — это делегаты, и вы рассказываете, как устроены/работают делегаты, и спрашиваете, на каком потоке делегат выполнится?


Вы лезете в дебри.

Это не дебри, это самые что ни на есть азы.


Возьмите 200 ядер и ваши потоки выполнятся в раз.

Во-первых, не обязательно. Во-вторых, все равно не понятно, сколько их будет создано.


Нет я так не считаю, я уже написал об этом раз 10.

Тогда зачем вы предлагаете такое решение?


В работе с потоками я прикрылся за абстракциями.

Нет, вы написали "BeginInvoke порождает новый поток". Это не абстракция, это конкретное — и ложное — утверждение.


Мне не важно, как отработает пул.

… вот только в статье нет ни слова о том, что делегат будет выполнен на пуле. Более того, явно написано, что будет порожден новый поток.


Зачем вы пытаетесь мне сказать, что я неправильно работаю с потоками, если я в начале статьи сказал, что я неправильно с ними работаю и статья не о них?

Затем, чтобы (а) вы не понимаете, что неправильно работете с потоками и (б) я не хочу видеть пример неправильной работы с потоками.


Именно поэтому я и говорю, что для вашего примера потоков будет 400, ведь я потребовал от системы генерацию 400 потоков

Это как раз пример того, что вы не понимаете происходящего. Сделав 400 BeginInvoke вы не потребовали от системы генерацию 400 потоков.

Я бы рекомендовал ознакомиться вот с этим материалом.
MCTS Self-Paced Training Kit (Exam 70-536)
Если мне не изменяет память там было достаточное описание, для основ. (Chapter 7
Threading, Lesson 3: The Asynchronous Programming Model )
После без проблем «поверх ляжет» await & async
Да вы сами похоже не понимаете что происходит.
Это не дебри, это самые что ни на есть азы.

Так разберитесь с ними сами прежде всего.

Нет, вы написали «BeginInvoke порождает новый поток». Это не абстракция, это конкретное — и ложное — утверждение.

… вот только в статье нет ни слова о том, что делегат будет выполнен на пуле. Более того, явно написано, что будет порожден новый поток.

Затем, чтобы (а) вы не понимаете, что неправильно работете с потоками и (б) я не хочу видеть пример неправильной работы с потоками.

Это как раз пример того, что вы не понимаете происходящего. Сделав 400 BeginInvoke вы не потребовали от системы генерацию 400 потоков.

Класс Thread в принципе не является потоком. Это обертка. Абстракция.
Говоря поток, я имею ввиду Thread. Говоря «порождает» поток, я имею ввиду создает Thread и дает ему команду «Пуск». Так вот, одновременно (почти) создаются 400 экземпляров Thread. Когда они будут выполняться, генератору событий не важно. Ему даже не важно, будут ли они вообще выполняться, или лягут с ошибкой. Обработка событий не входит в его компетенцию. Вы со своими заумными фразами про потоки забыли про принципы ООП!
Теперь что касается этих 400 потоков. Да, я просто кинул их бездумно в пул. Ну так мне не важно, как, когда и в каком порядке они будут выполняться и программа больше ничего не делает. Я не могу в этом случае рассмотреть каждый случай отдельно, поэтому ПРАВИЛЬНОГО решения тут нет и быть не может! Для случая, когда потребуется соблюдать порядок, я создам очередь из задач (или шелдер). Для случая, когда выполнение других потоков будет важнее, я укажу приоритет для этих событий ниже. Зато, если я не вынесу порождение событий в отдельный от воркера поток, мне придется снижать приоритет всего воркера по отношению к другим потокам. Но это уже тема не для джуниоров и я не стал включать это все в статью!
А вы тут обсуждаете абстрактного коня в вакууме, и при этом ругаетесь на моего коня. Мой конь круче!
Говоря «порождает» поток, я имею ввиду создает Thread и дает ему команду «Пуск». Так вот, одновременно (почти) создаются 400 экземпляров Thread.

Круто, а какие-то основания для этих утверждений у вас есть? Потому что "создать экземпляр класса Thread и сказать ему Start" — это и есть создание нового потока.


Когда они будут выполняться, генератору событий не важно. Ему даже не важно, будут ли они вообще выполняться, или лягут с ошибкой. Обработка событий не входит в его компетенцию.

Это если у вас fire-and-forget, что далеко не для всех событий верно. А вы это почему-то считаете поведением по умолчанию.


Вы со своими заумными фразами про потоки забыли про принципы ООП!

… принципы ООП тут ни при чем.


Теперь что касается этих 400 потоков. Да, я просто кинул их бездумно в пул.

Да нет же. Нельзя кинуть потоки в пул.


А вы тут обсуждаете абстрактного коня в вакууме, и при этом ругаетесь на моего коня.

Отнюдь. Я обсуждаю ваш конкретный код.

Вот я и говорю что вы сами не понимаете что говорите.

Круто, а какие-то основания для этих утверждений у вас есть? Потому что «создать экземпляр класса Thread и сказать ему Start» — это и есть создание нового потока.
Поток при этом не создается. Потоками пул управляет, а не пользователь. После команды старт происходит запрос на запуск потока (или как то так). Я могу стартануть 400 экземпляров Thread, но реально работать будут при этом только 4 из них (на моем железе и среде).
Это если у вас fire-and-forget, что далеко не для всех событий верно. А вы это почему-то считаете поведением по умолчанию.

Ничего не слышал про fire-and-forget для системы событий в .net. Гугл не помог. Подскажите где почитать, потому что в данный момент мне это кажется плохой архитектурой а не особым подходом.
Да нет же. Нельзя кинуть потоки в пул.
Беда с терминологие, походу… я создал 400 экземпляров Thread и сказал им старт.

Отнюдь. Я обсуждаю ваш конкретный код.
Пример с потоками абстрактный… мне, может, нужно было его на псевдо языке написать, чтобы это было понятно?
Поток при этом не создается.

Серьезно? Ссылку на доку вам ниже дали, вот вам еще из Рихтера цитаты.


Во-первых: "So today, a CLR thread is identical to a Windows thread".
Во-вторых: "To actually create the operating system thread and have it start executing the callback method, you must call Thread’s Start method, passing into it the object (state) that you want passed as the callback method’s argument."


Окей, в документацию вы не верите, давайте эксперимент устроим.


Код
        {
            var info = new ConcurrentBag<ThreadInfo>();

            const int howMany = 400;

            Console.WriteLine("For Thread pool:");
            var handles = Enumerable
                .Range(1, howMany)
                .Select(_ =>
                {
                    var manualResetEventSlim = new ManualResetEventSlim();
                    ThreadPool.QueueUserWorkItem(h =>
                    {
                        info.Add(new ThreadInfo
                        {
                            IsThreadPool = Thread.CurrentThread.IsThreadPoolThread,
                            ThreadId = Thread.CurrentThread.ManagedThreadId
                        });
                        Thread.Sleep(500);
                        ((ManualResetEventSlim)h).Set();
                    }, manualResetEventSlim);
                    return manualResetEventSlim.WaitHandle;
                }).ToArray();

            var threads = Process.GetCurrentProcess().Threads.Count;
            Console.WriteLine($"Threads in process: {threads}");

            //не влезаем в максимально допустимое число в WaitAll
            foreach (var handle in handles)
                handle.WaitOne();

            Console.WriteLine($"Invocations: {info.Count}");
            Console.WriteLine($"Different threads ids: {info.Select(i => i.ThreadId).Distinct().Count()}");
            Console.WriteLine($"Count of thread pool threads: {info.Count(i => i.IsThreadPool)}");

            var threadPoolIds = new HashSet<int>(info.Select(i => i.ThreadId));

            Console.WriteLine();
            Console.WriteLine("For Thread:");
            info = new ConcurrentBag<ThreadInfo>();
            handles = Enumerable
                .Range(1, howMany)
                .Select(_ =>
                {
                    var manualResetEventSlim = new ManualResetEventSlim();
                    var thread = new Thread(h =>
                    {
                        info.Add(new ThreadInfo
                        {
                            IsThreadPool = Thread.CurrentThread.IsThreadPoolThread,
                            ThreadId = Thread.CurrentThread.ManagedThreadId
                        });
                        Thread.Sleep(500);
                        ((ManualResetEventSlim)h).Set();
                    });
                    thread.Start(manualResetEventSlim);
                    return manualResetEventSlim.WaitHandle;
                }).ToArray();

            threads = Process.GetCurrentProcess().Threads.Count;
            Console.WriteLine($"Threads in process: {threads}");

            //не влезаем в максимально допустимое число в WaitAll
            foreach (var handle in handles)
                handle.WaitOne();

            Console.WriteLine($"Invocations: {info.Count}");
            Console.WriteLine($"Different threads ids: {info.Select(i => i.ThreadId).Distinct().Count()}");
            Console.WriteLine($"Matching to thread pool thread ids: {info.Select(i => i.ThreadId).Where(threadPoolIds.Contains).Distinct().Count()}");
            Console.WriteLine($"Count of thread pool threads: {info.Count(i => i.IsThreadPool)}");

            Console.WriteLine("");
            Console.WriteLine("For BeginInvoke:");

            info = new ConcurrentBag<ThreadInfo>();
            handles = Enumerable
                .Range(1, howMany)
                .Select(_ =>
                {
                    Action d = () =>
                    {
                        info.Add(new ThreadInfo
                        {
                            IsThreadPool = Thread.CurrentThread.IsThreadPoolThread,
                            ThreadId = Thread.CurrentThread.ManagedThreadId
                        });
                        Thread.Sleep(500);
                    };
                    return d.BeginInvoke(d.EndInvoke, null).AsyncWaitHandle;
                }).ToArray();

            threads = Process.GetCurrentProcess().Threads.Count;
            Console.WriteLine($"Threads in process: {threads}");

            //не влезаем в максимально допустимое число в WaitAll
            foreach (var handle in handles)
                handle.WaitOne();

            Console.WriteLine($"Invocations: {info.Count}");
            Console.WriteLine($"Different threads ids: {info.Select(i => i.ThreadId).Distinct().Count()}");
            Console.WriteLine($"Matching to thread pool thread ids: {info.Select(i => i.ThreadId).Where(threadPoolIds.Contains).Distinct().Count()}");
            Console.WriteLine($"Count of thread pool threads: {info.Count(i => i.IsThreadPool)}");
        }

А вот результат:


For Thread pool:
Threads in process: 15
Invocations: 400
Different threads ids: 9
Count of thread pool threads: 400

For Thread:
Threads in process: 416
Invocations: 400
Different threads ids: 400
Matching to thread pool thread ids: 0
Count of thread pool threads: 0

For BeginInvoke:
Threads in process: 16
Invocations: 400
Different threads ids: 16
Matching to thread pool thread ids: 9
Count of thread pool threads: 400

Во-первых, хорошо видно, что "создать Thread и сказать ему Start" — это именно "создать новый поток": число тредов увеличилось ровно на запрошенное количество, число разных идентификаторов такое же.


Во-вторых, видно, что пул ведет себя иначе: число потоков выросло на 15, но для работы ему вообще хватило девяти.


В-третьих, видно, что BeginInvoke ведет себя, как и ожидалось, идентично пулу (которым он, согласно источникам, и пользуется).


Потоками пул управляет, а не пользователь.

Это если вы используете ThreadPool. А если вы явно создаете Thread, то вы как раз явно управляете потоком. Он для этого и придуман.


После команды старт происходит запрос на запуск потока (или как то так)

Нет, после команды Start создается новый поток, и ему отдается команда на запуск.


Я могу стартануть 400 экземпляров Thread, но реально работать будут при этом только 4 из них

А вот это уже вопрос того, сколько будет работать параллельно, а не сколько будет создано.


Ничего не слышал про fire-and-forget для системы событий в .net.

Fire-and-forget — это такой подход к организации событий, при котором отправителя не интересует ни было ли событие доставлено, ни было ли оно успешно обработано. Это ровно то, что делаете (делали) вы, не обрабатывая результат делегата.


Беда с терминологие, походу… я создал 400 экземпляров Thread и сказал им старт.

Нет. Вы 400 раз вызвали BeginInvoke — который не создает Thread.


Но если бы вы 400 раз сделали new Thread(d).Start() — вы бы получили именно описанное мной поведение.

Почему не правильный то?

Полагаю, что нюанс в этом: Dispatcher Class

Dispatcher.BeginInvoke Method (Delegate, DispatcherPriority, Object[])
.NET Framework (current version)

Executes the specified delegate asynchronously with the specified arguments, at the specified priority, on the thread that the Dispatcher was created on.
Namespace: System.Windows.Threading
Assembly: WindowsBase (in WindowsBase.dll)

Нас вот в универе не учили работе с потоками.

Это все есть в документации. Согласен что муторно, то иногда надо :)
Если нас не учили, это не значит, что я не учил. Я говорю про других людей. Опять же я не все знаю и понимаю, но дальше своих знаний я в этой статье не лез. Почти все что тут назвали ошибками является обычной абстракцией (остальное я уже поправил), но я не могу этого объяснить. Слово абстрактный им не знакомо или хотят блеснуть знаниями я не знаю…
Вроде как за последние лет 20 выработаны достаточно походов к решение многопоточного взаимодействия?
Что-то из уже сделанного не подходит?
Обычно на этапе дизайна уже ищутся ответы на вопросы:
  1. Как должен себя вести родительский поток, если упадет созданный поток ?
  2. Стратегия обработки ошибок ?
  3. Стратегия работы с общими ресурсами ?

Исходя из этого уже и выбирают все events / messages / и т.д.

Полагаю, что проблема тут:
То есть генератору событий теперь до лампочки, кто, как и как долго будет обрабатывать его события.


Нафига приложение без «TimeOut»? Выжрать все ресурсы в одно рыло один генератор? Так вроде бы вычислительные ресуры то общие (см п3 выше)

А какую проблему то это все должно решить?
Да, да и еще раз да! Я устал отвечать на одни и те же замечания. Статья не об организации потоков, она просто рассказывает о том, в каких потоках выполняются события. Не все об этом знают. Это для джуниоров. Про организацию потоков я ничего не писал. Для этого одной статьи просто может не хватить из-за количества вариаций.
в каких потоках выполняются события

Поймите простую штуку: дьявол в деталях.
События не могут «выполняться», события можно вызвать.
А выполняется «Обработчик события».
Это два разных элемента. Когда вы сможете их рассмотреть отдельно, все станет на свои места. Пока Вы будете их рассматривать «связно» вы не получите правильного ответа.
Сам вопрос поставлен не очень корректно: как правильно молотком закручивать шуруп? :)
Правильный ответ: возьмите отвертку.
Вот, проблемы с терминологией. Заработался, простите. Я то же самое объяснял оратору выше.
((EventHandler<T>)delegates[i]).BeginInvoke(sender, e, AsyncCallbackClass<T>.CallBack, null);

//...

static class AsyncCallbackClass<T2> where T2 : EventArgs
        {
            public static void CallBack(IAsyncResult ar)
            {
                var tar = ar as AsyncResult;
                if (tar != null)
                    ((EventHandler<T2>)tar.AsyncDelegate).EndInvoke(ar);
            }
        }

Зачем так сложно, чем вам не угодил прямой вызов EndInvoke?


foreach (EventHandler<EventArgs> handler in h.GetInvocationList())
{
    handler.BeginInvoke(sender, e, handler.EndInvoke, null);
}

Более того, его код еще и неверный. Никто не обязывает CLR возвращать какой-то особый публичный AsyncResult...

… а если вернут не его, то мы молча пойдем дальше, так и не вызвав EndInvoke. Красота же.

Спасибо за заметку, сейчас исправлю
public static void RaiseEventAsync<T>(EventHandler<T> h, object sender, T e) where T : EventArgs
{
  if (h != null)
  {
    var delegates = h.GetInvocationList();
    for (var i = 0; i < delegates.Length; i++)
      ((EventHandler<T>)delegates[i]).BeginInvoke(sender, e, h.EndInvoke, null);
  }
}

Серьезно?

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

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

Более того, ваша реализация упирается в ограничения Thread Pool, и обработчики событий скорее всего выстроятся в очередь, в зависимости от загруженности пула; что еще хуже, пул может начать создавать новые потоки, которые неизбежно снизят производительность.
Если вам не хочется, чтобы обработчик повесил работу класса, то напишите такой обработчик — это нетрудно.
Генератор события написал я, а обработчик будет делать Вася Пупкин, который ничего не знает про логику работы моего генератора. поэтому, если обработчик будет работать в одном потоке с логикой генератора, это как раз и поставит разработчика в рамки.

Да кто такой этот Вася Пупкин, что вы его так боитесь — и одновременно о нем так заботитесь? И что он делает в вашей программе?

. поэтому, если обработчик будет работать в одном потоке с логикой генератора, это как раз и поставит разработчика в рамки.

Это как раз вас поставит в рамки :) Ваш генератор станет «колом», поскольу ничего не сгенерит, пока обработчик не вернет управление.
Если вы разрабатываете библиотеку «для того парня», вы обязаны продумать API взаимодействия. В частности Вашей задачей будет предоставить и делегаты под обработку и декларацию событий и модель асинхронной работы (Event Based, Task Based и т.д).
В общем случае не ваша забота, как будут обрабатываться ваши события и по какой модели. Даже если вы сделаете «просто один поток» то обработчики сами себе придумают как быстро вернуть управление генератору.
Вопрос только в том — генератору нужен результат «обработчика или нет».
UFO just landed and posted this here
Sign up to leave a comment.

Articles