异步编程的几个类
Parallel类
自.NET4以来.NET提供线程的一个抽线机制:任务。Parallel类是对线程一个很好的抽象。该类位于System.Threading.Tasks命名空间中,提供了数据和任务的并行性。如果需要数据并行(多个线程协同处理同一批数据)或任务并行(多个任务处理不同逻辑功能),而不需要更多的控制与线程间的交互,可以使用Parallel。下面介绍Parallel常用用法。
Parallel.For()
Parallel定义了并行的for静态方法。对于C#的for语言而言,循环在单个线程中运行。而Parallel类的for方法则使用了多任务,每迭代一次都会从线程池申请一个空闲线程来执行指定代码块。Parallel.For()方法在每次迭代中执行相同代码段,迭代顺序没有明确定义,用于数据并行性。下面举两个例子说明:
- Parallel.For()的用法
class Program
{
static void Main(string[] args)
{
ParallelFor();
Console.Read();
}
public static void ParallelFor()
{
var result = Parallel.For(0, 10, i =>//i从0到9执行10次
{
Task.Delay(1000).Wait();
Log($"E{i}");
});
Console.WriteLine($"Is cpmplete:{result.IsCompleted }");
}
public static void Log(string prefix)
{
Console.WriteLine($"{prefix}, task:{Task.CurrentId},threadid: {Thread.CurrentThread.ManagedThreadId}");
}
}
运行结果如下,可以看出每迭代一次都是启动一个新线程执行,而下图中线程ID为8出现两次,这是由线程池分配的空闲线程,由不得我们控制。
- 下面特别介绍Parallel.For< T >()的用法,Parallel.For()方法的泛型版本还接受3个委托参数,分别为
1.Func< TLocal > localInit,该委托只要用于为每个迭代任务初始化,返回一个泛型
2.Func< int,ParallelLoopState,TLocal,TLocal > body,改委托接受迭代下标、停用迭代的一个ParallelLoopState对象(**如果想提前停止迭代,可传入该对象并调用该对象的Break()方法即可)、以及上一个迭代的返回值,TLocal线程安全
3.Action< TLocal > localFinally,最后收尾处理,接受上一个迭代的返回值,TLocal线程安全
{
static void Main(string[] args)
{
ParallelForWithInit();
Console.Read();
}
public static void ParallelForWithInit()
{
var result = Parallel.For<string>(0, 2, () =>//对每个线程初始化返回值可在第4个参数(委托)中的str1获取到
{
Log($"init thread");
return $"{Thread.CurrentThread.ManagedThreadId}";
},
(i, pls, str1) =>// Func<int, ParallelLoopState, TLocal, TLocal> 类型,i为迭代下标,pls控制循环停止,str1为For第2参数(委托)的返回值
{
Log($"body i={i} str1={str1}");
Task.Delay(100).Wait();
return $"{i}";
},
(str1) =>//Action<TLocal> 类型,str1为For第3参数(委托)的返回值
{
Log($"finally str1={str1}");
});
Console.WriteLine($"Is cpmplete:{result.IsCompleted }");
}
public static void Log(string prefix)
{
Console.WriteLine($"{prefix}, task:{Task.CurrentId},threadid: {Thread.CurrentThread.ManagedThreadId}");
}
}
从下图可以看出线程id为1的三个委托中,body的str1是init的返回值,finally的str1是body的返回值,线程安全。通过这个功能,这个方法完美地累加了大量数据集合的结果。
Parallel.ForEach()
相信了解过C#语言的Foreach关键字的同学,应该能猜出Parallel.ForEach()这个静态方法的用途了吧,咱就不多做解释,直接看例子。
class Program
{
static void Main(string[] args)
{
ParallelForWithInit();
Console.Read();
}
public static void ParallelForWithInit()
{
List<int> data = new List<int> { 1, 2, 3, 4, 5, 6 };
var result = Parallel.ForEach<int>(data, i =>
{
Log($"data={i}");
});
Console.WriteLine($"Is cpmplete:{result.IsCompleted }");
}
public static void Log(string prefix)
{
Console.WriteLine($"{prefix}, task:{Task.CurrentId},threadid: {Thread.CurrentThread.ManagedThreadId}");
}
}
看下图运行结果,可以分析出ForEach方法对data进行遍历并从线程池申请空闲线程去打印,顺序没有明确定义。
Parallel.Invoke()
细心的同学应该发现了,不管是For还是Foreach,都只能迭代执行一个委托,那么下面我们介绍Parallel的另一个支持多个委托的静态方法Invoke,Invoke允许传递一个Action数组。
class Program
{
static void Main(string[] args)
{
ParallelForWithInit();
Console.Read();
}
public static void ParallelForWithInit()
{
Parallel.Invoke(Log1, Log2, Log3);
}
public static void Log1()
{
Console.WriteLine($"Log1:task:{Task.CurrentId},threadid: {Thread.CurrentThread.ManagedThreadId}");
}
public static void Log2()
{
Console.WriteLine($"Log2:task:{Task.CurrentId},threadid: {Thread.CurrentThread.ManagedThreadId}");
}
public static void Log3()
{
Console.WriteLine($"Log3:task:{Task.CurrentId},threadid: {Thread.CurrentThread.ManagedThreadId}");
}
}
如下图运行结果,每一个Action委托都是一个单独的线程在执行。
Parallel总结
Parallel类是.Net对线程的一个很好的抽象,主要提供了For、Foreach和Invoke三个静态方法的支持。For、Foreach适合数据并发执行,只能接受一个委托。而Invoke适合任务并行模式,亦即可接受多个不同的无参Action。
Task类
Task类作是.NET Framework 4新增加的异步操作类,极大的方便了对多线程和异步操作的代码编写。Task类相比之前使用委托进行异步操作和使用ThreadPool线程池进行多线程,不仅极大简化了代码编写,而且添加了许多实用的方法和良好的状态管理机制。
Task任务启动的几种写法
- 异步启动任务,每启动一个Task任务,底层都会到线程池申请一个空闲线程来完成指定委托。
class Program
{
static async Task Main(string[] args)
{
//方式1
var t1 = Task<string>.Run(() => {
Console.WriteLine($"方式1 的线程ID为{Thread.CurrentThread.ManagedThreadId}");
return "方式1"; });
//方式2
var t2 = Task<string>.Factory.StartNew(() => {
Console.WriteLine($"方式2 的线程ID为{Thread.CurrentThread.ManagedThreadId}");
return "方式2"; });
//方式3
var t3 =new TaskFactory <string>().StartNew(() => {
Console.WriteLine($"方式3 的线程ID为{Thread.CurrentThread.ManagedThreadId}");
return "方式3"; });
//方式4
var t4 = new Task<string>(() => {
Console.WriteLine($"方式4 的线程ID为{Thread.CurrentThread.ManagedThreadId}");
return "方式4"; });
t4.Start();
Console.WriteLine($"主线程 的线程ID为{Thread.CurrentThread.ManagedThreadId}");
Console.Read();
}
}
我们看到先打印“主线程。。。”,然后再打印各个任务,说明了Task不会阻塞主线程。
- 同步启动任务,任务不一定使用线程池中的线程来执行,也可以使用其他线程。
class Program
{
static async Task Main(string[] args)
{
var t = new Task<string>(() =>
{
Console.WriteLine($"任务线程ID为{Thread.CurrentThread.ManagedThreadId}");
return "完成";
});
t.RunSynchronously();
Console.WriteLine($"主线程 的线程ID为{Thread.CurrentThread.ManagedThreadId}");
Console.Read();
}
}
通过结果可以看出两个任务线程ID一致,RunSynchronously方法阻塞了主线程,并通过主线程执行了任务。
- 如果任务将长时间运行,Task支持同TaskCreationOptions.LongRunning告诉任务调度器创建一个新线程,而不是从线程池中申请使用线程,此时的线程可以不受线程池管理。当新任务向线程池申请线程时,线程池会选择等待其他任务执行完成后释放的线程,而不是重新创建。而对于长时间运行任务的新线程,任务调度器会立刻知道等待它们执行完成是没结果的,会过滤掉这类线程去等待其他任务线程释放。
class Program
{
static async Task Main(string[] args)
{
var t = new Task<string>(() =>
{
Console.WriteLine($"线程ID为{Thread.CurrentThread.ManagedThreadId},是否是托管于线程池:{Thread.CurrentThread.IsThreadPoolThread}");
return "完成";
}, TaskCreationOptions.LongRunning);
t.Start();
Console.WriteLine($"主线程 的线程ID为{Thread.CurrentThread.ManagedThreadId}");
Console.Read();
}
}
Task接受Action委托,泛型Task接受Func 委托