多线程
什么是进程、线程
- 任务管理器中,有进程;
- 进程包含一个/多个线程;
- 线程之间是可以共享数据的;
- 线程是要切换执行的。
创建线程
var task = new Task(() => {
for(int i=0; i<5000; i++){
Console.WriteLine("i");
}
});
// 声明线程之后并不会马上执行。
task.Start();
// 执行线程
- 上例中,不会输出到5000;
- 因为Main()本身就是一个线程,称为主线程。主线程可以调用其他线程。
- 主线程与task()线程之间不同步,不可控,因此Main()结束后task()不再运行。
- 在task.Start()后加task.Wait()可保证task()运行完全。
-
Wait()有阻塞效果,直到task()全部执行结束,才进行task.Wait()之后的语句。
int times = 5000;
var task1 = new Task(() => {
for(int i=0; i<times; i++){
Console.Write("+");
}
});
var task2 = new Task(() => {
for(int i=0; i<times; i++){
Console.Write("-");
}
});
task1.Start();
task2.Start();
task1.Wait();
task2.Wait();
// 输出结果为+-无规律地输出。一般在使用时可以理解为二者同时进行。
var task = Task.Factory.StartNew(() => "1");
// StartNew()会立刻执行
Console.WriteLine(task.Result); // Result会直接阻塞task,保证task运行完成
线程异常方法
- csharp中线程上的异常是被抑制住的,不会向外抛。
var task = Task.Factory.StartNew(() => {throw new ApplicationException();});
// csharp中线程上的异常是被抑制住的,不会向外抛。
- 为了获取线程上的异常,try下列语句,即可捕获异常:
- task.Wait()
- task.Result
- task.WaitAll()
- task.WaitAny()
var task = Task.Factory.StartNew(() => {throw new ApplicationException("Here is an error!");});
// csharp中线程上的异常是被抑制住的,不会向外抛。
try{
task.Wait();
}catch(AggregateException exs){
foreach(var ex in exs.InnerExceptions){
Console.WriteLine(ex.Message);
// 输出"Here is an error!"
}
}
var task = Task.Factory.StartNew(() => {throw new ApplicationException("Here is an error!");});
// csharp中线程上的异常是被抑制住的,不会向外抛。
task.ContinueWith((t) => { Cosole.WriteLine(t.Exception); //task完成后,继续执行这个地方 }, TaskCountinuationOptions.OnlyOnFaulted);
// t指前一个异常的引用,task
// TaskCountinuationOptions.OnlyOnFaulted指在只有在出异常的情况下才执行task.ContinueWith()。
并行处理
- Parallel.For()
- Parallel.ForEach()
- PLINQ x.AsParallel().x
var list = new List<int>();
for(int i = 0; i< 50000; i++){
list.Add(i);
}
static void CalcTime(Action action){
Stopwatch sw = new Stopwatch();
sw.Start();
action();
sw.Stop();
Console.WriteLine(sw.ElapsedMilliSeconds);
} // 计时器而已
Parallel.Each(list, i => i++);
CalcTime(() => list.ForEach(i => i++));
// 线性方法75ms
CalcTime(() => list.AsParallel.ForAll(i => i++));
// 并行方法412ms
CalcTime(() => list.ForEach(i => Do(ref i)));
// 线性方法290ms
CalcTime(() => list.AsParallel.ForAll(i => Do(ref i)));
// 并行方法165ms
CalcTime(() => Parallel.ForEach(list, i => Do(ref i)));
// 并行方法160ms
static void Do(ref int i){
i = (int)Math.Abs((Math.Sin(i)));
}
同步
原子性
static int Count = 0;
static void Increasement(){
for(int i=0; i<500000000; i++){
Count++;
}
}
static void Decreasement(){
for(int i=0; i<500000000; i++){
Count--;
}
}
static void Main(string[] args){
var task1 = new Task(Increasement);
var task2 = new Task(Decreasement);
task1.Start();
task2.Start();
Task.WaitAll(task1, task2);
// 最终Count输出的值不是0
}
- 上例中,最终Count输出的值不是0,因为两个线程执行任务时互相干扰**(Count++进行到复制内存时可能被另一个线程的Count–的一个步骤干扰)**。
- 在数据库,这叫做事故。
- 一个线程中,本来几个步骤具有原子性,不可分割,但是其中间却被另一个线程插了一腿。
- 因此使用lock关键字。
lock关键字
private static readonly object Syn = new object();
static int Count = 0;
static void Increasement(){
for(int i=0; i<500000000; i++){
lock(Syn){
Count++;
}
// 没有执行完一个完整的Count++,不允许切换线程
}
}
static void Decreasement(){
for(int i=0; i<500000000; i++){
lock(Syn){
Count--;
}
}
}
static void Main(string[] args){
var task1 = new Task(Increasement);
var task2 = new Task(Decreasement);
task1.Start();
task2.Start();
Task.WaitAll(task1, task2);
}
- lock为语法简洁的关键字,可能会牺牲一定效率。还有其他等价于lock的写法,可读性降低但是效率提升。
- 但是如果定义两个Syn类,一个Syn1、一个Syn2,两个线程分别使用不同的锁,则还是会互相干扰,最终Count不等于0。
- 在C#本质论中建议:对可变状态进行同步,不更改的东西不需要同步,多线程访问同一个对象时,必须同步。
InterLocked
- 原子操作,InterLocked封装好的类;
- 其中封装好了许多静态方法。
static int Count = 0;
static void Increasement(){
for(int i=0; i<500000000; i++){
InterLocked.Increment(ref Count);
}
}
static void Decreasement(){
for(int i=0; i<500000000; i++){
lock(Syn){
InterLocked.Decrement(ref Count);
// Interlocked封装好了静态方法Increment()和Decrement()
}
}
}
static void Main(string[] args){
var task1 = new Task(Increasement);
var task2 = new Task(Decreasement);
task1.Start();
task2.Start();
Task.WaitAll(task1, task2);
}
多线程处理模式