最終更新日 2021-03-21

タスクとTaskLogger

概要

一定以上複雑、もしくは計算量や待ち時間がある処理を、
秀丸エディタに組み入れていくためには、
async, await もしくは Taskクラス等による
非同期処理が多かれ少なかれ必要となってきます。

全てを同期処理で行ってしまうと、エディタの入力に「ひっかかり感」のようなものを覚えたり、
そのモジュールを実行している間、秀丸本体が何も処理出来ない、といった事態にもなってしまいます。

残りタスクが視覚的にわかるTaskLogger

そこで残りタスクが視覚的にわかる「TaskLogger」の使い方を習得しておきましょう。

下記ソース「Microsoft.VisualStudio.Threading」はnugetからダウンロードのこと

TaskLogger (出典:C# via .NET Framework 4.5)
using Microsoft.VisualStudio.Threading;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;

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={LogTime}, Tag={Tag ?? "none"}, Member={CallerMemberName}, File={CallerFilePath}({CallerLineNumber})");
        }
    }

    private static readonly ConcurrentDictionary<Task, TaskLogEntry> s_log = new ConcurrentDictionary<Task, TaskLogEntry>();
    public static IEnumerable<TaskLogEntry> GetLogEntries() { return s_log.Values; }

    public static Task<TResult> Log<TResult>(this Task<TResult> task, String tag = null,
        [CallerMemberName] String callerMemberName = null,
        [CallerFilePath] String callerFilePath = null,
        [CallerLineNumber] Int32 callerLineNumber = -1)
    {
        return (Task<TResult>)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)
    {
        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;

    }

}


public class MyClass
{
    public static async Task Go()
    {
        TaskLogger.LogLevel = TaskLogger.TaskLogLevel.Pending;

        var tasks = new List<Task>
        {
            Task.Delay(2000).Log("2s op"),
            Task.Delay(5000).Log("5s op"),
            Task.Delay(6000).Log("6s op")
        };

        try
        {
            await Task.WhenAll(tasks).WithCancellation(new CancellationTokenSource(3000).Token);
        }
        catch (OperationCanceledException) { }

        foreach (var op in TaskLogger.GetLogEntries().OrderBy(tle => tle.LogTime))
        {
            Console.WriteLine(op);
        }

    }

    static async void Test()
    {
        await Go();
    }

    static void Main()
    {
        Test();
        Console.ReadLine();
    }
}

上記ソースを実行してみれば、実行開始から「2秒後、5秒後、6秒後」に実行されるタスクが生成され、
実行から3秒後に「タスクロガー」が発動します。