C#语言中的异步事件

目录

介绍

事件机制在单个线程上提供同步调用

使用TPL的异步事件

使用TPL的异步事件——扩展方法

使用TAP的异步事件

使用TAP – Ver2的异步事件

结论

参考


介绍

C#这样的现代语言集成了事件机制,它实际上将观察者模式集成到语言机制中。

实际上,事件机制提供同步调用这一事实经常被忽视,并且没有得到足够的重视。程序员经常有并行的错觉,这不是现实,是当今多核处理器世界中的一个重要问题。接下来,我们提供多线程问题的分析和解决方案。

提供的代码是一个教程,概念演示级别,为简洁起见,不处理或显示所有变体/问题。

事件机制在单个线程上提供同步调用

需要强调的是,在调用中:

if (SubjectEvent != null)
{
    SubjectEvent(this, args);
}

//or

SubjectEvent?.Invoke(this, args);

正在单个线程上同步调用订阅的EventHandler。这有一些不太明显的后果:

  • 按订阅事件的顺序依次执行EventHandler
  • 这意味着早期订阅EventHandler的对象/值比其他订阅EventHandler的对象/值更新得更早,这可能会对程序逻辑产生影响。
  • 调用某些EventHandler将阻塞线程,直到完成其EventHandler中的所有工作。
  • 如果在某个EventHandler中抛出异常,则在该异常之后订阅的所有EventHandler将不会执行。

我们将在示例中演示它。计划是创建三个EventHandler,每个EventHandler需要10秒才能完成,并监视每个运行线程的线程以及所花费的总时间。我们将输出与此示例相关的每个线程ThreadId,以查看正在使用的线程数。

public class EventArgsW : EventArgs
{
    public string StateW = null;
}

public class EventWrapper
{
    public event EventHandler<EventArgsW> EventW;

    public string StateW;

    public void Notify()
    {
        Console.WriteLine("Notify is running on ThreadId:{0}",
            Thread.CurrentThread.ManagedThreadId);

        EventArgsW args = new EventArgsW();
        args.StateW = this.StateW;

        EventW?.Invoke(this, args);
    }
}

public class HandlerWrapper
{
    private string name;
    private string StateW;
    private ManualResetEvent mrs;

    public HandlerWrapper(string name, ManualResetEvent mrs)
    {
        this.name = name;
        this.mrs = mrs;
    }

    public void Handler(object subject, EventArgsW args)
    {
        Console.WriteLine("Handler{0} is running on ThreadId:{1}",
            name, Thread.CurrentThread.ManagedThreadId);

        Worker(subject, args);
    }

    private void Worker(object subject, EventArgsW args)
    {
        Console.WriteLine("Handler{0}.Worker is running on ThreadId:{1}, i:0",
            name, Thread.CurrentThread.ManagedThreadId);
        StateW = args.StateW;

        for (int i = 1; i <= 2; ++i)
        {
            Thread.Sleep(5000);
            Console.WriteLine("Handler{0}.Worker is running on ThreadId:{1}, i:{2}",
                name, Thread.CurrentThread.ManagedThreadId, i);
        }
        mrs.Set();
    }
}

internal class Client
{
    public static void Main(string[] args)
    {
        Console.WriteLine("Client is running on ThreadId:{0}",
            Thread.CurrentThread.ManagedThreadId);

        ManualResetEvent[] mres = new ManualResetEvent[3];
        for (int i = 0; i < mres.Length; i++) mres[i] = new ManualResetEvent(false);

        EventWrapper s = new EventWrapper();
        s.EventW += (new HandlerWrapper("1", mres[0])).Handler;
        s.EventW += (new HandlerWrapper("2", mres[1])).Handler;
        s.EventW += (new HandlerWrapper("3", mres[2])).Handler;

        // Change subject state and notify observers
        s.StateW = "ABC123";

        var timer = new Stopwatch();
        timer.Start();

        s.Notify();

        ManualResetEvent.WaitAll(mres);
        timer.Stop();
        TimeSpan timeTaken = timer.Elapsed;
        string tmp1 = "Client time taken: " + timeTaken.ToString(@"m\:ss\.fff");
        Console.WriteLine(tmp1);

        Console.ReadLine();
    }
}

执行结果为:

从执行结果可以看出,EventHandler一个接一个地运行,全部在thread Id=1上,与客户端运行的线程相同。完成所有工作需要30.059秒。

