【问题标题】:CallContext.SetData() - is object available when thread goes active-inactive-active (TPL)?CallContext.SetData() - 当线程变为活动-非活动-活动 (TPL) 时对象是否可用?
【发布时间】:2011-06-20 23:06:41
【问题描述】:

伙计们,

假设我使用线程 10、11、12 中的 CallContext.SetData() 存储了对象 Car 的三个新实例。这些线程完成执行。然后我执行另一个使用线程 10、11、12 的多线程操作(可能与第一个操作不同)。GetData() 会检索我存储的相同的三个对象吗?还是现在的上下文有所不同,这些对象消失了?

我的特殊用例是任务并行库。我正在使用 TPL 来并行化一些操作,并且我想了解在 TPL 调用之间通过 CallContext.SetData() 存储的数据会发生什么。

编辑
根据@wageoghe 的建议,我尝试了 ThreadLocal 并且成功了!

更新代码以证明这一点:

using System;
using System.Threading;
using System.Threading.Tasks;

namespace TlsTest
{

    public class Program
    {

        public static void Main()
        {
            Console.WriteLine( "-------using threadpool---------" );
            UseThreadPool();
            Console.WriteLine( "-------using tasks---------" );
            UseTasks();
            Console.WriteLine( "-------using parallel for---------" );
            UseParallelFor();
            Console.ReadKey();
        }

        public static void UseThreadPool()
        {

            var finish = new CountdownEvent( TotalThreads );

            for ( int i = 0 ; i < TotalThreads ; i++ )
            {
                ThreadPool.QueueUserWorkItem( x =>
                {
                    int id = Thread.CurrentThread.ManagedThreadId;

                    Thread.Sleep( SleepMilliseconds );

                    if ( ThreadId.IsValueCreated )
                    {
                        Console.WriteLine( "thread [{0}], tls.thread [{1}] - value already in Tls" , id , ThreadId.Value );                        
                    }
                    else
                    {                        
                        Console.WriteLine( "thread [{0}] - no Tls value" , id );
                        ThreadId.Value = id;
                    }
                    Thread.Sleep( SleepMilliseconds );
                    finish.Signal();
                } );
            }
            finish.Wait();
        }

        public static void UseTasks()
        {
            const TaskCreationOptions taskCreationOpt = TaskCreationOptions.None;

            var allTasks = new Task[ TotalThreads ];
            for ( int i = 0 ; i < TotalThreads ; i++ )
            {
                Task task = Task.Factory.StartNew( () =>
                {
                    int id = Thread.CurrentThread.ManagedThreadId;

                    Thread.Sleep( SleepMilliseconds );

                    if ( ThreadId.IsValueCreated )
                    {
                        Console.WriteLine( "thread [{0}], tls.thread [{1}] - value already in Tls" , id , ThreadId.Value );
                    }
                    else
                    {
                        Console.WriteLine( "thread [{0}] - no Tls value" , id );
                        ThreadId.Value = id;                        
                    }

                    Thread.Sleep( SleepMilliseconds );

                } , taskCreationOpt );
                allTasks[ i ] = task;
            }
            Task.WaitAll( allTasks );
        }

        public static void UseParallelFor()
        {

            var options = new ParallelOptions();
            options.MaxDegreeOfParallelism = 8;
            Parallel.For( 0 , TotalThreads , options , i =>
            {
                int id = Thread.CurrentThread.ManagedThreadId;

                Thread.Sleep( SleepMilliseconds );

                if ( ThreadId.IsValueCreated )
                {
                    Console.WriteLine( "thread [{0}], tls.thread [{1}] - value already in Tls" , id , ThreadId.Value );
                }
                else
                {
                    Console.WriteLine( "thread [{0}] - no Tls value" , id );
                    ThreadId.Value = id;                                        
                }

                Thread.Sleep( SleepMilliseconds );

            } );            
        }

        private static readonly ThreadLocal<int> ThreadId = new ThreadLocal<int>();
        private const int TotalThreads = 100;
        private const int SleepMilliseconds = 500;

    }    
}

