21 任务和并行编程

任务和并行编程

任务。任务允许建立任务之间的关系,例如,第一个任务完成时,应该继续下一个任务。也可以建立一个层次结构,其中包含多个任务。
除了使用任务之外,还可以使用Parallel类实现并行活动。需要区分数据并行(在不同的任务之间同时处理一些数据)和任务并行性(同时执行不同的功能)。

在创建并行程序时,有很多不同的选择。应该使用适合场景的最简单选项。本章首先介绍Parallel类,它提供了非常简单的并行性。如果这就是需要的类,使用这个类即可。如果需要更多的控制,比如需要管理任务之间的关系,或定义返回任务的方法,就要使用Task类。
本章还包括数据流库,如果需要基于操作的编程通过管道传送数据,这可能是最简单的一个
库了。
如果需要更多地控制并行性,如设置优先级,就需要使用Thread类。

Parallel类

命名空间:System.Threading.Tasks
提供了数据任务并行性

Parallel类定义了并行的for和foreach的静态方法。对于C#的for和foreach语句而言,循环从
一个线程中运行。Parallel类使用多个任务,因此使用多个线程来完成这个作业。

Parallel.For()Parallel.ForEach()方法在每次迭代中调用相同的代码
Parallel.lnvoke()方法允许同时调用不同的方法。
Parallel.lnvoke用于任务并行性
Parallel.ForEach用于数据并行性。

使用Parallel.For()方法循环

Parallel.For()方法类似于C#的循环语句,也是多次执行一个任务。使用Parallel.For()方法,可以并行运行迭代。迭代的顺序没有定义。

使用Task.Delay(10).Wait()控制

/// <summary>
/// 线程和任务标识符
/// </summary>
/// <param name="prefix"></param>
/// <returns></returns>
public void Log(string prefix)
{
    Console.WriteLine($"{prefix} task: {Task.CurrentId}, thread: {Thread.CurrentThread.ManagedThreadId}");
}
/// <summary>
/// Parallel.For
/// </summary>
/// <returns></returns>
public void ParallelFor()
{
    ParallelLoopResult res =
        Parallel.For(0, 10, i =>
        {
            Log($"S {i}");
            Task.Delay(10).Wait();
            Log($"E {i}");
        });
    Console.WriteLine($"Is Completed:{res.IsCompleted}");
}

输出结果

S 0 task: 210, thread: 9
S 2 task: 211, thread: 10
E 2 task: 211, thread: 10
S 3 task: 211, thread: 10
S 4 task: 212, thread: 11
S 6 task: 215, thread: 17
S 8 task: 218, thread: 15
E 8 task: 218, thread: 15
S 9 task: 222, thread: 15
E 4 task: 212, thread: 11
S 7 task: 224, thread: 17
E 3 task: 211, thread: 10
S 5 task: 225, thread: 11
S 1 task: 219, thread: 18
E 0 task: 210, thread: 9
E 7 task: 224, thread: 17
E 1 task: 219, thread: 18
E 5 task: 225, thread: 11
E 9 task: 222, thread: 15
Is Completed:True

删除Task.Delay(10).Wait()

并行体内的延迟等待10毫秒,会有更好的机会来创建新线程。如果删除Task.Delay(10).Wait(),就会使用更少的线程和任务。
在结果中还可以看到,循环的每个end-log使用与start-log相同的线程和任务。使用Task.Delay()和wait()方法会阻塞当前线程,直到延迟结束。

S 0 task: 289, thread: 10
E 0 task: 289, thread: 10
S 1 task: 289, thread: 10
E 1 task: 289, thread: 10
S 4 task: 291, thread: 11
E 4 task: 291, thread: 11
S 5 task: 291, thread: 11
E 5 task: 291, thread: 11
S 6 task: 291, thread: 11
E 6 task: 291, thread: 11
S 7 task: 291, thread: 11
E 7 task: 291, thread: 11
S 8 task: 291, thread: 11
E 8 task: 291, thread: 11
S 9 task: 291, thread: 11
E 9 task: 291, thread: 11
S 3 task: 291, thread: 11
E 3 task: 291, thread: 11
S 2 task: 290, thread: 6
E 2 task: 290, thread: 6
Is Completed:True

使用await关键字和Task.Delay()

