【问题标题】:How to get the current task reference?如何获取当前任务参考?
【发布时间】:2011-10-05 18:15:34
【问题描述】:

我如何获得对我的代码在其中执行的任务的引用?

ISomeInterface impl = new SomeImplementation();
Task.Factory.StartNew(() => impl.MethodFromSomeInterface(), new MyState());

...

void MethodFromSomeInterface()
{
    Task currentTask = Task.GetCurrentTask();    // No such method?
    MyState state = (MyState) currentTask.AsyncState();
}

由于我在调用一些接口方法,我不能只将新创建的任务作为附加参数传递。

【问题讨论】:

  • 你能把它作为参数传递给SomeImplementation的构造函数吗?更好的 IMO,将 MyState 传递给构造函数,并且根本不需要 MethodFromSomeInterface 中的 Task 知识。
  • @Stephen Cleary,好像不能换界面了。
  • 我不能改变接口,也不能改变实现。所以,我确实需要将MyState 实例与当前的Task 关联起来。
  • 此外,MethodFromSomeInterface 可以在不同的任务中同时调用。

标签: c# task-parallel-library


【解决方案1】:

由于您无法更改接口或实现,因此您必须自己做,例如,使用ThreadStaticAttribute

static class SomeInterfaceTask
{
  [ThreadStatic]
  static Task Current { get; set; }
}

...

ISomeInterface impl = new SomeImplementation();
Task task = null;
task = Task.Factory.StartNew(() =>
{
  SomeInterfaceTask.Current = task;
  impl.MethodFromSomeInterface();
}, new MyState());

...

void MethodFromSomeInterface()
{
  Task currentTask = SomeInterfaceTask.Current;
  MyState state = (MyState) currentTask.AsyncState();
}

【讨论】:

  • 这真的是线程安全的吗?事实上,除了在StartNew 线程中使用 lambda 参数之外还有什么安全的吗?在我看来,task 变量可能会在 lambda 甚至运行之前超出范围。
  • ThreadStatic 与 TaskStatic 不同,如果它存在的话。这对于默认调度程序可能是安全的,但不能保证线程不会被多个任务重用,在这种情况下,它没有您可能期望的保护。这可能是最糟糕的错误——看起来不错,运行良好(通常),然后在某个地方出现难以重现的错误。
  • 使用此代码,Current 总是在调用 MethodFromSomeInterface 之前立即设置(在同一线程上)。线程是否被重用于另一个任务并不重要。
  • 我明白你现在在说什么了。但是线程池永远不会那样做;任何排队到线程池的(完全同步的)工作一旦开始,就会一直运行到完成。实现“暂停当前正在运行的任务并在同一线程上运行另一个任务”将非常复杂,并导致更差性能吞吐量,所以没有理由相信线程池会做到这一点。
  • task = Task.Factory.StartNew...SomeInterfaceTask.Current = task; 中设置任务对象之间存在竞争条件...
【解决方案2】:

如果您可以使用 .NET 4.6 或更高版本、.NET Standard 或 .NET Core,他们已经使用 AsyncLocal 解决了这个问题。 https://docs.microsoft.com/en-gb/dotnet/api/system.threading.asynclocal-1?view=netframework-4.7.1

如果没有,您需要在使用之前设置数据存储,并通过闭包而不是线程或任务访问它。 ConcurrentDictionary 将帮助掩盖您在执行此操作时所犯的任何错误。

当代码等待时,当前任务释放线程——即线程与任务无关,至少在编程模型中是这样。

演示:

// I feel like demo code about threading needs to guarantee
// it actually has some in the first place :)
// The second number is IOCompletionPorts which would be relevant
// if we were using IO (strangely enough).
var threads = Environment.ProcessorCount * 4;
ThreadPool.SetMaxThreads(threads, threads);
ThreadPool.SetMinThreads(threads, threads);

var rand = new Random(DateTime.Now.Millisecond);

var tasks = Enumerable.Range(0, 50)
    .Select(_ =>
    {
        // State store tied to task by being created in the same closure.
        var taskState = new ConcurrentDictionary<string, object>();
        // There is absolutely no need for this to be a thread-safe
        // data structure in this instance but given the copy-pasta,
        // I thought I'd save people some trouble.

        return Task.Run(async () =>
        {
            taskState["ThreadId"] = Thread.CurrentThread.ManagedThreadId;
            await Task.Delay(rand.Next() % 100);
            return Thread.CurrentThread.ManagedThreadId == (int)taskState["ThreadId"];
        });
    })
    .ToArray();

Task.WaitAll(tasks);
Console.WriteLine("Tasks that stayed on the same thread: " + tasks.Count(t => t.Result));
Console.WriteLine("Tasks that didn't stay on the same thread: " + tasks.Count(t => !t.Result));

