TPL 和模拟

Posted

技术标签:

【中文标题】TPL 和模拟【英文标题】:TPL and Impersonation 【发布时间】:2014-07-22 13:33:01 【问题描述】:

我正在使用 Impersonator 类(请参阅http://www.codeproject.com/KB/cs/zetaimpersonator.aspx)在运行时切换用户上下文。

同时,我现在正在将我的程序从单线程设计重组为多线程设计(使用 TPL / 主要是 Task 类型)。

由于这种模拟是在线程级别的原生 API 函数中发生的,我想知道 TPL 与它的兼容程度。如果我在任务中更改用户上下文,如果任务完成并且线程返回到 ThreadPool,用户上下文是否仍然设置?在此任务中启动的其他任务是否会隐式使用该上下文?

我试图通过单元测试自己找出答案,以及我从第一个单元测试中的推论:

在线程内启动的任务在“神奇地”模拟时继承了用户上下文。 当源任务/线程执行 Impersonation.Undo() 时,继承的模拟不会被撤销。

第二个单元测试表明,如果没有显式撤销模拟,用户上下文“存活”在返回线程池的线程上,其他后续任务现在可能会随机运行在不同的用户上下文中,具体取决于线程他们被分配到。

我的问题:有没有比通过本机 API 调用更好的方式来实现模拟?也许一个更专注于 TPL 并绑定到任务而不是线程?如果有一些改变可以降低在随机上下文中执行任务的风险,我很乐意这样做......

这是我编写的 2 个单元测试。如果您想自己运行测试,则必须稍微修改代码以使用自己的机制来接收用户凭据,并且 log4net 调用肯定很容易删除。

是的,我知道,Thread.Sleep() 的风格很糟糕,我因为在那里偷懒而感到内疚... ;-)

  private string RetrieveIdentityUser()
  
     var windowsIdentity = WindowsIdentity.GetCurrent();
     if (windowsIdentity != null)
     
        return windowsIdentity.Name;
     
     return null;
  

  [TestMethod]
  [TestCategory("LocalTest")]
  public void ThreadIdentityInheritanceTest()
  
     string user;
     string pw;
     Security.Decode(CredentialsIdentifier, out user, out pw);

     string userInMainThread = RetrieveIdentityUser();
     string userInTask1BeforeImpersonation = null;
     string userInTask1AfterImpersonation = null;
     string userInTask2 = null;
     string userInTask3 = null;
     string userInTask2AfterImpersonationUndo = null;

     var threadlock = new object();
     lock (threadlock)
     
        new Task(
           () =>
           
              userInTask1BeforeImpersonation = RetrieveIdentityUser();
              using (new Impersonator(user, Domain, pw))
              
                 userInTask1AfterImpersonation = RetrieveIdentityUser();
                 lock (threadlock)
                 
                    Monitor.Pulse(threadlock);
                 
                 new Task(() =>
                 
                    userInTask2 = RetrieveIdentityUser();
                    Thread.Sleep(200);
                    userInTask2AfterImpersonationUndo = RetrieveIdentityUser();
                 ).Start();
                 Thread.Sleep(100);
              
           ).Start();

        Monitor.Wait(threadlock);
        RetrieveIdentityUser();
        new Task(() =>  userInTask3 = RetrieveIdentityUser(); ).Start();
        Thread.Sleep(300);

        Assert.IsNotNull(userInMainThread);
        Assert.IsNotNull(userInTask1BeforeImpersonation);
        Assert.IsNotNull(userInTask1AfterImpersonation);
        Assert.IsNotNull(userInTask2);
        Assert.IsNotNull(userInTask3);

        // context in both threads equal before impersonation
        Assert.AreEqual(userInMainThread, userInTask1BeforeImpersonation);

        // context has changed in task1
        Assert.AreNotEqual(userInTask1BeforeImpersonation, userInTask1AfterImpersonation);

        // impersonation to the expected user
        Assert.AreEqual(Domain + "\\" + user, userInTask1AfterImpersonation);

        // impersonation is inherited
        Assert.AreEqual(userInTask1AfterImpersonation, userInTask2);

        // a newly started task from the main thread still shows original user context
        Assert.AreEqual(userInMainThread, userInTask3);

        // inherited impersonation is not revoked
        Assert.AreEqual(userInTask2, userInTask2AfterImpersonationUndo);
     
  

  [TestMethod]
  [TestCategory("LocalTest")]
  public void TaskImpersonationTest()
  
     int tasksToRun = 100; // must be more than the minimum thread count in ThreadPool
     string userInMainThread = RetrieveIdentityUser();
     var countdownEvent = new CountdownEvent(tasksToRun);
     var exceptions = new List<Exception>();
     object threadLock = new object();
     string user;
     string pw;
     Security.Decode(CredentialsIdentifier, out user, out pw);
     for (int i = 0; i < tasksToRun; i++)
     
        new Task(() =>
        
           try
           
              try
              
                 Logger.DebugFormat("Executing task 0 on thread 1...", Task.CurrentId, Thread.CurrentThread.GetHashCode());
                 Assert.AreEqual(userInMainThread, RetrieveIdentityUser());
                 //explicitly not disposing impersonator / reverting impersonation
                 //to see if a thread reused by TPL has its user context reset
                 // ReSharper disable once UnusedVariable
                 var impersonator = new Impersonator(user, Domain, pw);
                 Assert.AreEqual(Domain + "\\" + user, RetrieveIdentityUser());
              
              catch (Exception e)
              
                 lock (threadLock)
                 
                    var newException = new Exception(string.Format("Task 0 on Thread 1: 2", Task.CurrentId, Thread.CurrentThread.GetHashCode(), e.Message));
                    exceptions.Add(newException);
                    Logger.Error(newException);
                 
              
           
           finally
           
              countdownEvent.Signal();
           
        ).Start();
     
     if (!countdownEvent.Wait(TimeSpan.FromSeconds(5)))
     
        throw new TimeoutException();
     
     Assert.IsTrue(exceptions.Any());
     Assert.AreEqual(typeof(AssertFailedException), exceptions.First().InnerException.GetType());
  