在输出中可以看到,调用Thread.Delay()方法后,线程发生了变化。
例如,循环迭代8在延迟前的线程ID为8,在延迟后的线程ID为6。
在输出中还可以看到,任务不再存在,只有线程留下了,而且这里重用了前面的线程。
另外一个重要的方面是,Parallel类的For()方法并没有等待延迟,而是直接完成。
Parallel类只等待它创建的任务,而不等待其他后台活动。
在延迟后,也有可能完全看不到方法的输出,出现这种情况的原因是主线程(是一个前台线程)结束,所有的后台线程被终止。

public string ParallelForWithAsync()
{
    StringBuilder sb = new StringBuilder();
    ParallelLoopResult res =
        Parallel.For(0, 10, async i =>
        {
            Log($"S {i}");
            await Task.Delay(10);
            Log($"E {i}");
        });
    Console.WriteLine($"Is Completed:{res.IsCompleted}");
    return sb.ToString();
}

输出结果

S 0 task: 1957, thread: 1
S 2 task: 1958, thread: 6
S 1 task: 1957, thread: 1
S 6 task: 1960, thread: 3
S 4 task: 1959, thread: 7
S 5 task: 1957, thread: 1
S 7 task: 1960, thread: 3
S 9 task: 1959, thread: 7
S 3 task: 1958, thread: 6
S 8 task: 1961, thread: 8
Is Completed:True
E 3 task: , thread: 3
E 8 task: , thread: 6
E 6 task: , thread: 6
E 5 task: , thread: 1
E 2 task: , thread: 6
E 1 task: , thread: 1
E 7 task: , thread: 7
E 0 task: , thread: 1
E 4 task: , thread: 3
E 9 task: , thread: 8

提前停止Parallel.For

Parallel.For(int fromInclusive, int toExclusive, Action<int, ParallelLoopState> body)

使用这些参数定义一个方法,就可以调用ParallelLoopState的Break()或Stop()方法,以影响循环的结果。如下。

public void StopParallelForEarly()
{
    ParallelLoopResult result = Parallel.For(10, 40, (int i, ParallelLoopState pls) =>
    {
        Log($"S {i}");
        if (i > 12)
        {
            pls.Break();
            Log($"break now... {i}");
        }
        Task.Delay(10).Wait();
        Log($"E {i}");

    });

    Console.WriteLine($"Is Completed:{result.IsCompleted}");
    Console.WriteLine($"lowest break iteration:{result.LowestBreakIteration}");
}

应用程序的这次运行说明,迭代在值大于12时中断,但其他任务可以同时运行,有其他值的任务也可以运行。在中断前开始的所有任务都可以继续运行,直到结束。利用LowestBreakIteration属性,可以忽略其他你不需要的任务的结果。

S 10 task: 539, thread: 1
S 24 task: 541, thread: 7
S 17 task: 540, thread: 6
S 31 task: 542, thread: 3
break now... 24 task: 541, thread: 7
break now... 31 task: 542, thread: 3
break now... 17 task: 540, thread: 6
S 38 task: 543, thread: 8
break now... 38 task: 543, thread: 8
E 38 task: 543, thread: 8
E 31 task: 542, thread: 3
E 17 task: 540, thread: 6
E 24 task: 541, thread: 7
E 10 task: 539, thread: 1
S 11 task: 539, thread: 1
E 11 task: 539, thread: 1
S 12 task: 539, thread: 1
E 12 task: 539, thread: 1
S 13 task: 539, thread: 1
break now... 13 task: 539, thread: 1
E 13 task: 539, thread: 1
Is Completed:False
lowest break iteration:13

Parallel.For()的初始化

public static ParallelLoopResult For<TLocal>(long fromInclusive, long toExclusive, Func<TLocal> localInit, Func<long, ParallelLoopState, TLocal, TLocal> body, Action<TLocal> localFinally);

通过这个功能,这个方法完美地累加了大量数据集合的结果。如下:

public void ParallelForWithInit() {
    ParallelLoopResult result = Parallel.For<string>(0, 10, () =>
      {
          //每个线程调用一次
          Log($"init thread");
          return $"{Thread.CurrentThread.ManagedThreadId}";
      },
      (i, pls, strl) => 
      {
          //每个成员都会调用
          Log($"body i {i} strl {strl}");
          Task.Delay(10).Wait();
          return $"i {i}";
      },
      (strl) => 
      {
          //每个线程最后执行的操作
          Log($"finally {strl}");
      } );
}

结果