使用TPL的异步事件

使用任务并行库(TPL),我们可以使我们的EventHandler在单独的线程上异步运行。更重要的是,如果我们想将Client线程从任何工作中释放出来(假设我们的ClientUI线程),我们可以在与Client线程不同的线程上引发Event(调度EventHandler调用)。以下是新的实现:

新的解决方案代码如下所示:

public class EventArgsW : EventArgs
{
    public string StateW = null;
}

public class EventWrapper
{
    public event EventHandler<EventArgsW> EventW;

    public string StateW;

    public void Notify()
    {
        Task.Factory.StartNew(
            () => {
                Console.WriteLine("Notify is running on ThreadId:{0}",
                Thread.CurrentThread.ManagedThreadId);

                EventArgsW args = new EventArgsW();
                args.StateW = this.StateW;

                EventW?.Invoke(this, args);
            });
    }
}

public class HandlerWrapper
{
    private string name;
    private string StateW;
    private ManualResetEvent mrs;

    public HandlerWrapper(string name, ManualResetEvent mrs)
    {
        this.name = name;
        this.mrs = mrs;
    }

    public void Handler(object subject, EventArgsW args)
    {
        Console.WriteLine("Handler{0} is running on ThreadId:{1}",
            name, Thread.CurrentThread.ManagedThreadId);

        Task.Factory.StartNew(
            () => Worker(subject, args)); ;
    }

    private void Worker(object subject, EventArgsW args)
    {
        Console.WriteLine("Handler{0}.Worker is running on ThreadId:{1}, i:0",
            name, Thread.CurrentThread.ManagedThreadId);
        StateW = args.StateW;

        for (int i = 1; i <= 2; ++i)
        {
            Thread.Sleep(5000);
            Console.WriteLine("Handler{0}.Worker is running on ThreadId:{1}, i:{2}",
                name, Thread.CurrentThread.ManagedThreadId, i);
        }
        mrs.Set();
    }
}

internal class Client
{
    public static void Main(string[] args)
    {
        Console.WriteLine("Client is running on ThreadId:{0}",
            Thread.CurrentThread.ManagedThreadId);

        ManualResetEvent[] mres = new ManualResetEvent[3];
        for (int i = 0; i < mres.Length; i++) mres[i] = new ManualResetEvent(false);

        EventWrapper s = new EventWrapper();
        s.EventW += (new HandlerWrapper("1", mres[0])).Handler;
        s.EventW += (new HandlerWrapper("2", mres[1])).Handler;
        s.EventW += (new HandlerWrapper("3", mres[2])).Handler;

        // Change subject state and notify observers
        s.StateW = "ABC123";

        var timer = new Stopwatch();
        timer.Start();

        s.Notify();

        ManualResetEvent.WaitAll(mres);
        timer.Stop();
        TimeSpan timeTaken = timer.Elapsed;
        string tmp1 = "Client time taken: " + timeTaken.ToString(@"m\:ss\.fff");
        Console.WriteLine(tmp1);

        Console.ReadLine();
    }
}

执行结果在这里:

从执行结果可以看出,我们看到EventHandler在单独的线程上运行,从执行日志中可以看到并发性,总耗时为10.020秒。

使用TPL的异步事件——扩展方法

由于TPL的使用需要更改现有代码并混淆代码的可读性,因此我创建了一个Extension方法来简化TPL的使用。而不是编写:

EventW?.Invoke(this, args);

有人会这样编写:

EventW?.InvokeAsync<EventArgsW>(this, args);

所有的TPL魔法都会在幕后发生。以下是新解决方案的所有源代码:

public class EventArgsW : EventArgs
{
    public string StateW = null;
}

public class EventWrapper
{
    public event EventHandler<EventArgsW> EventW;

    public string StateW;

    public void Notify()
    {
        Console.WriteLine("Notify is running on ThreadId:{0}",
            Thread.CurrentThread.ManagedThreadId);

        EventArgsW args = new EventArgsW();
        args.StateW = this.StateW;

        EventW?.InvokeAsync<EventArgsW>(this, args);  //(1)
    }
}

public class HandlerWrapper
{
    private string name;
    private string StateW;
    private ManualResetEvent mrs;

    public HandlerWrapper(string name, ManualResetEvent mrs)
    {
        this.name = name;
        this.mrs = mrs;
    }

