800
Глава.28 .Асинхронные.операции.ввода-вывода
хронных операциях. Такая информация чрезвычайно полезна в ходе отладки, осо-
бенно если ваше приложение «виснет» из-за некорректного запроса или отсутствия
реакции сервера.
public static class TaskLogger {
public enum TaskLogLevel { None, Pending }
public static TaskLogLevel LogLevel { get; set; }
public sealed class TaskLogEntry {
public Task Task { get;
internal set; }
public String Tag { get; internal set; }
public DateTime LogTime { get; internal set; }
public String CallerMemberName { get; internal set; }
public String CallerFilePath { get; internal set; }
public Int32 CallerLineNumber { get; internal set; }
public override string ToString() {
return String.Format("LogTime={0}, Tag={1}, Member={2}, File={3}({4})",
LogTime, Tag ?? "(none)", CallerMemberName, CallerFilePath,
CallerLineNumber);
}
}
private static readonly ConcurrentDictionary
s_log =
new ConcurrentDictionary();
public static IEnumerable GetLogEntries() { return s_log.Values; }
public static Task Log(this Task task,
String tag = null,
[CallerMemberName] String callerMemberName = null,
[CallerFilePath] String callerFilePath = null,
[CallerLineNumber] Int32 callerLineNumber = 1) {
return (Task)
Log((Task)task, tag, callerMemberName, callerFilePath, callerLineNumber);
}
public static Task Log(this Task task, String tag = null,
[CallerMemberName] String callerMemberName = null,
[CallerFilePath] String callerFilePath = null,
[CallerLineNumber] Int32 callerLineNumber = 1) {
if (LogLevel == TaskLogLevel.None) return task;
var logEntry = new TaskLogEntry {
Task = task,
LogTime = DateTime.Now,
Tag = tag,
CallerMemberName = callerMemberName,
CallerFilePath = callerFilePath,
CallerLineNumber = callerLineNumber
};
s_log[task] = logEntry;
task.ContinueWith(t => { TaskLogEntry entry;
s_log.TryRemove(t, out entry); },
TaskContinuationOptions.ExecuteSynchronously);
return task;
}
}
801
Расширяемость.асинхронных.функций
Следующий фрагмент кода демонстрирует использование класса:
public static async Task Go() {
#if DEBUG
// Использование TaskLogger приводит к лишним затратам памяти
// и снижению производительности; включить для отладочной версии
TaskLogger.LogLevel = TaskLogger.TaskLogLevel.Pending;
#endif
// Запускаем 3 задачи; для тестирования TaskLogger их продолжительность
// задается явно.
var tasks =
new List {
Task.Delay(2000).Log("2s op"),
Task.Delay(5000).Log("5s op"),
Task.Delay(6000).Log("6s op")
};
try {
// Ожидание всех задач с отменой через 3 секунды; только одна задача
// должна завершиться в указанное время.
// Примечание: WithCancellation - мой метод расширения,
// описанный позднее в этой главе.
await Task.WhenAll(tasks).
WithCancellation(new CancellationTokenSource(3000).Token);
}
catch (OperationCanceledException) { }
// Запрос информации о незавершенных задачах и их сортировка
// по убыванию продолжительности ожидания
foreach (var op in TaskLogger.GetLogEntries().OrderBy(tle => tle.LogTime))
Console.WriteLine(op);
}
Построив и запустив эту программу, я получаю следующий результат:
LogTime=7/16/2012 6:44:31 AM, Tag=6s op, Member=Go,
File=C:\CLR via C#\Code\Ch28 1 IOOps.cs(332)
LogTime=7/16/2012 6:44:31 AM, Tag=5s op, Member=Go,
File=C:\CLR via C#\Code\Ch28 1 IOOps.cs(331)
Наряду с гибкостью, обусловленной использованием
Task
, асинхронные функ-
ции предоставляют еще одну точку расширения: компилятор вызывает
GetAwaiter
для операнда, использовавшегося с
await
. Таким образом, операнд вообще не
обязан быть объектом
Task
; он может относиться к любому типу, содержащему
метод
GetAwaiter
. Пример моего собственного объекта ожидания, связывающего
конечный автомат
async
-метода с инициируемым событием.
public sealed class EventAwaiter : INotifyCompletion {
private ConcurrentQueue m_events = new ConcurrentQueue();
private Action m_continuation;
#region Члены, вызываемые конечным автоматом
// Конечный автомат сначала вызывает этот метод для получения
// объекта ожидания; возвращаем текущий объект
public EventAwaiter GetAwaiter() { return this; }
продолжение