init thread task: 527, thread: 6
init thread task: 528, thread: 3
init thread task: 526, thread: 1
init thread task: 530, thread: 8
body i 2 strl 6 task: 527, thread: 6
body i 0 strl 1 task: 526, thread: 1
body i 8 strl 8 task: 530, thread: 8
init thread task: 529, thread: 7
body i 6 strl 7 task: 529, thread: 7
body i 4 strl 3 task: 528, thread: 3
finally i 4 task: 528, thread: 3
init thread task: 538, thread: 3
body i 5 strl 3 task: 538, thread: 3
init thread task: 531, thread: 10
body i 3 strl 10 task: 531, thread: 10
finally i 6 task: 529, thread: 7
body i 1 strl i 0 task: 526, thread: 1
init thread task: 541, thread: 7
body i 7 strl 7 task: 541, thread: 7
finally i 8 task: 530, thread: 8
finally i 2 task: 527, thread: 6
init thread task: 544, thread: 8
body i 9 strl 8 task: 544, thread: 8
finally i 5 task: 538, thread: 3
finally i 7 task: 541, thread: 7
finally i 9 task: 544, thread: 8
finally i 1 task: 526, thread: 1
finally i 3 task: 531, thread: 10

Parallel.ForEach()方法循环

Parallel.ForEach()方法遍历实现了IEnumerable的集合,其方式类似于foreach语句,但以异步方式遍历。这里也没有确定遍历顺序

public void ParallForEach()
{
    string[] data = { "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12" };

    ParallelLoopResult result = Parallel.ForEach<string>(data, s =>
    {
        Console.WriteLine(s);
    });
}

结果

0
3
9
12
7
8
1
2
4
5
10
11
6

如果需要中断循环,就可以使用ForEach()方法的重载版本和ParallelLoopState参数。其方式与前
面的For方法相同。ForEach()方法的一个重版本也可以用于访问索引器,从而获得迭代次数,如下
所示:

Parallel.ForEach<string>(data, (s, pls, l) => {
    Console.WriteLine($"{s} {l}");
});

通过Parallel.lnvoke()方法调用多个方法

如果多个任务将并行运行,就可以使用Parallel.lnvoke()方法,它提供了任务并行性模式。Parallel.lnvoke()方法允许传递一个Action委托的数组,在其中可以指定将运行的方法。

public void ParallelInvoke()
{
    Parallel.Invoke(Foo, Bar);
}

private void Foo() {
    Console.WriteLine("Foo");
}
private void Bar()
{
    Console.WriteLine("Bar");
}

结果

Foo
Bar

Parallel类使用起来十分方便,而且既可以用于任务,又可以用于数据并行性。如果需要更细致的控制,并且不想等到Parallel类结束后再开始动作,就可以使用Task类。当然,结合使用Task类和Parallel类也是可以的。

任务

命名空间:System.Threadmg.Tasks
任务表示将完成的某个工作单元。这个工作单元可以在单独的线程中运行,也可以以同步方式启动一个任务,这需要等待主调线程。使用任务不仅可以获得一个抽象层,还可以对底层线程进行很多控制。

在安排需要完成的工作时,任务提供了非常大的灵活性。例如,可以定义连续的工作一一一在一个任务完成后该执行什么工作。这可以根据任务成功与否来区分。另外,还可以在层次结构中安排任务。例如,父任务可以创建新的子任务。这可以创建一种依赖关系,这样,取消父任务,也会取消其子任务。

启动任务

要启动任务,可以使用TaskFactory类或Task类的构造函数和Start()方法。Task类的构造函数在创建任务上提供的灵活性较大。

使用线程池的任务

现在,只需要知道线程池独自管理线程,根据需要增加或减少线程池中的线程数。线程池中的线程用于实现一些操作,之后仍然返回线程池中。

创建任务的方式
使用实例化的TaskFactory类,在其中把TaskMethod方法传递给StartNew方法,就会立即启动任务。

第二种方式是使用Task类的静态属性Factory来访问TaskFactory,以及调用StartNew()方法。它与第一种方式很类似,也使用了工厂,但是对工厂创建的控制则没有那么全面。

第三种方式是使用Task类的构造函数。实例化Task对象时,任务不会立即运行,而是指定Created状态。接着调用Task类的Start()方法,来启动任务。

第四种方式调用Task类的Run方法,立即启动任务。Run方法没有可以传递Action<object>委托的重载版本,但是通过传递Action类型的lambda表达式并在其实现中使用参数,可以模拟这种行为。