    public void Handler(object subject, EventArgsW args)
    {
        Console.WriteLine("Handler{0} is running on ThreadId:{1}",
            name, Thread.CurrentThread.ManagedThreadId);

        Worker(subject, args);
    }

    private void Worker(object subject, EventArgsW args)
    {
        Console.WriteLine("Handler{0}.Worker is running on ThreadId:{1}, i:0",
            name, Thread.CurrentThread.ManagedThreadId);
        StateW = args.StateW;

        for (int i = 1; i <= 2; ++i)
        {
            Thread.Sleep(5000);
            Console.WriteLine("Handler{0}.Worker is running on ThreadId:{1}, i:{2}",
                name, Thread.CurrentThread.ManagedThreadId, i);
        }
        mrs.Set();
    }
}

public static class AsyncEventsUsingTplExtension
{
    public static void InvokeAsync<TEventArgs>   //(2)
        (this EventHandler<TEventArgs> handler, object sender, TEventArgs args)
    {
        Task.Factory.StartNew(() =>
        {
            Console.WriteLine("InvokeAsync<TEventArgs> is running on ThreadId:{0}",
                Thread.CurrentThread.ManagedThreadId);

            var delegates = handler?.GetInvocationList();

            foreach (var delegated in delegates)
            {
                var myEventHandler = delegated as EventHandler<TEventArgs>;
                if (myEventHandler != null)
                {
                    Task.Factory.StartNew(() => myEventHandler(sender, args));
                }
            };
        });
    }
}

internal class Client
{
    public static void Main(string[] args)
    {
        Console.WriteLine("Client is running on ThreadId:{0}",
            Thread.CurrentThread.ManagedThreadId);

        ManualResetEvent[] mres = new ManualResetEvent[3];
        for (int i = 0; i < mres.Length; i++) mres[i] = new ManualResetEvent(false);

        EventWrapper s = new EventWrapper();
        s.EventW += (new HandlerWrapper("1", mres[0])).Handler;
        s.EventW += (new HandlerWrapper("2", mres[1])).Handler;
        s.EventW += (new HandlerWrapper("3", mres[2])).Handler;

        // Change subject state and notify observers
        s.StateW = "ABC123";

        var timer = new Stopwatch();
        timer.Start();

        s.Notify();

        ManualResetEvent.WaitAll(mres);
        timer.Stop();
        TimeSpan timeTaken = timer.Elapsed;
        string tmp1 = "Client time taken: " + timeTaken.ToString(@"m\:ss\.fff");
        Console.WriteLine(tmp1);

        Console.ReadLine();
    }
}

这是执行结果:

从执行结果可以看出,我们看到EventHandle在单独的线程上运行,从执行日志中可以看到并发性,总耗时为10.039秒。TPL正在将工作分派给线程池中的线程,可以看到线程Id=4已被使用了两次,可能它提前完成了工作并再次可用于工作。

使用TAP的异步事件

根据它们在C#中的定义方式的性质,EventHandler是任务异步模式(TAP)上下文中的同步函数。如果您希望EventHandlerTAP的上下文中异步,以便您可以在其中等待,您需要实际推出自己的事件通知机制,以支持您的自定义异步EventHandler版本。在[1]中可以看到这种工作的一个很好的例子。出于示例的目的,我修改了该代码,以下是解决方案的新版本:

public class EventArgsW : EventArgs
{
    public string StateW = null;
}

public class EventWrapper
{
    public event AsyncEventHandler<EventArgsW> EventW;

    public string StateW;

    public async Task Notify(CancellationToken token)
    {
        Console.WriteLine("Notify is running on ThreadId:{0}",
            Thread.CurrentThread.ManagedThreadId);

        EventArgsW args = new EventArgsW();
        args.StateW = this.StateW;

        await this.EventW.InvokeAsync(this, args, token);
    }
}

public class HandlerWrapper
{
    private string name;
    private string StateW;
    private ManualResetEvent mrs;

    public HandlerWrapper(string name, ManualResetEvent mrs)
    {
        this.name = name;
        this.mrs = mrs;
    }

    public async Task Handler(object subject, EventArgsW args,
        CancellationToken token)
    {
        Console.WriteLine("Handler{0} is running on ThreadId:{1}",
            name, Thread.CurrentThread.ManagedThreadId);

        await Worker(subject, args);
    }

