多线程

什么是进程、线程

  • 任务管理器中,有进程;
  • 进程包含一个/多个线程;
  • 线程之间是可以共享数据的;
  • 线程是要切换执行的。

创建线程

  • 第一种方法,没有返回值的。
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下列语句,即可捕获异常:
  1. task.Wait()
  2. task.Result
  3. task.WaitAll()
  4. 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!"
	}
}
  • 还可以使用ContinueWith()处理异常。
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()。

并行处理

  • C#封装了许多实现并行的方法。
  1. Parallel.For()
  2. Parallel.ForEach()
  3. 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);
}

多线程处理模式

  • 在项目需要时可以参考多线程处理模式。