2015-03-12 16 views
9

Tùy thuộc vào việc tôi đang sử dụng mã dựa trên mã async/await hay mã TPL, tôi nhận được hai hành vi khác nhau liên quan đến việc dọn dẹp hợp lý CallContext.Làm sạch CallContext trong TPL

tôi có thể thiết lập và rõ ràng logic CallContext chính xác như tôi mong đợi nếu tôi sử dụng async sau/chờ đợi mã:

class Program 
{ 
    static async Task DoSomething() 
    { 
     CallContext.LogicalSetData("hello", "world"); 

     await Task.Run(() => 
      Debug.WriteLine(new 
      { 
       Place = "Task.Run", 
       Id = Thread.CurrentThread.ManagedThreadId, 
       Msg = CallContext.LogicalGetData("hello") 
      })) 
      .ContinueWith((t) => 
       CallContext.FreeNamedDataSlot("hello") 
       ); 

     return; 
    } 

    static void Main(string[] args) 
    { 
     DoSomething().Wait(); 

     Debug.WriteLine(new 
     { 
      Place = "Main", 
      Id = Thread.CurrentThread.ManagedThreadId, 
      Msg = CallContext.LogicalGetData("hello") 
     }); 

    } 
} 

Ở trên kết quả đầu ra như sau:

{Nơi = Task.Run , Id = 9, Msg = thế giới}
{Nơi = Main, Id = 8, Msg =}

Thông báo các 012.cho biết rằng CallContext trên chủ đề chính đã được giải phóng và trống.

Nhưng khi tôi chuyển sang tinh khiết TPL mã/TAP tôi không thể đạt được hiệu quả tương tự ...

class Program 
{ 
    static Task DoSomething() 
    { 
     CallContext.LogicalSetData("hello", "world"); 

     var result = Task.Run(() => 
      Debug.WriteLine(new 
      { 
       Place = "Task.Run", 
       Id = Thread.CurrentThread.ManagedThreadId, 
       Msg = CallContext.LogicalGetData("hello") 
      })) 
      .ContinueWith((t) => 
       CallContext.FreeNamedDataSlot("hello") 
       ); 

     return result; 
    } 

    static void Main(string[] args) 
    { 
     DoSomething().Wait(); 

     Debug.WriteLine(new 
     { 
      Place = "Main", 
      Id = Thread.CurrentThread.ManagedThreadId, 
      Msg = CallContext.LogicalGetData("hello") 
     }); 
    } 
} 

Các kết quả trên như sau:

{Nơi = Task.Run , Id = 10, Msg = thế giới}
{Nơi = Main, Id = 9, Msg = thế giới}

có bất cứ điều gì tôi có thể làm gì để ép buộc TPL để "tự do" sự hợp lý CallContext giống như mã async/await hiện không?

Tôi không quan tâm đến các lựa chọn thay thế cho CallContext.

Tôi hy vọng sẽ nhận được mã TPL/TAP ở trên được khắc phục để tôi có thể sử dụng nó trong các dự án nhắm mục tiêu khuôn khổ .net 4.0. Nếu điều đó là không thể trong .net 4.0, tôi vẫn tò mò nếu nó có thể được thực hiện trong .net 4.5.

Trả lời

8

Trong một phương pháp async các CallContext được sao chép trên ghi:

Khi một phương pháp async bắt đầu , nó thông báo ngữ cảnh cuộc gọi hợp lý của nó để kích hoạt hành vi sao chép trên ghi. Điều này có nghĩa là ngữ cảnh cuộc gọi logic hiện tại không thực sự thay đổi, nhưng nó được đánh dấu sao cho nếu mã của bạn gọi CallContext.LogicalSetData, dữ liệu ngữ cảnh cuộc gọi logic được sao chép vào ngữ cảnh cuộc gọi logic mới hiện tại trước khi nó được thay đổi.

Từ Implicit Async Context ("AsyncLocal")

Điều đó có nghĩa rằng trong phiên bản async bạn các CallContext.FreeNamedDataSlot("hello") tiếp tục là không cần thiết như ngay cả khi không nó:

static async Task DoSomething() 
{ 
    CallContext.LogicalSetData("hello", "world"); 

    await Task.Run(() => 
     Console.WriteLine(new 
     { 
      Place = "Task.Run", 
      Id = Thread.CurrentThread.ManagedThreadId, 
      Msg = CallContext.LogicalGetData("hello") 
     })); 
} 

Các CallContext trong Main sẽ không chứa các khe "hello":

{Nơi = Task.Run, Id = 3, Msg = thế giới}
{Nơi = Main, Id = 1, Msg =}

Trong TPL tương đương tất cả các mã bên ngoài Task.Run (mà phải được Task.Factory.StartNewTask.Run đã được thêm vào .Net 4.5) chạy trên cùng một sợi với cùng chính xác CallContext. Nếu bạn muốn để làm sạch nó, bạn cần phải làm điều đó trên bối cảnh đó (và không có trong việc tiếp tục):

static Task DoSomething() 
{ 
    CallContext.LogicalSetData("hello", "world"); 

    var result = Task.Factory.StartNew(() => 
     Debug.WriteLine(new 
     { 
      Place = "Task.Run", 
      Id = Thread.CurrentThread.ManagedThreadId, 
      Msg = CallContext.LogicalGetData("hello") 
     })); 

    CallContext.FreeNamedDataSlot("hello"); 
    return result; 
} 

Bạn thậm chí có thể trừu tượng một phạm vi ra khỏi nó để đảm bảo bạn luôn dọn dẹp sau khi tự hỏi:

static Task DoSomething() 
{ 
    using (CallContextScope.Start("hello", "world")) 
    { 
     return Task.Factory.StartNew(() => 
      Debug.WriteLine(new 
      { 
       Place = "Task.Run", 
       Id = Thread.CurrentThread.ManagedThreadId, 
       Msg = CallContext.LogicalGetData("hello") 
      })); 
    } 
} 

Sử dụng:

public static class CallContextScope 
{ 
    public static IDisposable Start(string name, object data) 
    { 
     CallContext.LogicalSetData(name, data); 
     return new Cleaner(name); 
    } 

    private class Cleaner : IDisposable 
    { 
     private readonly string _name; 
     private bool _isDisposed; 

     public Cleaner(string name) 
     { 
      _name = name; 
     } 

     public void Dispose() 
     { 
      if (_isDisposed) 
      { 
       return; 
      } 

      CallContext.FreeNamedDataSlot(_name); 
      _isDisposed = true; 
     } 
    } 
} 
+0

trong phiên bản TPL của bạn, là có một nguy cơ rằng CallContext logic sẽ được trả tự do trước khi Task.Factory.StartNew đã có một cơ hội để Capture nó? Tôi cũng cần phải chắc chắn rằng tất cả các tiếp tục (nếu có) từ Task.Factory.StartNew thực sự có CallContext, ngay cả khi nó được "giải phóng" bởi chủ đề chính. –

+0

@BrentArias bạn có thể kiểm tra nó bằng Thread.Sleep (tôi đã làm). Task.Factory.StartNew cũng như Task.Run capture (copy) bối cảnh và lưu trữ nó trên Task nên bạn không cần phải lo lắng về nó. Bạn có thêm thông tin về nó tại đây: http://blogs.msdn.com/b/pfxteam/archive/2012/06/15/executioncontext-vs-synchronizationcontext.aspx – i3arnon

+0

@BrentArias * "khi bạn sử dụng Task.Run, Call to Run bắt ExecutionContext từ thread đang gọi, lưu trữ cá thể ExecutionContext đó vào đối tượng Task.Khi đại biểu được cung cấp cho Task.Run sau đó được gọi như là một phần của thực thi của Task, nó được thực hiện thông qua ExecutionContext.Run bằng cách sử dụng ngữ cảnh được lưu trữ. Điều này đúng với Task.Run, cho ThreadPool.QueueUserWorkItem, cho Delegate.BeginInvoke, cho Stream.BeginRead, cho DispatcherSynchronizationContext.Post và cho bất kỳ API async nào khác mà bạn có thể nghĩ đến. "* – i3arnon

4

Câu hỏi hay. Phiên bản await có thể không hoạt động theo cách bạn nghĩ ở đây. Hãy thêm một dòng khác khai thác gỗ bên trong DoSomething:

class Program 
{ 
    static async Task DoSomething() 
    { 
     CallContext.LogicalSetData("hello", "world"); 

     await Task.Run(() => 
      Debug.WriteLine(new 
      { 
       Place = "Task.Run", 
       Id = Thread.CurrentThread.ManagedThreadId, 
       Msg = CallContext.LogicalGetData("hello") 
      })) 
      .ContinueWith((t) => 
       CallContext.FreeNamedDataSlot("hello") 
       ); 

     Debug.WriteLine(new 
     { 
      Place = "after await", 
      Id = Thread.CurrentThread.ManagedThreadId, 
      Msg = CallContext.LogicalGetData("hello") 
     }); 
    } 

    static void Main(string[] args) 
    { 

     DoSomething().Wait(); 

     Debug.WriteLine(new 
     { 
      Place = "Main", 
      Id = Thread.CurrentThread.ManagedThreadId, 
      Msg = CallContext.LogicalGetData("hello") 
     }); 

     Console.ReadLine(); 
    } 
} 

Output:

 
{ Place = Task.Run, Id = 10, Msg = world } 
{ Place = after await, Id = 11, Msg = world } 
{ Place = Main, Id = 9, Msg = } 

Lưu ý "world" vẫn còn đó sau khi await, bởi vì nó đã có trước await. Và nó không có ở đó sau DoSomething().Wait() bởi vì nó không có ở đó trước đó, ngay từ đầu.

Điều thú vị đủ, phiên bản async của DoSomething tạo bản sao chép-trên-ghi của LogicalCallContext cho phạm vi của nó, vào ngày LogicalSetData đầu tiên. Nó làm điều đó ngay cả khi không có sự không đồng bộ bên trong nó - hãy thử await Task.FromResult(0). Tôi giả sử toàn bộ ExecutionContext được nhân bản cho phạm vi của phương pháp async, khi hoạt động ghi đầu tiên.

OTOH, cho phiên bản không async không có "logic" phạm vi và không ngoài ExecutionContext đây, vì vậy các bản sao copy-on-write của ExecutionContext trở thành hiện tại cho Main ren (nhưng continuations và Task.Run lambdas vẫn lấy bản sao của riêng họ). Vì vậy, bạn muốn hoặc cần phải di chuyển CallContext.LogicalSetData("hello", "world") bên trong Task.Run lambda, hoặc sao chép bối cảnh bằng tay:

static Task DoSomething() 
{ 
    var ec = ExecutionContext.Capture(); 
    Task task = null; 
    ExecutionContext.Run(ec, _ => 
    { 
     CallContext.LogicalSetData("hello", "world"); 

     var result = Task.Run(() => 
      Debug.WriteLine(new 
      { 
       Place = "Task.Run", 
       Id = Thread.CurrentThread.ManagedThreadId, 
       Msg = CallContext.LogicalGetData("hello") 
      })) 
      .ContinueWith((t) => 
       CallContext.FreeNamedDataSlot("hello") 
       ); 

     task = result; 
    }, null); 

    return task; 
}