public class TaskSamplesClass
{
    public void TaskMethod(object o)
    {
        Log(o?.ToString());
    }
    private object s_logLock = new object();
    public void Log(string title)
    {
        lock (s_logLock)
        {
            Console.WriteLine(title);
            Console.WriteLine($"Task id:{Task.CurrentId?.ToString() ?? "no task"}," + $"thread:{Thread.CurrentThread.ManagedThreadId}");
            #if (!DNXCORE)
            Console.WriteLine($"is pooled thread:{Thread.CurrentThread.IsThreadPoolThread}");
            #endif
            Console.WriteLine($"is background thread:{Thread.CurrentThread.IsBackground}");
            Console.WriteLine();
        }
    }

    public void TasksUsingThreadPool()
    {
        var tf = new TaskFactory();
        Task t1 = tf.StartNew(TaskMethod, "using a task factory");
        Task t2 = Task.Factory.StartNew(TaskMethod, "factory via a task");
        var t3 = new Task(TaskMethod, "using a task constructor and Start");
        t3.Start();
        Task t4 = Task.Run(() => TaskMethod("using the Run menthod"));
    }
}
using a task factory
Task id:415,thread:12
is pooled thread:True
is background thread:True

using a task constructor and Start
Task id:417,thread:15
is pooled thread:True
is background thread:True

factory via a task
Task id:416,thread:13
is pooled thread:True
is background thread:True

using the Run menthod
Task id:418,thread:16
is pooled thread:True
is background thread:True

使用Task构造函数和TaskFactory的StartNew方法时,可以传递TaskCreabonOptions枚举中的值。利用这个创建选项,可以改变任务的行为。

同步任务

任务不一定要使用线程池中的线程,也可以使用其他线程。任务也可以同步运行,以相同的线程作为主调线程。

public void RunSynchronousTask()
{
    TaskMethod("Just the main thread");
    var t1 = new Task(TaskMethod, "run sync");
    t1.RunSynchronously();
}

这里,TaskMethod()方法首先在主线程上直接调用,然后在新创建的Task上调用。从如下所示的控制台输出可以看到,主线程没有任务ID,也不是线程池中的线程。调用RunSynchronously()方法时,会使用相同的线程作为主调线程,但是如果以前没有创建任务,就会创建一个任务:

Just the main thread
Task id:no task,thread:9
is pooled thread:False
is background thread:False

run sync
Task id:467,thread:9
is pooled thread:False
is background thread:False

使用单独线程的任务

如果任务的代码将长时间运行,就应该使用TaskCreation0ptions.LongRunning告诉任务调度器创建一个新线程,而不是使用线程池中的线程。此时,线程可以不由线程池管理。当线程来自线程池时,任务调度器可以决定等待己经运行的任务完成,然后使用这个线程,而不是在线程池中创建一个新线程。对于长时间运行的线程,任务调度器会立即知道等待它们完成没有意义。

public void LongRunningTask() {
    var t1 = new Task(TaskMethod, "long running", TaskCreationOptions.LongRunning);
    t1.Start();
}
long running
Task id:356,thread:15
is pooled thread:False
is background thread:True

Future——任务的结果

当任务结束时,它可以把一些有用的状态信息写到共享对象中。这个共享对象必须是线程安全的。另一个选页是使用返回某个结果的任务。这种任务也称为future,因为它在将来返回一个结果。

早期版本的Task Parallel Library(TPL)的类名也称为Future,现在它是Task类的一个泛型版本。使用这个类时,可以定义任务返回的结果的类型。

由任务调用来返回结果的方法可以声明为任何返回类型。

public void TaskWithResultDemo()
{
    var t1 = new Task<Tuple<int, int>>(TaskWithResult, Tuple.Create(8, 3));
    t1.Start();
    Console.WriteLine(t1.Result);
    t1.Wait();
    Console.WriteLine($"result from task:{t1.Result.Item1}{t1.Result.Item2}");
}

private Tuple<int, int> TaskWithResult(object division)
{
    Tuple<int, int> div = (Tuple<int, int>)division;
    int result = div.Item1 / div.Item2;
    int remider = div.Item1 % div.Item2;
    Console.WriteLine("task creates  a result...");

    return Tuple.Create(result, remider);
}

连续的任务

通过任务,可以指定在任务完成后,运行另一个特定的任务。
连续任务写法:

Task t1 = new Task(DoOnFirst);
Task t2 = t1.ContinueWith(DoOnSecond);
var tf = new TaskFactory(TaskCreationOptions, TaskContinuationOptions);

无论前一个任务是如何结束的,前面的连续任务总是在前一个任务结束时启动。使用TaskContinuationOptions枚举中的值可以指定,连续任务只有在起始任务成功(或失败)结束时启动。

示例

public void ContinuationTasks()
{
    Task t1 = new Task(DoOnFirst);
    Task t2 = t1.ContinueWith(DoOnSecond);
    Task t3 = t1.ContinueWith(DoOnSecond);
    Task t4 = t2.ContinueWith(DoOnSecond);
    t1.Start();
}


private void DoOnFirst()
{
    Console.WriteLine($"做一些任务{Task.CurrentId}");
    Task.Delay(3000).Wait();
}

private void DoOnSecond(Task t)
{
    Console.WriteLine($"任务{t.Id}已完成");
    Console.WriteLine($"本次任务ID:{Task.CurrentId}");
    Console.WriteLine("做一些清理工作");
    Task.Delay(3000).Wait();
}

任务层次结构

利用任务连续性,可以在一个任务结束后启动另一个任务。任务也可以构成一个层次结构。一个任务启动一个新任务时,就启动了一个子层次结构。
如果父任务在子任务之前结束,父任务的状态就显示为WaitingForChildrenToComplete。所有的子任务也结束时,父任务的状态就变成RanToCompletion。当然,如果父任务用TaskCreationOption.DetachedFromParent创建一个任务时,这就无效。

public void ParentAndChild()
{
    var parent = new Task(ParentTask);
    parent.Start();
    Task.Delay(2000).Wait();
    Console.WriteLine(parent.Status);
    Task.Delay(4000).Wait();
    Console.WriteLine(parent.Status);
}

private void ParentTask()
{
    Console.WriteLine($"Task id{Task.CurrentId}");
    var child = new Task(ChildTask);
    child.Start();
    Task.Delay(1000).Wait();
    Console.WriteLine("父任务开始子任务");
}

private void ChildTask()
{
    Console.WriteLine("子任务");
    Task.Delay(5000).Wait();
    Console.WriteLine("子任务已完成");
}

结果

Task id1
子任务
父任务开始子任务
RanToCompletion
子任务已完成
RanToCompletion

从方法中返回任务

public Task<IEnumerable<string>> TaskMethodAsync()
{
    //new List<string>() { "one", "two" }为返回的内容,或者说是需要的对象。
    return Task.FromResult<IEnumerable<string>>(new List<string>() { "one", "two" });
}

等待任务

等待任务的几种方式:

  • Task.WaitAll()——.net4之后可用,阻塞调用任务,等待所有任务完成。
  • Task.WhenAll()——.net4.5之后可用,Task.WhenAll()返回一个任务,从而允许使用async关键字等待结果,它不会阻塞等待的任务。
  • Task.WaitAny()——等待任务列表中的一个任务完成,会阻塞任务的调用。
  • Task.WhenAny()——等待任务列表中的一个任务完成,返回可以等待的任务。
  • Task.Delay()——等待任务
  • Task.Yeild()——释放CPU,允许其他任务运行

取消架构

Parallel.For()方法的取消

public void CancelParallelFor()
{
    var cts = new CancellationTokenSource();
    cts.Token.Register(() => Console.WriteLine("*** token cancelled"));
    //500毫秒后取消标记
    cts.CancelAfter(500);
    try
    {
        ParallelLoopResult result =
            Parallel.For(0, 100, new ParallelOptions
            {
                CancellationToken = cts.Token
            },
            x =>
            {
                Console.WriteLine($"Loop{x}开始");
                int sum = 0;
                for (int i = 0; i < 100; i++)
                {
                    Task.Delay(2).Wait();
                    sum += i;
                }
                Console.WriteLine($"Loop{x}完成");
            });
    }
    catch (OperationCanceledException ex)
    {
        Console.WriteLine(ex.Message);
    }
}
Loop50开始
Loop0开始
Loop1开始
*** token cancelled
Loop0完成
Loop50完成
Loop1完成
The operation was canceled.

任务的取消

