并行化时要注意的线程安全与任务安全

在串行编程时,操作都是按顺序执行的,比如数字从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等)。


原文地址:https://www.cnblogs.com/sparkleDai/p/7605071.html