    private async Task Worker(object subject, EventArgsW args)
    {
        Console.WriteLine("Handler{0}.Worker is running on ThreadId:" +
            "{1}, i:0",
            name, Thread.CurrentThread.ManagedThreadId);
        StateW = args.StateW;

        for (int i = 1; i <= 2; ++i)
        {
            Thread.Sleep(5000);
            Console.WriteLine("Handler{0}.Worker is running on ThreadId:" +
                "{1}, i:{2}",
                name, Thread.CurrentThread.ManagedThreadId, i);
        }
        await Task.Delay(0);
        mrs.Set();
    }
}

public delegate Task AsyncEventHandler<TEventArgs>(
        object sender, TEventArgs e, CancellationToken token);

public static class AsynEventHandlerExtensions
{
    // invoke an async event (with null-checking)
    public static async Task InvokeAsync<TEventArgs>(
        this AsyncEventHandler<TEventArgs> handler,
        object sender, TEventArgs args, CancellationToken token)
    {
        await Task.Run(async () =>
        {
            Console.WriteLine("InvokeAsync<TEventArgs> is running on ThreadId:{0}",
                Thread.CurrentThread.ManagedThreadId);

            var delegates = handler?.GetInvocationList();
            if (delegates?.Length > 0)
            {
                var tasks =
                delegates
                .Cast<AsyncEventHandler<TEventArgs>>()
                .Select(e => Task.Run(
                    async () => await e.Invoke(sender, args, token)));
                await Task.WhenAll(tasks);
            }
        }).ConfigureAwait(false);
    }
}

internal class Client
{
    public static async Task Main(string[] args)
    {
        Console.WriteLine("Client is running on ThreadId:{0}",
            Thread.CurrentThread.ManagedThreadId);

        ManualResetEvent[] mres = new ManualResetEvent[3];
        for (int i = 0; i < mres.Length; i++)
            mres[i] = new ManualResetEvent(false);

        EventWrapper s = new EventWrapper();
        s.EventW += (new HandlerWrapper("1", mres[0])).Handler;
        s.EventW += (new HandlerWrapper("2", mres[1])).Handler;
        s.EventW += (new HandlerWrapper("3", mres[2])).Handler;

        // Change subject state and notify observers
        s.StateW = "ABC123";

        var timer = new Stopwatch();
        timer.Start();

        await s.Notify(CancellationToken.None);

        ManualResetEvent.WaitAll(mres);
        timer.Stop();
        TimeSpan timeTaken = timer.Elapsed;
        string tmp1 = "Client time taken: " +
            timeTaken.ToString(@"m\:ss\.fff");
        Console.WriteLine(tmp1);

        Console.ReadLine();
    }
}

这是执行结果:

从执行结果可以看出,我们看到EventHandler,现在异步在单独的线程上运行,从执行日志中可以看到并发性,总耗时为10.063秒。

使用TAP – Ver2的异步事件

虽然这不是本文的主要目的,但我们可以更改代码以更好地演示TAP模式。我们只对上面的项目代码做一个小的更改,改变一种方法,所有其他方法都与上面相同。

private async Task Worker(object subject, EventArgsW args)
{
    Console.WriteLine("Handler{0}.Worker is running on ThreadId:" +
        "{1}, i:0",
        name, Thread.CurrentThread.ManagedThreadId);
    StateW = args.StateW;

    for (int i = 1; i <= 2; ++i)
    {
        await Task.Delay(5000);
        Console.WriteLine("Handler{0}.Worker is running on ThreadId:" +
            "{1}, i:{2}",
            name, Thread.CurrentThread.ManagedThreadId, i);
    }
    mrs.Set();
}

现在,我们得到以下执行结果:

例如,如果我们把注意力集中在Handler1.Worker上,我们可以看到该异步方法已经在ID 586的线程的三个来自ThreadPool的不同线程上运行。由于TAP模式,这一切都很好,因为await方法的工作被ThreadPool中的下一个可用线程选择。并发性再次明显,总时间为10.101秒。

结论

实际上,事件机制提供对EventHandler的同步调用。我们在上面的例子中展示了如何异步调用EventHandler。代码中提供了两种可重用的扩展方法,它们简化了异步调用实现。好处是并行调用EventHandler,这在当今的多核系统中很重要。

参考

https://www.codeproject.com/Articles/5341837/Asynchronous-Events-in-Csharp

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值