如果我每次都从不同的线程调用事件,为啥会从同一个线程触发事件处理程序的多次执行?

Posted

技术标签:

【中文标题】如果我每次都从不同的线程调用事件,为啥会从同一个线程触发事件处理程序的多次执行?【英文标题】:Why are several executions of an eventhandler triggered from the same thread if I am invoking the event from a different thread each time?如果我每次都从不同的线程调用事件,为什么会从同一个线程触发事件处理程序的多次执行? 【发布时间】:2020-08-09 15:35:30 【问题描述】:

为了了解有关 .NET 中事件的基础知识,我制作了一个控制台应用程序,该应用程序对一排 5 个多米诺骨牌令牌进行建模,当用手指按下第一个令牌时,这些令牌会掉落。每对连续令牌之间的交互由当前令牌中的事件 Fall 和下一个令牌中的事件处理程序 Collided 处理。当一个令牌掉落时,它会调用一个 Fall 事件,下一个令牌由 Collided 委托订阅。每个令牌需要 1000 毫秒才能下落。

程序的第一个版本,在单线程中运行,需要 5000 毫秒才能按预期完成:

using System;
using System.Threading;

namespace SimpleRubeGoldbergMachine

    public class Finger  

    public class DominoToken
    
        public event EventHandler Fall;

        public void KickOff()
        
            //Collides with your finger and kicks off the chain reaction
            this.Collided(new Finger(), EventArgs.Empty);
        

        public void Collided(object sender, EventArgs e)
        
            var objectType = sender.GetType().Name;
            Console.WriteLine($"A objectType has bumped into the domino token.");
            Console.WriteLine("The token falls!");

            Thread.Sleep(1000);

            //On falling, the domino token collides with the next token
            this.OnFalling(EventArgs.Empty);
        

        private void OnFalling(EventArgs e)
        
            Fall?.Invoke(this, EventArgs.Empty);
        
    

    public class Program
    
        private static void Main(string[] args)
        
            var rowOfDominoes = new[]
            
                new DominoToken(),
                new DominoToken(),
                new DominoToken(),
                new DominoToken(),
                new DominoToken()
            ;

            //Attach the Collided delegate of each domino Token to the Fall event of the previous Token
            for (var i = 0; i < 4; i++)
            
                rowOfDominoes[0].Fall += rowOfDominoes[i + 1].Collided;
            

            //Kick-off
            rowOfDominoes[0].KickOff();
        
    

控制台输出:

A Finger has bumped into the domino token.
The token falls!
A DominoToken has bumped into the domino token.
The token falls!
A DominoToken has bumped into the domino token.
The token falls!
A DominoToken has bumped into the domino token.
The token falls!
A DominoToken has bumped into the domino token.
The token falls!

当我尝试在不同的线程中运行每个事件处理程序时,我的问题就出现了。请注意,使用不同线程的原因是我试图从一个可以并行运行(即下降)的事件中启动几行多米诺骨牌,但这部分代码不是重现我的问题所必需的。

我从 Collided 事件处理程序中为每个标记启动一个新线程。令我惊讶的是,该程序只需要 2000 毫秒即可完成。我期望事件处理程序的每次执行都是从不同的线程产生的,但它们都是从同一个线程产生的。出于这个原因,同时发生了几个 Fall 事件(这不是预期的行为)。

我为每次 Collided() 的执行添加了带有 spawner 线程 ID 和当前线程的跟踪,以便解决问题:

using System;
using System.Threading;

namespace SimpleRubeGoldbergMachine


    public class Finger  

    public class DominoToken
    
        public event EventHandler<int> Fall;

        public void KickOff()
        
            //Collides with your finger and kicks off the chain reaction
            this.Collided(new Finger(), Thread.CurrentThread.ManagedThreadId);
        

        public void Collided(object sender, int threadId)
        
            var thread = new Thread(() =>
            
                var objectType = sender.GetType().Name;
                Console.WriteLine($"A objectType has bumped into the domino token.");
                Console.WriteLine("The token falls!");

                Console.WriteLine("Spawner Thread: " + threadId);
                Console.WriteLine("Current Thread: " + Thread.CurrentThread.ManagedThreadId);

                Thread.Sleep(1000);

                //On falling, the domino token collides with the next token
                this.OnFalling(Thread.CurrentThread.ManagedThreadId);
            );

            thread.IsBackground = false;
            thread.Start();
        

        private void OnFalling(int threadId)
        
            Fall?.Invoke(this, threadId);
        
    

    public class Program
    
        private static void Main(string[] args)
        
            var rowOfDominoes = new[]
            
                new DominoToken(),
                new DominoToken(),
                new DominoToken(),
                new DominoToken(),
                new DominoToken()
            ;

            //Attach the Collided delegate of each domino Token to the Fall event of the previous Token
            for (var i = 0; i < 4; i++)
            
                rowOfDominoes[0].Fall += rowOfDominoes[i + 1].Collided;
            

            //Kick-off
            rowOfDominoes[0].KickOff();
        
    