public void CancelTask()
{
    var cts = new CancellationTokenSource();
    cts.Token.Register(() => Console.WriteLine("*** Task 取消"));
    cts.CancelAfter(500);
    Task t1 = Task.Run(() => {
        Console.WriteLine("进入Task");
        for (int i = 0; i < 20; i++)
        {
            Task.Delay(100).Wait();
            CancellationToken token = cts.Token;
            if (token.IsCancellationRequested)
            {
                Console.WriteLine("请求取消操作,任务取消");
                token.ThrowIfCancellationRequested();
                break;
            }
            Console.WriteLine("在Task中的循环中");
        }
        Console.WriteLine("Task已完成,且没有被取消");
    }, cts.Token);
    try
    {
        t1.Wait();
    }
    catch (AggregateException ex)
    {
        Console.WriteLine($"异常:{ex.GetType().Name},{ex.Message}");
        foreach (var innerException in ex.InnerExceptions)
        {
            Console.WriteLine($"内部异常:{ex.InnerException.GetType()},{ex.InnerException.Message}");
        }
    }
}

结果

进入Task
在Task中的循环中
在Task中的循环中
在Task中的循环中
*** Task 取消
请求取消操作,任务取消
异常:AggregateException,One or more errors occurred. (A task was canceled.)
内部异常:System.Threading.Tasks.TaskCanceledException,A task was canceled.

数据流

Parallel类、Task类和Parallel LINQ为数据并行性提供了很多帮助。但是,这些类不能直接支持数据流的处理,以及并行转换数据。此时,需要使用Task Parallel Library Data Flow(TPL Data Flow)。

使用动作块ActionBlock

TPL Data Flow 的核心是数据块,这些数据块作为提供数据的源或者接收数据的目标,或者同时作为源和目标。

/// <summary>
/// ActionBlock示例
/// </summary>
public void SimpleDataFlowDemoUsingActionBlock()
{
    //ActionBlock异步处理消息,把信息写入控制台。
    var processInput = new ActionBlock<string>(s =>
    {
        Console.WriteLine($"用户输入:{s}");
    });

    bool exit = false;
    while (!exit)
    {
        //读取控制台
        string input = Console.ReadLine();
        if (string.Compare(input, "exit", ignoreCase: true) == 0)
        {
            exit = true;
        }
        else
        {
            //使用Post()把读入的所有字符串写入ActionBlock
            processInput.Post(input);
        }
    }
}

结果

11111
用户输入:11111
exit

源和目标数据块

ActionBlock
上个示例中分配给ActionBlock的方法执行时,ActionBlock会使用一个任务来并行执行。通过检查任务和线程标识符,并把它们写入控制台可以验证这一点。
每个块都实现了IDataflowBlock接口,该接口包含了返回一个Task的属性Completion,以及Complete()和Fault()方法。调用Complete()方法后,块不再接受任何输入,也不再产生任何输出。调用Fault()方法则把块放入失败状态。

块既可以是源,也可以是目标,还可以同时是源和目标。在示例中,ActionBlock是一个目标块,所以实现了ITargetBlock接口。ITargetBlock派生自IDataflowBlock,除了提供IDataBlock接口的成员以外,还定义了OfferMessage()方法。OfferMessage()发送一条由块处理的消息。Post是比OfferMessage更方便的一个方法,它实现为ITargetBlock接口的扩展方法。

ISourceBlock接口由作为数据源的块实现。除了IDataBlock接口的成员以外,ISourceBlock还
提供了链接到目标块以及处理消息的方法。

BufferBlock同时作为数据源和数据目标,它实现了ISourceBlock和ITargetBlock。

public void SimpleDataFlowDemoUsingBufferBlock()
{
    Task t1 = Task.Run(() => Producer());
    Task t2 = Task.Run(async () => await ConsumerAsync());
    Task.WaitAll(t1, t2);
}

private BufferBlock<string> s_buffer = new BufferBlock<string>();

/// <summary>
/// 此方法从控制台读取字符串,并通过调用post方法把字符串写到BufferBlock中:
/// </summary>
public void Producer()
{
    bool exit = false;
    while (!exit)
    {
        string input = Console.ReadLine();
        if (string.Compare(input, "exit", ignoreCase: true) == 0)
        {
            exit = true;
        }
        else
        {
            s_buffer.Post(input);
        }
    }
}

/// <summary>
/// 此方法在一个循环中调用ReceiveAsync()方法来接收BufferBlock中的数据。
/// </summary>
/// <returns></returns>
public async Task ConsumerAsync()
{
    while (true)
    {
        string data = await s_buffer.ReceiveAsync();
        Console.WriteLine($"用户输入:{data}");
    }
}

