在单独的线程上引发事件

时间:2013-09-18 20:30:50

标签: c# .net multithreading events asynchronous

我正在开发一个需要处理实时反馈并以非常快的方式向听众广播数据的组件(大约100纳秒级精度,甚至比我能做到的还要少)目前我正在筹集一个来自我的代码的事件订阅者可以订阅。但是因为在C#中,事件处理程序在引发事件的同一个线程上运行,所以引发事件的线程将被阻塞,直到所有订阅者完成事件的处理。我无法控制订阅者的代码,因此他们可能会在事件处理程序中执行任何耗时的操作,这可能会阻塞正在广播的线程。

我能做些什么才能将数据广播给其他订阅者,但仍能快速播放?

5 个答案:

答案 0 :(得分:10)

100 ns是一个非常艰难的目标。我相信它会深刻理解你正在做什么以及为什么要达到这种性能。

但是,异步调用事件订阅者很容易解决。 它已经由Jon Skeet以及其他人回答了here

foreach (MyDelegate action in multicast.GetInvocationList())
{
    action.BeginInvoke(...);
}

修改 我还要提一下,您需要在real-time operating system上运行才能为用户提供严格的性能保证。

答案 1 :(得分:5)

好像你正在寻找任务。以下是我为我的工作编写的扩展方法,它可以异步调用一个事件,以便每个事件处理程序都在自己的线程上。我不能对它的速度发表评论,因为这对我来说从来都不是必需的。


更新

根据评论我调整它,以便只创建一个任务来调用所有订阅者

/// <summary>
/// Extension method to safely encapsulate asynchronous event calls with checks
/// </summary>
/// <param name="evnt">The event to call</param>
/// <param name="sender">The sender of the event</param>
/// <param name="args">The arguments for the event</param>
/// <param name="object">The state information that is passed to the callback method</param>
/// <remarks>
/// This method safely calls the each event handler attached to the event. This method uses <see cref="System.Threading.Tasks"/> to
/// asynchronously call invoke without any exception handling. As such, if any of the event handlers throw exceptions the application will
/// most likely crash when the task is collected. This is an explicit decision since it is really in the hands of the event handler
/// creators to make sure they handle issues that occur do to their code. There isn't really a way for the event raiser to know
/// what is going on.
/// </remarks>
[System.Diagnostics.DebuggerStepThrough]
public static void AsyncSafeInvoke( this EventHandler evnt, object sender, EventArgs args )
{
    // Used to make a temporary copy of the event to avoid possibility of
    // a race condition if the last subscriber unsubscribes
    // immediately after the null check and before the event is raised.
    EventHandler handler = evnt;
    if (handler != null)
    {
        // Manually calling all event handlers so that we could capture and aggregate all the
        // exceptions that are thrown by any of the event handlers attached to this event.  
        var invocationList = handler.GetInvocationList();

        Task.Factory.StartNew(() =>
        {
            foreach (EventHandler h in invocationList)
            {
                // Explicitly not catching any exceptions. While there are several possibilities for handling these 
                // exceptions, such as a callback, the correct place to handle the exception is in the event handler.
                h.Invoke(sender, args);
            }
        });
    }
}

答案 2 :(得分:4)

您可以在事件处理程序上使用这些简单的扩展方法:

public static void Raise<T>(this EventHandler<T> handler, object sender, T e) where T : EventArgs {
    if (handler != null) handler(sender, e);
}

public static void Raise(this EventHandler handler, object sender, EventArgs e) {
    if (handler != null) handler(sender, e);
}

public static void RaiseOnDifferentThread<T>(this EventHandler<T> handler, object sender, T e) where T : EventArgs {
    if (handler != null) Task.Factory.StartNewOnDifferentThread(() => handler.Raise(sender, e));
}

public static void RaiseOnDifferentThread(this EventHandler handler, object sender, EventArgs e) {
    if (handler != null) Task.Factory.StartNewOnDifferentThread(() => handler.Raise(sender, e));
}

public static Task StartNewOnDifferentThread(this TaskFactory taskFactory, Action action) {
    return taskFactory.StartNew(action: action, cancellationToken: new CancellationToken());
}

用法:

public static Test() {
     myEventHandler.RaiseOnDifferentThread(null, EventArgs.Empty);
}

cancellationToken是保证StartNew()实际使用其他主题所必需的,如here所述。

答案 3 :(得分:2)

我不能说这是否能够可靠地满足100ns的要求,但是这里有一个替代方案,你可以为最终用户提供一种方法来为你提供一个你可以填充的ConcurrentQueue,他们可以在一个单独的线程上收听。

class Program
{
    static void Main(string[] args)
    {
        var multicaster = new QueueMulticaster<int>();

        var listener1 = new Listener(); //Make a couple of listening Q objects. 
        listener1.Listen();
        multicaster.Subscribe(listener1);

        var listener2 = new Listener();
        listener2.Listen();
        multicaster.Subscribe(listener2);

        multicaster.Broadcast(6); //Send a 6 to both concurrent Queues. 
        Console.ReadLine();
    }
}

//The listeners would run on their own thread and poll the Q like crazy. 
class Listener : IListenToStuff<int>
{
    public ConcurrentQueue<int> StuffQueue { get; set; }

    public void Listen()
    {
        StuffQueue = new ConcurrentQueue<int>();
        var t = new Thread(ListenAggressively);
        t.Start();

    }

    void ListenAggressively()
    {
        while (true)
        {
            int val;
            if(StuffQueue.TryDequeue(out val))
                Console.WriteLine(val);
        }
    }
}

//Simple class that allows you to subscribe a Queue to a broadcast event. 
public class QueueMulticaster<T>
{
    readonly List<IListenToStuff<T>> _subscribers = new List<IListenToStuff<T>>();
    public void Subscribe(IListenToStuff<T> subscriber)
    {
        _subscribers.Add(subscriber);
    }
    public void Broadcast(T value)
    {
        foreach (var listenToStuff in _subscribers)
        {
            listenToStuff.StuffQueue.Enqueue(value);
        }
    }
}

public interface IListenToStuff<T>
{
    ConcurrentQueue<T> StuffQueue { get; set; }
}

由于您无法阻止对其他侦听器的处理,因此这意味着多个线程。在侦听器上拥有专用的侦听线程似乎是一种合理的尝试方法,并发队列似乎是一个不错的传递机制。在这个实现中,它只是不断轮询,但您可能使用线程信令来减少使用AutoResetEvent之类的CPU负载。

答案 4 :(得分:0)

信号和共享内存非常快。您可以发送一个单独的信号,告诉应用程序从共享内存位置读取消息。当然,如果您希望降低延迟,信号仍然是您的应用程序必须在高优先级线程上消耗的事件。我将在数据中添加一个时间标签,以便接收器可以补偿不可避免的延迟。