فصل ۱۴: Cancellation، Progress، TAP، Task Combinators و الگوهای قدیمی Asynchrony
فروش یا انتشار این ترجمه منوط به داشتن مجوز لازم از صاحب حقوق اثر است.
الگوهای ناهمگام (Asynchronous Patterns)
لغو (Cancellation)
اغلب مهم است بتوان یک Operation همزمان را پس از Start شدن Cancel کرد؛ مثلاً در پاسخ به درخواست User. راه سادهٔ پیادهسازی این کار استفاده از Cancellation Flag است که میتوانیم با Class زیر Encapsulate کنیم:
class CancellationToken
{
public bool IsCancellationRequested { get; private set; }
public void Cancel() { IsCancellationRequested = true; }
public void ThrowIfCancellationRequested()
{
if (IsCancellationRequested)
throw new OperationCanceledException();
}
}
بعد میتوانیم یک Method ناهمگامِ قابل Cancel به این شکل بنویسیم:
async Task Foo (CancellationToken cancellationToken)
{
for (int i = 0; i < 10; i++)
{
Console.WriteLine (i);
await Task.Delay (1000);
cancellationToken.ThrowIfCancellationRequested();
}
}
هر وقت Caller بخواهد Cancel کند، روی Cancellation Tokenای که به Foo داده است Method با نام Cancel را Call میکند. این کار IsCancellationRequested را true میکند و کمی بعد باعث میشود Foo با OperationCanceledException Fault شود؛ Exception از پیش تعریفشدهای در Namespace با نام System که برای همین هدف طراحی شده است.
اگر مسئلهٔ Thread Safety را کنار بگذاریم ــ باید هنگام Read/Write کردن IsCancellationRequested Lock کنیم ــ این Pattern مؤثر است و CLR Typeای به نام CancellationToken ارائه میدهد که بسیار شبیه چیزی است که نشان دادیم. با این تفاوت که Method با نام Cancel ندارد؛ این Method روی Type دیگری به نام CancellationTokenSource قرار دارد. این جداسازی مقداری Security فراهم میکند: Methodی که فقط به Object از نوع CancellationToken دسترسی دارد میتواند Cancellation را بررسی کند، اما نمیتواند آن را آغاز کند.
برای گرفتن Cancellation Token ابتدا یک CancellationTokenSource Instantiate میکنیم:
var cancelSource = new CancellationTokenSource();
این Object Property با نام Token دارد که یک CancellationToken برمیگرداند. بنابراین میتوانیم Foo را چنین Call کنیم:
var cancelSource = new CancellationTokenSource();
Task foo = Foo (cancelSource.Token);
...
... (sometime later)
cancelSource.Cancel();
بیشتر Methodهای ناهمگام CLR از Cancellation Token پشتیبانی میکنند، از جمله Delay. اگر Foo را طوری تغییر دهیم که Token را به Delay بدهد، Task بهمحض درخواست Cancel تمام میشود؛ نه اینکه احتمالاً تا یک ثانیه بعد منتظر بماند:
async Task Foo (CancellationToken cancellationToken)
{
for (int i = 0; i < 10; i++)
{
Console.WriteLine (i);
await Task.Delay (1000, cancellationToken);
}
}
توجه کنید دیگر لازم نیست ThrowIfCancellationRequested را Call کنیم، چون Task.Delay آن کار را برای ما انجام میدهد. Cancellation Tokenها بهخوبی به سمت پایین Call Stack Propagate میشوند؛ درست همانطور که Cancellation Requestها بهواسطهٔ Exception بودن به سمت بالای Call Stack Cascade میکنند.
Methodهای همگام نیز میتوانند از Cancellation پشتیبانی کنند، مثل Wait در Task. در چنین حالتهایی Instruction لغو باید بهصورت ناهمگام، مثلاً از Task دیگری، برسد:
var cancelSource = new CancellationTokenSource();
Task.Delay (5000).ContinueWith (ant => cancelSource.Cancel());
...
در واقع هنگام Construct کردن CancellationTokenSource میتوانید Time Interval مشخص کنید تا پس از مدت تعیینشده Cancellation را آغاز کند. این برای پیادهسازی Timeout، چه همگام چه ناهمگام، مفید است:
var cancelSource = new CancellationTokenSource (5000);
try { await Foo (cancelSource.Token); }
catch (OperationCanceledException ex) { Console.WriteLine ("Cancelled"); }
Struct با نام CancellationToken Method با نام Register دارد که اجازه میدهد Callback Delegateای Register کنید تا هنگام Cancellation Fire شود؛ Objectی برمیگرداند که میتوانید برای لغو Registration آن را Dispose کنید.
Taskهایی که توسط توابع ناهمگام Compiler تولید میشوند، در صورت OperationCanceledException مدیریتنشده خودکار وارد State با نام «Canceled» میشوند؛ IsCanceled مقدار true و IsFaulted مقدار false میدهد. همین قاعده دربارهٔ Taskهایی که با Task.Run و همان CancellationToken ساخته میشوند برقرار است. تفاوت Task Faulted و Canceled در سناریوهای ناهمگام مهم نیست، چون هر دو هنگام Await شدن OperationCanceledException پرتاب میکنند.
این تفاوت در سناریوهای پیشرفتهٔ Parallel Programming ــ بهویژه Conditional Continuationها ــ اهمیت پیدا میکند. این موضوع را در «Canceling Tasks» صفحهٔ 957 ادامه میدهیم.
گزارش پیشرفت (Progress Reporting)
گاهی میخواهید Operation ناهمگام حین اجرا Progress خود را گزارش کند. راه ساده این است که Delegate از نوع Action را به Method ناهمگام بدهید تا هر زمان Progress تغییر کرد آن را Fire کند:
Task Foo (Action<int> onProgressPercentChanged)
{
return Task.Run (() =>
{
for (int i = 0; i < 1000; i++)
{
if (i % 10 == 0) onProgressPercentChanged (i / 10);
// Do something compute-bound...
}
});
}
روش Call کردن:
Action<int> progress = i => Console.WriteLine (i + " %");
await Foo (progress);
هرچند این روش در Console Application خوب کار میکند، برای Rich Client ایدئال نیست؛ چون Progress را از Worker Thread گزارش میکند و برای Consumer احتمال Thread-safety Issue به وجود میآورد. در واقع Side Effect ناشی از Concurrency را به «دنیای بیرون» نشت دادهایم، در حالی که اگر Method از UI Thread Call شود، در سایر جنبهها Isolated است.
IProgress<T> و Progress<T>
CLR برای حل این مشکل دو Type ارائه میدهد: Interface با نام IProgress<T> و Class پیادهساز آن با نام Progress<T>. هدف عملی آنها Wrap کردن Delegate است تا UI Applicationها بتوانند از طریق Synchronization Context با امنیت Progress گزارش کنند.
Interface فقط یک Method تعریف میکند:
public interface IProgress<in T>
{
void Report (T value);
}
استفاده از IProgress<T> ساده است و Method ما تقریباً تغییر نمیکند:
Task Foo (IProgress<int> onProgressPercentChanged)
{
return Task.Run (() =>
{
for (int i = 0; i < 1000; i++)
{
if (i % 10 == 0) onProgressPercentChanged.Report (i / 10);
// Do something compute-bound...
}
});
}
Class با نام Progress<T> Constructorی دارد که Delegate از نوع Action<T> را میپذیرد و Wrap میکند:
var progress = new Progress<int> (i => Console.WriteLine (i + " %"));
await Foo (progress);
Progress<T> Eventای به نام ProgressChanged نیز دارد که میتوانید بهجای یا علاوه بر دادن Action Delegate به Constructor، به آن Subscribe کنید. هنگام Instantiate شدن Progress<int>، Class در صورت وجود Synchronization Context را Capture میکند. وقتی Foo بعداً Report را Call میکند، Delegate از طریق همان Context Invoke میشود.
Methodهای ناهمگام میتوانند با جایگزینکردن int با Type سفارشی دارای مجموعهای از Propertyها، Progress Reporting پیچیدهتری پیادهسازی کنند.
Methodهای ناهمگام WinRT نیز Progress Reporting دارند، هرچند Protocol بهدلیل Type System نسبتاً ابتدایی COM پیچیدهتر است. بهجای پذیرفتن Object از نوع IProgress<T>، Methodهای WinRT که Progress گزارش میکنند یکی از Interfaceهای زیر را بهجای IAsyncAction و IAsyncOperation<TResult> برمیگردانند:
IAsyncActionWithProgress<TProgress>
IAsyncOperationWithProgress<TResult, TProgress>
جالب اینکه هر دو بر پایهٔ IAsyncInfo هستند، نه IAsyncAction و IAsyncOperation<TResult>.
خبر خوب این است که Extension Method با نام AsTask برای Interfaceهای یادشده Overloadی دارد که IProgress<T> میپذیرد؛ بنابراین بهعنوان Consumer در .NET میتوانید Interfaceهای COM را نادیده بگیرید و چنین بنویسید:
var progress = new Progress<int> (i => Console.WriteLine (i + " %"));
CancellationToken cancelToken = ...
var task = someWinRTobject.FooAsync().AsTask (cancelToken, progress);
الگوی ناهمگام مبتنی بر Task (Task-Based Asynchronous Pattern)
.NET صدها Method ناهمگامِ Task-returning ارائه میدهد که میتوانید Await کنید؛ بیشتر آنها به I/O مربوطاند. بیشتر این Methodها دستکم تا حدی از Patternای به نام Task-Based Asynchronous Pattern یا TAP پیروی میکنند که Formalization معقولی از چیزهایی است که تا اینجا توضیح دادیم. یک Method از نوع TAP:
- یک
Task یا Task<TResult> «Hot» و در حال اجرا برمیگرداند. - پسوند
Async دارد؛ بهجز موارد خاصی مانند Task Combinatorها. - اگر Cancellation یا Progress Reporting را پشتیبانی کند، Overloadی دارد که Cancellation Token و/یا
IProgress<T> میپذیرد. - سریع به Caller برمیگردد و فقط Phase همگام اولیهٔ کوچکی دارد.
- اگر I/O-bound باشد Thread را اشغال نمیکند.
همانطور که دیدیم، TAP Methodها با توابع ناهمگام C# بهسادگی نوشته میشوند.
Task Combinatorها
نتیجهٔ خوبِ داشتن Protocol یکنواخت برای توابع ناهمگام ــ اینکه همیشه Task برمیگردانند ــ این است که میتوان Task Combinatorها را استفاده و حتی نوشت: Functionهایی که بدون توجه به اینکه Taskهای خاص چه کاری انجام میدهند، آنها را بهشکلی مفید ترکیب میکنند.
CLR دو Task Combinator دارد: Task.WhenAny و Task.WhenAll. برای توضیح آنها فرض میکنیم Methodهای زیر تعریف شدهاند:
async Task<int> Delay1() { await Task.Delay (1000); return 1; }
async Task<int> Delay2() { await Task.Delay (2000); return 2; }
async Task<int> Delay3() { await Task.Delay (3000); return 3; }
WhenAny
Task.WhenAny Taskی برمیگرداند که وقتی هرکدام از مجموعه Taskها Complete شود، Complete میشود. مثال زیر در یک ثانیه Complete میشود:
Task<int> winningTask = await Task.WhenAny (Delay1(), Delay2(), Delay3());
Console.WriteLine ("Done");
Console.WriteLine (winningTask.Result); // 1
چون خود Task.WhenAny Task برمیگرداند، آن را Await میکنیم و Taskی را میگیریم که اول Finish شده است. مثال کاملاً Nonblocking است، حتی Line آخر که Property با نام Result را میخوانیم، چون winningTask از قبل Finish شده است.
با این حال معمولاً بهتر است خود Winning Task را Await کنیم:
Console.WriteLine (await winningTask); // 1
زیرا Exceptionها بدون Wrap شدن در AggregateException دوباره پرتاب میشوند. حتی میتوانیم هر دو Await را در یک Step انجام دهیم:
int answer = await await Task.WhenAny (Delay1(), Delay2(), Delay3());
اگر Taskی که برنده نشده بعداً Fault شود، Exception آن Unobserved میماند مگر اینکه بعداً آن Task را Await یا Property با نام Exception آن را Query کنید.
WhenAny برای اعمال Timeout یا Cancellation روی Operationهایی که ذاتاً پشتیبانی نمیکنند مفید است:
Task<string> task = SomeAsyncFunc();
Task winner = await (Task.WhenAny (task, Task.Delay(5000)));
if (winner != task) throw new TimeoutException();
string result = await task; // Unwrap result/re-throw
توجه کنید چون در اینجا WhenAny را با Taskهای دارای Type متفاوت Call کردهایم، Winner بهشکل Task ساده گزارش میشود، نه Task<string>.
WhenAll
Task.WhenAll Taskی برمیگرداند که وقتی همهٔ Taskهای دادهشده Complete شدند Complete میشود. مثال زیر پس از سه ثانیه Complete میشود و Pattern از نوع Fork/Join را نشان میدهد:
await Task.WhenAll (Delay1(), Delay2(), Delay3());
میتوانستیم بهجای WhenAll، task1، task2 و task3 را بهترتیب Await کنیم و Result مشابهی بگیریم:
Task task1 = Delay1(), task2 = Delay2(), task3 = Delay3();
await task1; await task2; await task3;
تفاوت ــ جدا از Inefficiency بیشتر بهدلیل سه Await بهجای یکی ــ این است که اگر task1 Fault شود دیگر به Await کردن task2/task3 نمیرسیم و Exceptionهای آنها Unobserved میماند.
در مقابل، Task.WhenAll تا Complete شدن همهٔ Taskها Complete نمیشود، حتی اگر Fault وجود داشته باشد. اگر چند Fault باشد، Exceptionهای آنها در AggregateException مربوط به Task ترکیب میشوند. این همان جایی است که AggregateException واقعاً مفید میشود، اگر به همهٔ Exceptionها علاقه داشته باشید. با این حال Await کردن Task ترکیبی فقط نخستین Exception را پرتاب میکند؛ پس برای دیدن همه باید چنین عمل کنید:
Task task1 = Task.Run (() => { throw null; } );
Task task2 = Task.Run (() => { throw null; } );
Task all = Task.WhenAll (task1, task2);
try { await all; }
catch
{
Console.WriteLine (all.Exception.InnerExceptions.Count); // 2
}
فراخوانی WhenAll با Taskهای Task<TResult> یک Task<TResult[]> برمیگرداند که Resultهای ترکیبی همهٔ Taskها را میدهد؛ با Await کردن به TResult[] تبدیل میشود.
Task<int> task1 = Task.Run (() => 1);
Task<int> task2 = Task.Run (() => 2);
int[] results = await Task.WhenAll (task1, task2); // { 1, 2 }
بهعنوان مثال عملی، کد زیر URIها را Parallel دانلود میکند و Total Length آنها را جمع میزند:
async Task<int> GetTotalSize (string[] uris)
{
IEnumerable<Task<byte[]>> downloadTasks = uris.Select (uri =>
new WebClient().DownloadDataTaskAsync (uri));
byte[][] contents = await Task.WhenAll (downloadTasks);
return contents.Sum (c => c.Length);
}
اینجا Inefficiency کوچکی وجود دارد: Byte Arrayهای دانلودشده را بیدلیل تا Complete شدن همهٔ Taskها نگه میداریم. بهتر است Byte Arrayها را بلافاصله پس از دانلود به Lengthشان Collapse کنیم. در اینجا Lambda ناهمگام مفید است، چون باید Expression از نوع await را داخل Query Operator با نام Select در LINQ بدهیم:
async Task<int> GetTotalSize (string[] uris)
{
IEnumerable<Task<int>> downloadTasks = uris.Select (async uri =>
(await new WebClient().DownloadDataTaskAsync (uri)).Length);
int[] contentLengths = await Task.WhenAll (downloadTasks);
return contentLengths.Sum();
}
Combinatorهای سفارشی
نوشتن Task Combinatorهای خودتان میتواند مفید باشد. سادهترین «Combinator» یک Task میپذیرد؛ مانند مثال زیر که اجازه میدهد هر Task را با Timeout Await کنید:
async static Task<TResult> WithTimeout<TResult> (this Task<TResult> task,
TimeSpan timeout)
{
Task winner = await Task.WhenAny (task, Task.Delay (timeout))
.ConfigureAwait (false);
if (winner != task) throw new TimeoutException();
return await task.ConfigureAwait (false); // Unwrap result/re-throw
}
چون این یک «Library Method» واقعی است که به Shared State خارجی دسترسی ندارد، هنگام Await کردن از ConfigureAwait(false) استفاده میکنیم تا از Bounce احتمالی به UI Synchronization Context جلوگیری شود. میتوانیم با Cancel کردن Task.Delay وقتی Task بهموقع Complete میشود Efficiency را بیشتر کنیم؛ این کار سربار کوچک Timer باقیمانده را حذف میکند:
async static Task<TResult> WithTimeout<TResult> (this Task<TResult> task,
TimeSpan timeout)
{
var cancelSource = new CancellationTokenSource();
var delay = Task.Delay (timeout, cancelSource.Token);
Task winner = await Task.WhenAny (task, delay).ConfigureAwait (false);
if (winner == task)
cancelSource.Cancel();
else
throw new TimeoutException();
return await task.ConfigureAwait (false); // Unwrap result/re-throw
}
Method زیر اجازه میدهد Task را از طریق CancellationToken «Abandon» کنید:
static Task<TResult> WithCancellation<TResult> (this Task<TResult> task,
CancellationToken cancelToken)
{
var tcs = new TaskCompletionSource<TResult>();
var reg = cancelToken.Register (() => tcs.TrySetCanceled ());
task.ContinueWith (ant =>
{
reg.Dispose();
if (ant.IsCanceled)
tcs.TrySetCanceled();
else if (ant.IsFaulted)
tcs.TrySetException (ant.Exception.InnerExceptions);
else
tcs.TrySetResult (ant.Result);
});
return tcs.Task;
}
نوشتن Task Combinatorها میتواند پیچیده باشد و گاهی Signaling Constructهایی لازم دارد که در فصل 21 بررسی میشوند. این در واقع نکتهٔ خوبی است، چون Complexity مربوط به Concurrency را از Business Logic بیرون میکشد و داخل Methodهای Reusable قرار میدهد که میتوان آنها را در Isolation Test کرد.
Combinator بعدی مانند WhenAll عمل میکند، جز اینکه اگر هر Task Fault شود، Task حاصل بلافاصله Fault میشود:
async Task<TResult[]> WhenAllOrError<TResult>
(params Task<TResult>[] tasks)
{
var killJoy = new TaskCompletionSource<TResult[]>();
foreach (var task in tasks)
task.ContinueWith (ant =>
{
if (ant.IsCanceled)
killJoy.TrySetCanceled();
else if (ant.IsFaulted)
killJoy.TrySetException (ant.Exception.InnerExceptions);
});
return await await Task.WhenAny (killJoy.Task, Task.WhenAll (tasks))
.ConfigureAwait (false);
}
ابتدا یک TaskCompletionSource میسازیم که تنها وظیفهاش این است که اگر Taskی Fault شد «مهمانی را تمام کند». بنابراین هرگز Method با نام SetResult آن را Call نمیکنیم و فقط TrySetCanceled و TrySetException را Call میکنیم. در اینجا ContinueWith از GetAwaiter().OnCompleted مناسبتر است، چون به Result Taskها دسترسی نداریم و نمیخواهیم در آن نقطه به UI Thread Bounce کنیم.
Locking ناهمگام
در «Asynchronous semaphores and locks» صفحهٔ 906 توضیح میدهیم چگونه از SemaphoreSlim برای Lock کردن یا محدودکردن Concurrency بهشکل ناهمگام استفاده کنید.
Patternهای منسوخ (Obsolete Patterns)
.NET Patternهای دیگری برای Asynchrony دارد که پیش از Taskها و توابع ناهمگام به وجود آمدهاند. اکنون که Asynchrony مبتنی بر Task Pattern غالب شده است، بهندرت به آنها نیاز میشود.
Asynchronous Programming Model
قدیمیترین Pattern، Asynchronous Programming Model یا APM نام دارد و از یک جفت Method که با «Begin» و «End» شروع میشوند و Interface با نام IAsyncResult استفاده میکند. برای مثال Class با نام Stream در System.IO و Method با نام Read آن را ببینیم. ابتدا نسخهٔ همگام:
public int Read (byte[] buffer, int offset, int size);
احتمالاً میتوانید شکل نسخهٔ ناهمگام مبتنی بر Task را حدس بزنید:
public Task<int> ReadAsync (byte[] buffer, int offset, int size);
اکنون نسخهٔ APM:
public IAsyncResult BeginRead (byte[] buffer, int offset, int size,
AsyncCallback callback, object state);
public int EndRead (IAsyncResult asyncResult);
فراخوانی Method از نوع Begin* Operation را آغاز میکند و Object از نوع IAsyncResult برمیگرداند که Token آن Operation ناهمگام است. وقتی Operation Complete یا Fault شد، Delegate با نام AsyncCallback Fire میشود:
public delegate void AsyncCallback (IAsyncResult ar);
کسی که این Delegate را Handle میکند سپس Method از نوع End* را Call میکند؛ Methodی که Return Value Operation را میدهد و اگر Operation Fault شده باشد Exception را نیز دوباره پرتاب میکند.
APM نهتنها استفادهٔ دستوپاگیری دارد، بلکه پیادهسازی صحیح آن نیز بهطرز شگفتآوری دشوار است. سادهترین روش برای کار با Methodهای APM استفاده از Adapter با نام Task.Factory.FromAsync است که جفت Methodهای APM را به Task تبدیل میکند. در داخل از TaskCompletionSource استفاده میکند تا Taskی بدهد که هنگام Complete یا Fault شدن Operation از نوع APM Signal شود.
Method با نام FromAsync پارامترهای زیر را نیاز دارد:
- Delegateای که Method از نوع
BeginXXX را مشخص کند. - Delegateای که Method از نوع
EndXXX را مشخص کند. - Argumentهای اضافی که به این Methodها داده میشوند.
FromAsync برای پذیرش Delegate Typeها و Argumentهایی که تقریباً با همهٔ Signatureهای Method ناهمگام در .NET Match میشوند Overload شده است. مثلاً اگر stream از نوع Stream و buffer از نوع byte[] باشد:
Task<int> readChunk = Task<int>.Factory.FromAsync (
stream.BeginRead, stream.EndRead, buffer, 0, 1000, null);
Event-Based Asynchronous Pattern
Event-Based Asynchronous Pattern یا EAP در سال 2005 برای ارائهٔ Alternative سادهتر از APM، بهویژه در سناریوهای UI، معرفی شد. با این حال فقط در تعداد کمی Type پیادهسازی شد که مهمترین آنها WebClient در System.Net است. EAP فقط یک Pattern است و Type کمکی خاصی ارائه نمیشود. در اصل Class خانوادهای از Memberها عرضه میکند که Concurrency را در داخل مدیریت میکنند، شبیه نمونهٔ زیر:
// These members are from the WebClient class:
public byte[] DownloadData (Uri address); // Synchronous version
public void DownloadDataAsync (Uri address);
public void DownloadDataAsync (Uri address, object userToken);
public event DownloadDataCompletedEventHandler DownloadDataCompleted;
public void CancelAsync (object userState); // Cancels an operation
public bool IsBusy { get; } // Indicates if still running
Methodهای *Async Operation را بهصورت ناهمگام آغاز میکنند. وقتی Operation Complete شود، Event از نوع *Completed Fire میشود و اگر Synchronization Context Capture شده باشد خودکار به آن Post میشود. این Event Object مربوط به Event Arguments را برمیگرداند که شامل موارد زیر است:
- Flagی که نشان میدهد Operation با فراخوانی
CancelAsync توسط Consumer لغو شده یا نه. - Object از نوع
Error که Exception پرتابشده را ــ اگر وجود داشته باشد ــ مشخص میکند. - Object با نام
userToken، اگر هنگام Call کردن Async Method داده شده باشد.
Typeهای EAP میتوانند Event گزارش Progress هم ارائه کنند که با تغییر Progress Fire میشود و آن نیز از طریق Synchronization Context Post میشود:
public event DownloadProgressChangedEventHandler DownloadProgressChanged;
پیادهسازی EAP مقدار زیادی Boilerplate Code نیاز دارد و به همین دلیل Pattern از نظر Composition ضعیف است.
BackgroundWorker
BackgroundWorker در System.ComponentModel یک Implementation عمومی از EAP است. به Rich Client Applicationها اجازه میدهد Worker Thread را Start کنند و بدون نیاز به Capture صریح Synchronization Context، Completion و Progress درصدی را گزارش کنند. مثال:
var worker = new BackgroundWorker { WorkerSupportsCancellation = true };
worker.DoWork += (sender, args) =>
{ // This runs on a worker thread
if (args.Cancel) return;
Thread.Sleep(1000);
args.Result = 123;
};
worker.RunWorkerCompleted += (sender, args) =>
{ // Runs on UI thread
// We can safely update UI controls here...
if (args.Cancelled)
Console.WriteLine ("Cancelled");
else if (args.Error != null)
Console.WriteLine ("Error: " + args.Error.Message);
else
Console.WriteLine ("Result is: " + args.Result);
};
worker.RunWorkerAsync(); // Captures sync context and starts operation
RunWorkerAsync Operation را Start میکند و Event با نام DoWork را روی Pooled Worker Thread Fire میکند. همچنین Synchronization Context را Capture میکند و هنگامی که Operation Complete یا Fault شود، Event با نام RunWorkerCompleted از طریق همان Synchronization Context Invoke میشود؛ مانند Continuation.
BackgroundWorker Coarse-grained Concurrency ایجاد میکند، چون Event با نام DoWork بهطور کامل روی Worker Thread اجرا میشود. اگر در آن Event Handler لازم باشد UI Controlها را Update کنید ــ بهجز Post کردن پیام درصد پیشرفت ــ باید از Dispatcher.BeginInvoke یا مکانیزم مشابه استفاده کنید.
کتاب برای جزئیات بیشتر دربارهٔ BackgroundWorker به آدرس albahari.com/threading ارجاع میدهد.
صفحهٔ 692 در فایل منبع صفحهٔ جداکنندهٔ پایان فصل است و متن آموزشی دیگری ندارد.