结果

7
用户输入:7
exit

连接块

本节将连接多个块,创建一个管道。首先,创建由块使用的3个方法。

/// <summary>
/// 此方法只需要启动管道。调用Post()方法传递目录时,管道就会启动,并最终将单词从c#源代码写入控制台。
/// 这里可以发出多个启动管道的请求,传递多个目录,并行执行这些任务
/// </summary>
public void ContectBlockDemoStart() {
    var target = SetupPipeline();
    target.Post(@"C:\Users\Dream\Desktop\UICalculator\CalculatorContract");
}

/// <summary>
/// 方法接收一个目录路径作为参数,得到以.cs为扩展名的文件名
/// </summary>
/// <param name="path"></param>
/// <returns></returns>
private IEnumerable<string> GetFileNames(string path)
{
    foreach (var fileName in Directory.EnumerateFiles(path, "*.cs"))
    {
        yield return fileName;
    }
}

/// <summary>
/// 方法以一个文件名列表作为参数,得到文件中的每一行
/// </summary>
/// <param name="fileNames"></param>
/// <returns></returns>
private IEnumerable<string> LoadLines(IEnumerable<string> fileNames)
{
    foreach (var fileName in fileNames)
    {
        using (FileStream stream = File.OpenRead(fileName))
        {
            var reader = new StreamReader(stream);
            string line = null;
            while (((line = reader.ReadLine()) != null))
            {
                yield return line;
            }
        }
    }

}

/// <summary>
/// 方法接收一个lines集合作为参数,将其逐行分割,从而得到并返回一个单词列表
/// </summary>
/// <param name="lines"></param>
/// <returns></returns>
private IEnumerable<string> GetWords(IEnumerable<string> lines)
{
    foreach (var line in lines)
    {
        string[] words = line.Split(' ', ';', '(', ')', '{', '}', '.', ',');
        foreach (var word in words)
        {
            if (!string.IsNullOrEmpty(word))
            {
                yield return word;
            }
        }
    }
}

/// <summary>
/// 为了创建管道,SetupPipeline()方法创建了3个TransformBlock对象。
/// TransformBlock是一个源和目标块,通过使用委托来转换源。
/// 第一个TransfomBlock被声明为将一个字符串转换为IEnumerable<sfring>
/// 这种转换是通过GetFileNames()方法完成的,GetFileNames()方法在传递给第一个块的构造函数的lambda表达式中调用。
/// 类似地,接下来的两个TransformBlock对象用于调用LoadLines()和GetWords()方法
/// </summary>
/// <returns></returns>
private ITargetBlock<string> SetupPipeline()
{
    var fileNamesForPath = new TransformBlock<string, IEnumerable<string>>(
        path =>
        {
            return GetFileNames(path);
        });

    var lines = new TransformBlock<IEnumerable<string>, IEnumerable<string>>(
       fileNames =>
       {
           return LoadLines(fileNames);
       });

    var words = new TransformBlock<IEnumerable<string>, IEnumerable<string>>(
       lines2 =>
       {
           return GetWords(lines2);
       });

    //定义的最后一个块是ActionBlock。这个块只是一个用于接收数据的目标块
    var display = new ActionBlock<IEnumerable<string>>(
        coll=> {
            foreach (var s in coll)
            {
                Console.WriteLine(s);
            }
        });
    //最后,将这些块彼此连接起来。最后返回用于启用管道的块。
    fileNamesForPath.LinkTo(lines);
    lines.LinkTo(words);
    words.LinkTo(display);
    return fileNamesForPath;
}

结果

namespace
CalculatorContract
public
interface
IBinaryOperation
double
Operation
double
x
double
y

通过对TPL Data Flow库的简单介绍,可以看到这种技术的主要用法。该库还提供了其他许多功能,例如以不同方式处理数据的不同块。BroadcastBlock允许向多个目标传递输入源(例如将数据写入一个文件并显示该文件),JoinBlock将多个源连接到一个目标,BatchBlock将输入作为数组进行批处理。使用DataflowBlockOptions选项可以配置块,例如一个任务中可以处理的最大项数,还可以向其传递取消标记来取消管道。使用链接技术,可以对消息进行筛选,只传递满足指定条件的
消息。

猜你喜欢

转载自blog.csdn.net/Star_Inori/article/details/80498952
21
21)