预期的控制台输出:

A Finger has bumped into the domino token.
The token falls!
Spawner Thread: 1
Current Thread: 2
A DominoToken has bumped into the domino token.
The token falls!
Spawner Thread: 2
Current Thread: 3
A DominoToken has bumped into the domino token.
The token falls!
Spawner Thread: 3
Current Thread: 4
A DominoToken has bumped into the domino token.
The token falls!
Spawner Thread: 4
Current Thread: 5
A DominoToken has bumped into the domino token.
The token falls!
Spawner Thread: 5
Current Thread: 6

实际控制台输出:

A Finger has bumped into the domino token.
The token falls!
Spawner Thread: 1
Current Thread: 5
A DominoToken has bumped into the domino token.
The token falls!
A DominoToken has bumped into the domino token.
The token falls!
Spawner Thread: 5
Current Thread: 6
A DominoToken has bumped into the domino token.
The token falls!
Spawner Thread: 5
Current Thread: 9
A DominoToken has bumped into the domino token.
The token falls!
Spawner Thread: 5
Current Thread: 8
Spawner Thread: 5
Current Thread: 7

为什么 DominoToken 的不同实例的多个委托是从同一个线程产生的 (5)?第一次交互(finger-1st token,#1)和第二次交互(2nd token-3rd token,thread #5)之间的生成线程实际上是不同的

【问题讨论】:

我认为这行有一个错误:rowOfDominoes[0].Fall += rowOfDominoes[i + 1].Collided; 您正在将多个处理程序附加到同一个多米诺令牌的事件。 @TheodorZoulias 是对的 - 所有其他四个多米诺骨牌都在处理来自同一个多米诺骨牌的事件。你有一个多米诺骨牌同时与其他四个相撞。如果您希望它们一次碰撞一个,请在添加事件处理程序时将 rowOfDominoes[0] 替换为 rowOfDominoes[i]。 @Theodor Zoulias 和 Sean Skelly,感谢您指出我的愚蠢错误。现在可以了。 现在你必须弄清楚为什么程序的第一个版本(单线程),尽管 bug 可以按预期工作。顺便说一句,很抱歉投反对票。你在这个问题上做了很多工作。所以你有一个(虚拟的)+1 我的努力。 :-) 别担心,我明白。我现在无法调试它,但据我所知,附加到单个事件的多个事件处理程序是按顺序执行的,因此 Collide 一个接一个地执行 5 次是预期的行为。因为 Collide 事件处理程序的每次执行都会产生完全相同的输出,无论 dominoToken 实例如何,我都没有意识到它们都是来自同一个实例的执行。虽然它实际上不是预期的行为,但它看起来确实如此。 【参考方案1】:

代码没有按预期工作,因为事件处理程序的附件不正确。正如 Theodor Zoulias 和 Sean Skelly 在their comments 中指出的那样:

这一行有一个错误:rowOfDominoes[0].Fall += rowOfDominoes[i + 1].Collided;您将多个处理程序附加到同一个多米诺令牌的事件。

你有一个多米诺骨牌同时与其他四个相撞。如果您希望它们一次碰撞一个,请在添加事件处理程序时将 rowOfDominoes[0] 替换为 rowOfDominoes[i]。

通过此修复,它可以按预期工作。

【讨论】:

以上是关于如果我每次都从不同的线程调用事件,为啥会从同一个线程触发事件处理程序的多次执行?的主要内容,如果未能解决你的问题,请参考以下文章

为啥 BackgroundWorker 的 Progress 事件运行在不同的线程上?

为啥 WCF 服务能够处理来自不同进程的调用而不是来自线程的调用

为啥 Application_Start 从不同的线程调用两次?

线程之间如何调用变量

C++ volatile关键字(多线程中声明为易变值不稳定值,告诉程序每次都从内存读取,不被编译优化,防止被优化后变量异常)

为啥标准 C# 事件调用模式是线程安全的,没有内存屏障或缓存失效?类似的代码呢?