【问题标题】:How to force multiple commands to execute in same threading timeslice?如何强制多个命令在同一个线程时间片中执行?
【发布时间】:2009-11-19 18:28:35
【问题描述】:

我有一个 C# 应用程序需要在不中断数据流的情况下将数据输入流热交换到新的处理程序类。

为此,由于 CPU 切换,我必须在单个线程中执行多个步骤,而没有任何其他线程(大部分是数据接收线程)在它们之间运行。

这是情况的简化版本,但它应该能说明问题。

void SwapInputHandler(Foo oldHandler, Foo newHandler)
{
    UnhookProtocol(oldHandler);
    HookProtocol(newHandler);
}

这两行(unhook 和 hook)必须在同一个 cpu 片中执行,以防止任何数据包通过,以防另一个线程在它们之间执行。

如何确保这两个命令使用 C# 线程方法按顺序运行?

编辑
似乎有些混乱,所以我会尝试更具体。我的意思不是同时执行,只是在同一个 cpu 时间片中,这样在这两个完成之前没有线程执行。锁不是我要找的,因为这只会阻止在两个命令运行之前再次执行此代码。在这些命令完成之前,我需要阻止 ANY THREAD 运行。另外,我再次说这是我的问题的简化版本,所以不要试图解决我的例子,请回答问题。

【问题讨论】:

  • 您不应该关心托管代码中的处理器调度。可能有更好的方法来做你想做的事。你能提供更多关于你想要达到的目标的细节吗?
  • 您不希望它们同时运行 - 您希望它们按顺序运行而不会中断。并发意味着它们都在完全相同的时间运行。
  • 好的-你的问题起初是误导性的。为什么不锁定不希望超过 1 个线程访问的资源?
  • 根据您的问题,我认为您对锁定的工作原理有误解。您可以锁定以防止一段代码被输入两次 - 就像您在问题中提到的那样。但您也可以锁定以防止访问一个或一组变量。
  • @RichardOD 如果我锁定资源,我会通过程序输入数据流。我需要通过新的处理程序重新路由它,而程序的其余部分不知道发生了变化。

标签: c# concurrency multithreading


【解决方案1】:

在单个时间片中执行操作完全没有帮助 - 该操作可以在另一个内核或处理器上并行执行,并在您执行交换时访问流。您必须使用锁定来防止每个人在流处于不一致状态时访问它。