【讨论】:

    【解决方案3】:

    这是一个“hacky”类,可用于此目的。
    只需使用 CurrentTask 属性来获取当前正在运行的任务。
    我强烈建议不要在生产代码附近的任何地方使用它!

    public static class TaskGetter
    {
        private static string _propertyName;
        private static Type _taskType;
        private static PropertyInfo _property;
        private static Func<Task> _getter;
    
        static TaskGetter()
        {
            _taskType = typeof(Task);
            _propertyName = "InternalCurrent";
            SetupGetter();
        }
    
        public static void SetPropertyName(string newName)
        {
            _propertyName = newName;
            SetupGetter();
        }
    
        public static Task CurrentTask
        {
            get
            {
                return _getter();
            }
        }
    
        private static void SetupGetter()
        {
            _getter = () => null;
            _property = _taskType.GetProperties(BindingFlags.Static | BindingFlags.NonPublic).Where(p => p.Name == _propertyName).FirstOrDefault();
            if (_property != null)
            {
                _getter = () =>
                {
                    var val = _property.GetValue(null);
                    return val == null ? null : (Task)val;
                };
            }
        }
    }
    

    【讨论】:

    • 这是一种很好的 hacky,它会提前中断(解析属性),所以还不错。 AsyncState(以及通常与命名空间无关的随机数据存储)不是任何人都应该使用的东西,但如果是的话,我会在生产中使用它。竖起大拇指。
    【解决方案4】:

    以下示例显示了如何实现它,并使用@stephen-cleary 提供的答案解决了问题。这有点令人费解,但关键在于下面的 TaskContext 类,它使用 CallContext.LogicalSetDataCallContext.LogicalGetDataCallContext .FreeNamedDataSlot 这对于创建您自己的任务上下文很有用。剩下的就是回答 OP 的问题了:

    class Program
    {
        static void Main(string[] args)
        {
            var t1 = Task.Factory.StartNewWithContext(async () => { await DoSomething(); });
            var t2 = Task.Factory.StartNewWithContext(async () => { await DoSomething(); });
    
            Task.WaitAll(t1, t2);
        }
    
        private static async Task DoSomething()
        {
            var id1 = TaskContext.Current.Task.Id;
            Console.WriteLine(id1);
            await Task.Delay(1000);
    
            var id2 = TaskContext.Current.Task.Id;
            Console.WriteLine(id2);
            Console.WriteLine(id1 == id2);
        }
    }
    
    public static class TaskFactoryExtensions
    {
        public static Task StartNewWithContext(this TaskFactory factory, Action action)
        {
            Task task = null;
    
            task = new Task(() =>
            {
                Debug.Assert(TaskContext.Current == null);
                TaskContext.Current = new TaskContext(task);
                try
                {
                    action();
                }
                finally
                {
                    TaskContext.Current = null;
                }
            });
    
            task.Start();
    
            return task;
        }
    
        public static Task StartNewWithContext(this TaskFactory factory, Func<Task> action)
        {
            Task<Task> task = null;
    
            task = new Task<Task>(async () =>
            {
                Debug.Assert(TaskContext.Current == null);
                TaskContext.Current = new TaskContext(task);
                try
                {
                    await action();
                }
                finally
                {
                    TaskContext.Current = null;
                }
            });
    
            task.Start();
    
            return task.Unwrap();
        }
    }
    
    public sealed class TaskContext
    {
        // Use your own unique key for better performance
        private static readonly string contextKey = Guid.NewGuid().ToString();
    
        public TaskContext(Task task)
        {
            this.Task = task;
        }
    
        public Task Task { get; private set; }
    
        public static TaskContext Current
        {
            get { return (TaskContext)CallContext.LogicalGetData(contextKey); }
            internal set
            {
                if (value == null)
                {
                    CallContext.FreeNamedDataSlot(contextKey);
                }
                else
                {
                    CallContext.LogicalSetData(contextKey, value);
                }
            }
        }
    }
    

    【讨论】:

      【解决方案5】:

      如果您可以更改界面(当我遇到类似问题时,这对我来说不是一个限制),对我来说,Lazy&lt;Task&gt; 似乎可以用来解决这个问题。于是我试了一下。

      它有效,至少对于我想要“当前任务”的含义而言。但它是微妙的代码,因为AsyncMethodThatYouWantToRun 必须做Task.Yield()

      如果您让步,它将失败并显示System.AggregateException: 'One or more errors occurred. (ValueFactory attempted to access the Value property of this instance.)'

      Lazy<Task> eventuallyATask = null; // silly errors about uninitialized variables :-/
      eventuallyATask = new Lazy<Task>(
          () => AsyncMethodThatYouWantToRun(eventuallyATask));
      
      Task t = eventuallyATask.Value; // actually start the task!
      
      async Task AsyncMethodThatYouWantToRun(Lazy<Task> lazyThisTask)
      {
          await Task.Yield(); // or else, the 'task' object won't finish being created!
      
          Task thisTask = lazyThisTask.Value;
          Console.WriteLine("you win! Your task got a reference to itself");
      }
      
      t.Wait();
      

      或者,我们可以一直执行任务,而不是 Task.Yield 的微妙之处,并使用 TaskCompletionSource&lt;Task&gt; 来解决它。 (消除任何潜在的错误/死锁,因为我们的任务安全地释放线程,直到它可以知道自己!)

          var eventuallyTheTask = new TaskCompletionSource<Task>();
          Task t = AsyncMethodThatYouWantToRun(eventuallyTheTask.Task); // start the task!
          eventuallyTheTask.SetResult(t); //unblock the task and give it self-knowledge
      
          async Task AsyncMethodThatYouWantToRun(Task<Task> thisTaskAsync)
          {
              Task thisTask = await thisTaskAsync; // gets this task :)
              Console.WriteLine("you win! Your task got a reference to itself (== 't')");
          }
      
          t.Wait();
      

      【讨论】:

        猜你喜欢
        • 2012-03-19
        • 1970-01-01
        • 2011-03-19
        • 2014-06-25
        • 2010-09-28
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多