【问题讨论】:

    标签: .net .net-3.5 .net-4.0 task-parallel-library


    【解决方案1】:

    [更新]

    实际上,我的原始答案(在这篇文章的底部)似乎有部分错误!

    我编写了一个小测试程序,用于测试从线程和任务、ThreadPool 线程以及 Parallel.For 中的线程将数据存储在 CallContext 中的场景。在 Tasks 测试和 ThreadPool 测试中,当同一个线程(由 ManagedThreadId 确定)被重用时,CallContext 中存储的数据不会再次出现。但是,在 Parallel.For 的情况下,当重用同一线程(由 ManagedThreadId 确定)时,会再次看到存储在 CallContext 中的数据。我觉得这很有趣。我不确定这些结果是否符合预期,或者我的程序是否有问题。

    要尝试每种情况,只需取消注释所需的测试功能。

    您将看到 Tasks 和 ThreadPool 线程在线程的后续重用中永远不会遇到 CallContext 数据,而 Parallel.For 线程会遇到 CallContext 数据。

    Parallel.For 的行为似乎不一致。当我运行 Parallel.For 案例时,我可以看到给定线程在重用该线程时不一定总是找到 CallContext 数据。例如,这是程序一次运行的输出(未注释 UseParallelFor):

    thread [9] - no CallContext value
    thread [10] - no CallContext value 
    thread [11] - no CallContext value
    thread [12] - no CallContext value
    thread [9], cc.thread [9] - value already in CallContext <-- this is expected as this is the main thread
    thread [10] - no CallContext value 
    thread [13] - no CallContext value
    thread [11] - no CallContext value
    thread [12] - no CallContext value
    thread [14] - no CallContext value
    thread [9], cc.thread [9] - value already in CallContext
    thread [10], cc.thread [10] - value already in CallContext 
    thread [11], cc.thread [11] - value already in CallContext
    thread [13] - no CallContext value
    thread [15] - no CallContext value
    thread [12], cc.thread [12] - value already in CallContext
    thread [16] - no CallContext value
    thread [14] - no CallContext value
    thread [9], cc.thread [9] - value already in CallContext
    thread [10] - no CallContext value
    thread [17] - no CallContext value
    thread [13], cc.thread [13] - value already in CallContext
    thread [15] - no CallContext value
    thread [11] - no CallContext value
    thread [12] - no CallContext value
    thread [14], cc.thread [14] - value already in CallContext
    thread [18] - no CallContext value
    thread [16] - no CallContext value
    thread [9], cc.thread [9] - value already in CallContext
    thread [10], cc.thread [10] - value already in CallContext 
    thread [13] - no CallContext value
    thread [15], cc.thread [15] - value already in CallContext
    thread [11], cc.thread [11] - value already in CallContext
    thread [17] - no CallContext value
    thread [19] - no CallContext value
    thread [18] - no CallContext value
    thread [16], cc.thread [16] - value already in CallContext
    thread [14] - no CallContext value
    thread [20] - no CallContext value
    thread [12], cc.thread [12] - value already in CallContext
    thread [9], cc.thread [9] - value already in CallContext
    thread [10], cc.thread [10] - value already in CallContext
    thread [21] - no CallContext value
    thread [15] - no CallContext value
    thread [11], cc.thread [11] - value already in CallContext
    thread [17], cc.thread [17] - value already in CallContext
    thread [13], cc.thread [13] - value already in CallContext
    thread [19] - no CallContext value
    thread [22] - no CallContext value
    thread [18], cc.thread [18] - value already in CallContext
    thread [16] - no CallContext value
    thread [20] - no CallContext value
    thread [14], cc.thread [14] - value already in CallContext
    thread [12], cc.thread [12] - value already in CallContext
    thread [9], cc.thread [9] - value already in CallContext
    thread [10], cc.thread [10] - value already in CallContext
    thread [23] - no CallContext value
    thread [15], cc.thread [15] - value already in CallContext
    thread [21] - no CallContext value
    thread [11], cc.thread [11] - value already in CallContext
    thread [17] - no CallContext value
    thread [13], cc.thread [13] - value already in CallContext
    thread [19], cc.thread [19] - value already in CallContext
    thread [22] - no CallContext value
    thread [16], cc.thread [16] - value already in CallContext
    thread [18] - no CallContext value
    thread [24] - no CallContext value
    thread [20], cc.thread [20] - value already in CallContext
    thread [14], cc.thread [14] - value already in CallContext
    thread [12], cc.thread [12] - value already in CallContext
    thread [9], cc.thread [9] - value already in CallContext
    thread [10] - no CallContext value
    thread [15], cc.thread [15] - value already in CallContext
    thread [21], cc.thread [21] - value already in CallContext
    thread [17], cc.thread [17] - value already in CallContext
    thread [13], cc.thread [13] - value already in CallContext
    thread [22], cc.thread [22] - value already in CallContext
    thread [18], cc.thread [18] - value already in CallContext
    thread [16], cc.thread [16] - value already in CallContext
    thread [14], cc.thread [14] - value already in CallContext
    thread [9], cc.thread [9] - value already in CallContext
    thread [10], cc.thread [10] - value already in CallContext
    thread [15], cc.thread [15] - value already in CallContext
    thread [17], cc.thread [17] - value already in CallContext
    thread [18], cc.thread [18] - value already in CallContext
    thread [16], cc.thread [16] - value already in CallContext
    thread [9], cc.thread [9] - value already in CallContext
    thread [10], cc.thread [10] - value already in CallContext
    thread [17], cc.thread [17] - value already in CallContext
    thread [18], cc.thread [18] - value already in CallContext
    thread [9], cc.thread [9] - value already in CallContext
    thread [10], cc.thread [10] - value already in CallContext
    thread [9], cc.thread [9] - value already in CallContext
    thread [10], cc.thread [10] - value already in CallContext
    thread [9], cc.thread [9] - value already in CallContext
    thread [10], cc.thread [10] - value already in CallContext
    thread [9], cc.thread [9] - value already in CallContext
    thread [10], cc.thread [10] - value already in CallContext
    thread [9], cc.thread [9] - value already in CallContext
    thread [10], cc.thread [10] - value already in CallContext
    

    如您所见,一旦在重用对象中找到一个值,它就不会永远保留在 CallContext 中。在某些情况下,对于给定线程的几次迭代,在 CallContext 中找不到该值,因此将其添加。然后迭代将报告找到该值。然后,也许,下一次迭代会说找不到该值。

    结果告诉我,对于在线程重用之间清除给定线程的数据,您不应依赖 CallContext 中保持完整的数据。他们还告诉我,在 Parallel.For 的情况下,您不应该依赖在同一线程的重用之间清除 CallContext。

    这是我的测试程序:

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    
    using System.Threading;
    using System.Threading.Tasks;
    using System.Runtime.Remoting.Messaging;
    
    namespace CallContextTest
    {
      class Program
      {
        static void Main(string[] args)
        {
          //UseTasks();
          //UseThreadPool();
          UseParallelFor();
    
          Console.ReadKey();
        }
    
        public static void UseThreadPool()
        {
          int totalThreads = 100;
    
          CountdownEvent finish = new CountdownEvent(totalThreads);
    
          for (int i = 0; i < totalThreads; i++)
          {
            int ii = i;
    
            ThreadPool.QueueUserWorkItem(x =>
            {
              int id = Thread.CurrentThread.ManagedThreadId;
    
              Thread.Sleep(1000);
    
              object o = CallContext.GetData("threadid");
              if (o == null)
              {
                //Always gets here.
                Console.WriteLine("thread [{0}] - no CallContext value", id);
                CallContext.SetData("threadid", id);
              }
              else
              {
                //Never gets here.
                Console.WriteLine("thread [{0}], cc.thread [{1}] - value already in CallContext", o, id);
              }
    
              Thread.Sleep(1000);
              finish.Signal();
            });
    
          }
    
          finish.Wait();
        }
    
        public static void UseTasks()
        {
          int totalThreads = 100;
          TaskCreationOptions taskCreationOpt = TaskCreationOptions.None;
          Task task = null;
    
    
          Task[] allTasks = new Task[totalThreads];
          for (int i = 0; i < totalThreads; i++)
          {
            int ii = i;
            task = Task.Factory.StartNew(() =>
            {
              int id = Thread.CurrentThread.ManagedThreadId;
    
              Thread.Sleep(1000);
    
              object o = CallContext.GetData("threadid");
              if (o == null)
              {
                //Always gets here.
                Console.WriteLine("thread [{0}] - no CallContext value", id);
                CallContext.SetData("threadid", id);
              }
              else
              {
                //Never gets here.
                Console.WriteLine("thread [{0}], cc.thread [{1}] - value already in CallContext", o, id);
              }
    
              Thread.Sleep(1000);
    
            }, taskCreationOpt);
            allTasks[i] = task;
          }
          Task.WaitAll(allTasks);
        }
    
        public static void UseParallelFor()
        {
          int totalThreads = 100;
          Parallel.For(0, totalThreads, i =>
          {
            int ii = i;
            int id = Thread.CurrentThread.ManagedThreadId;
    
            Thread.Sleep(1000);
    
            object o = CallContext.GetData("threadid");
            if (o == null)
            {
              //Sometimes gets here.
              Console.WriteLine("thread [{0}] - no CallContext value", id);
              CallContext.SetData("threadid", id);
            }
            else
            {
              //Sometimes gets here as threads are reused.
              Console.WriteLine("thread [{0}], cc.thread [{1}] - value already in CallContext", o, id);
            }
    
            Thread.Sleep(1000);
    
          });
        }
    
      }
    }
    

    注意,鉴于上述测试程序和我的新 cmets,在这个答案的顶部,我最初的讨论似乎是错误的。从我的测试来看,在 Tasks 和 ThreadPool 线程的情况下,存储在 CallContext 中的数据似乎在后续重复使用相同的线程 id 时不可用。但是,在 Parallel.For 的情况下,存储在 CallContext 中的数据似乎可用于同一线程的重用。

    结束更新后忽略所有内容。

    [结束更新]

    我当然不是 TPL 专家,但我最近一直在研究 CallContext.SetData(和 LogicalSetData),所以我对 CallContext 的工作原理有了一些了解。 SO上有更好的人来描述在TPL的“上下文”中CallContext数据可能会或可能不会发生的事情。

    我对 CallContext.SetData 工作原理的理解是,当线程消失时,“数据”将被清除。因此,如果您创建一个新线程,并且在该线程上执行时,您使用 CallContext.SetData 存储一些数据,那么当线程终止时数据将消失。如果您使用的是 ThreadPool 线程,则该线程永远不会死掉(嗯,也许 never 太强了),因此通过 CallContext.SetData 存储的数据在下一次在 (重用)线程。

    我的理解也是Task Parallel Library在内部使用了ThreadPool,所以当再次使用底层的ThreadPool线程时,任何通过CallContext.SetData存储的数据可能仍然存在。

    编写一个或多个小测试应该很容易,以查看将数据放入 CallContext 时会发生什么,然后检查它是否存在于同一线程的后续使用中。

    【讨论】:

    • 非常感谢您修改答案和详细解释!鉴于您的示例显示数据是否使用 CallContext 保存不一致,我想自然的问题是我们如何确保每个线程 id 只需要创建一个对象?我猜在 AppDomain 中使用具有托管线程 ID 前缀的键存储值? AppDomain.SetData 是线程安全的吗?我希望 SO 上的其他人能加入进来,让我们知道 CallContext 发生了什么。想法?
    • @SFun28 - 我的猜测是,也许人们不应该依赖 CallContext 来让特定线程 id 在使用该线程时保持不变。可以通过 CallContext.SetData 存储某些内容,然后从线程中的任何位置访问它,但是在其中存储某些内容并期望它永远与该线程 ID 相关联可能是不合适的。我想这取决于您尝试达到的效果。也许如果您在原始问题中准确描述您想要做什么,它可能会帮助人们权衡实现它的最佳方式。
    • @SFun28 - 仅供参考 - 我现在没有时间,但您可能想使用我的示例代码进行测试,但使用其他一些线程本地设施(如 Thread.SetData)存储一些信息, [ThreadStatic],ThreadLocal (msdn.microsoft.com/en-us/library/dd642243.aspx)。还要查看 CallContext.LogicalSetData - 它与 SetData 类似,但您存储的数据流到子线程。在 Jeffrey Richter 的博客(2010 年 9 月 27 日)中查看有关 LogicalSetData 的文章。
    • 感谢您的提示!!! ThreadLocal 是诀窍(ThreadStatic 也可以)。我更新了您的代码并发布为对我原始帖子的编辑。非常感谢您提供完整、有效的代码示例!
    猜你喜欢
    • 2015-04-27
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-12-05
    • 2021-02-05
    相关资源
    最近更新 更多