在串行编程时,操作都是按顺序执行的,比如数字从1到100000递增,就必然的是1、2、3、4……100000。代码如下
for (int i = 1; i <= 100000; i++) { Console.WriteLine(i); }
然而,在并行化编程时,因为是并行运行的,所以执行顺序会与系统和硬件有关,这样一来,执行顺序就变的不可预知,比如。
Parallel.For(1,100001,(i)=>Console.WriteLine(i));
很明显,最大的数100000在前面就已经输出了,如果执行多次,会看到这个结果不是固定的。
因为执行顺序的不确定性,所以当我们在多个线程或者多个任务中去同时操作一个变量时,就可能引发问题。比如
int testValue = 0; Task.Factory.StartNew(() => { for (int i = 0; i < 100; i++) { testValue++; Console.WriteLine("Add:" + testValue); } }); Task.Factory.StartNew(() => { for (int i = 0; i < 100; i++) { testValue--; Console.WriteLine("subtract:" + testValue); } }); Console.ReadLine();
在两个task中执行增加和减少操作,结果变的不可测。而如果我们在线程或者任务中去依赖于这样的变量作判断条件时,后果就会变的相当严重。这里,testValue变量是一种非线程安全/非任务安全的,如果要对其实现递增和递减就得采用原子操作,即Interlocked.Increment和Interlocked.Decrement。
既然有非线程安全/非任务安全,自然就有对应的线程安全/任务安全,这包含在System.Collections.Concurrent的命名空间中。ConcurrentQueue、ConcurrentStack、ConcurrentBag、ConcurrentDictionary,这些对应了Queue、Stack、Array/List、Dictionary。
比如我们需要在A线程添加对象,又需要在B线程中移除对象时,可能道先想到的是List,但List在多线程操作会出问题,因为并行运行会导致并发,结果有可能在同一时间内对List进行操作。这时就要考虑使用线程安全/任务安全的类型来替代。
总的来说,在多线程或者多任务的并行化操作时,要优先考虑线程安全/任务安全的数据类型,如果一定要用非线程安全的数据时,就得增加同步控制(比如原子操作Interlocked、锁lock、信号量SemaphoreSlim/Semaphore、倒计事件CountdownEvent、共享事件ManualResetEventSlim/ManualResetEvent、自旋锁SpinLock等)。