【问题讨论】:

【参考方案1】:

WindowsIdentity(通过模拟更改)存储在SecurityContext 中。您可以在various ways 中确定这种模拟如何“流动”。既然您提到了,您正在使用 p/invoke,请注意 SecurityContext documentation 中的警告:

公共语言运行时 (CLR) 可以识别模拟操作 仅使用托管代码执行,而不是执行模拟 在托管代码之外,例如通过平台调用...

但是,我不完全确定模拟是否真的从一个任务继承到另一个任务,或者您观察到的行为是否是由于任务内联。在某些情况下,新任务可能会在同一个线程池线程上同步执行。您可以找到关于此here 的精彩讨论。

不过,如果您想确保某些任务始终在模拟下运行,而其他任务则不会,我建议您研究自定义任务计划程序。 how to write your own 上的 MSDN 上有文档,包括 code-samples for a few common types of schedulers,您可以将其用作起点。

由于模拟是针对每个线程的设置,因此您可以拥有自己的任务调度程序,在执行任务时围绕一个线程(或几个线程)在模拟下运行。当您有许多小型工作单元时,这还可以减少您必须切换到模拟和退出模拟的次数。

【讨论】:

以上是关于TPL 和模拟的主要内容,如果未能解决你的问题,请参考以下文章

TPL 数据流是不是适用于这种设计类型?

Python 模拟淘宝登录的两种方法

TPL-Dataflow 是不是适用于高并发应用程序?

一些字符和字符串库函数操作模拟实现

iPhone模拟器和Android模拟器之间的区别

Arduino提供多少个模拟接口,模拟信号的分辨率为多少位?