【讨论】:

    【解决方案2】:

    您的数据接收线程需要锁定访问处理程序指针,并且您需要锁定更改处理程序指针。

    或者,如果您的处理程序是单个变量,您可以使用 Interlocked.Exchange() 以原子方式交换值。

    【讨论】:

    • 这行不通,访问处理程序引用的锁立即返回,您需要线程之间的共享锁,该锁使用应该是原子的处理程序锁定所有逻辑操作(即读取后跟写)
    • @Pop 虽然这是明智的建议,但我们无法确定,因为没有给出实现细节。完全可以正确使用Interlocked.Exchange来达到预期的效果。
    【解决方案3】:

    为什么不从另一个方向着手,让有问题的线程处理交换。据推测,当有数据要处理时,某些东西会被唤醒,并将其传递给当前的 Foo。您能否向该线程发布通知,它需要在下次唤醒时交换新的处理程序?我想,这样就不会那么令人担忧了。

    【讨论】:

    • 这个简单的例子并不代表整个问题,我只是在寻找一个非常具体的答案来实现我的解决方案的一部分。由于应用程序其余部分的结构方式,我无法使用您的建议。
    【解决方案4】:

    好的 - 回答您的具体问题。

    您可以枚举进程中的所有线程并在每个线程(活动线程除外)上调用 Thread.Suspend(),进行更改,然后调用 Thread.Resume()。

    【讨论】:

    • 如果另一个线程同时尝试做同样的事情怎么办?有什么可怕的事情会出错吗?是否存在每个线程都挂起的情况?
    • 是的 - 如果两个线程这样做,他们可以互相杀死。我最初的建议是使用锁,但显然 OP 知道得更好......
    【解决方案5】:

    假设您的处理程序是线程安全的,我的建议是在您的处理程序上编写一个公共包装器,该包装器使用私有锁执行所需的所有锁定,以便您可以在幕后安全地更改处理程序。

    如果您这样做,您还可以使用ReaderWriterLockSlim,来访问允许并发读取访问的包装处理程序。

    或者您可以构建您的包装类和处理程序类,这样就不需要锁定,并且可以使用简单的互锁写入或比较交换来完成处理程序淹没。

    下面是例子:

    public interface IHandler
    {
        void Foo();
        void Bar();
    }
    
    public class ThreadSafeHandler : IHandler
    {
    
        ReaderWriterLockSlim rwLock = new ReaderWriterLockSlim();
        IHandler wrappedHandler;
    
        public ThreadSafeHandler(IHandler handler)
        {
            wrappedHandler = handler;
        }
    
        public void Foo()
        {
            try
            {
                rwLock.EnterReadLock();
                wrappedHandler.Foo();
            }
            finally
            {
                rwLock.ExitReadLock();
            }
        }
    
        public void Bar()
        {
            try
            {
                rwLock.EnterReadLock();
                wrappedHandler.Foo();
            }
            finally
            {
                rwLock.ExitReadLock();
            }
        }
    
        public void SwapHandler(IHandler newHandler)
        {
            try
            {
                rwLock.EnterWriteLock();
                UnhookProtocol(wrappedHandler);
                HookProtocol(newHandler);
            }
            finally
            {
                rwLock.ExitWriteLock();
            }
        }
    }
    

    请注意,如果处理程序的方法需要原子操作,这仍然不是线程安全的,那么您需要在线程之间使用高阶锁定或在包装器类上添加方法以支持线程安全的原子操作(例如, BeginTreadSafeBlock() 后跟 EndTreadSafeBlock() 锁定包装的处理程序以进行一系列操作的写入。

    【讨论】:

      【解决方案6】:

      你不能,你不能这样做是合乎逻辑的。您可以做的最好的事情是避免任何其他线程破坏这两个操作之间的状态(如前所述)。

      这就是为什么你不能:

      想象一下有一个块告诉操作系统在你在那个块上时永远不要进行线程切换。这在技术上是可行的,但会导致到处挨饿。

      您可能认为您的线程是唯一被使用的线程,但这是一个不明智的假设。有垃圾收集器,有与线程池线程一起工作的异步操作,外部引用,例如 COM 对象可以跨越它自己的线程(在你的内存空间中),这样当你在它的时候没有人可以前进。

      假设您在 HookOperation 方法中进行了很长的操作。它涉及许多非泄漏操作,但是由于垃圾收集器无法接管以释放您的资源,因此您最终会失去任何内存。或者想象您调用一个使用多线程处理您的请求的 COM 对象......但它无法启动新线程(它可以启动它们但它们永远不会运行)然后加入它们等待它们完成后再来回来......因此你加入自己,永远不会回来!。

      【讨论】:

        【解决方案7】:

        正如其他发帖人已经说过的,您不能从用户模式代码强制执行系统范围的关键部分。但是,您不需要它来实现热插拔。

        方法如下。

        实现一个与您的可热交换Foo 对象具有相同接口的代理。代理应调用HookProtocol 并且永远不会取消挂钩(直到您的应用程序停止)。它应包含对当前Foo 处理程序的引用,您可以在需要时将其替换为新实例。代理应将其从挂钩函数接收到的数据定向到当前处理程序。此外,它还应提供一种原子替换当前Foo 处理程序实例的方法(有多种实现方法,从简单的互斥体到无锁)。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2013-04-12
          • 2022-07-25
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2021-01-28
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多