网站 演示代码中国目前最好的搜索引擎
news/
2025/9/22 22:37:25/
文章来源:
网站 演示代码,中国目前最好的搜索引擎,苏州百度,软件开发培训机构电话理解线程同步线程的数据访问在并行#xff08;多线程#xff09;环境中#xff0c;不可避免地会存在多个线程同时访问某个数据的情况。多个线程对共享数据的访问有下面3种情形#xff1a;多个线程同时读取数据#xff1b;单个线程更新数据#xff0c;此时其他线程读取数据… 理解线程同步线程的数据访问在并行多线程环境中不可避免地会存在多个线程同时访问某个数据的情况。多个线程对共享数据的访问有下面3种情形多个线程同时读取数据单个线程更新数据此时其他线程读取数据多个线程同时更新数据。显而易见多个线程同时读取数据是不会产生任何问题的。仅有一个线程更新数据的时候貌似也没有问题但真的没有问题吗多个线程同时更新数据很明显你可能把我的更改覆盖掉了数据从此不再可信。什么是线程同步为了解决多线程同时访问共享数据可能导致数据被破坏的问题我们需要采取一些措施来保证数据的一致性让每个线程都能准确地读取或更新数据。问题的根源在于多个线程同时访问数据那么只要我们保证同一时间只有一个线程访问数据就能解决问题。保证同一时间只有一个线程访问数据的处理就是线程同步了。我在访问数据的时候你们都先等着我完事了你们再来。C#中的线程同步.NET提供了很多线程同步的方式这些方式分为用户模式和内核模式以及混合模式即用户模式与内核模式的结合下面会总结C#/.NET中各模式下的线程同步。用户模式与内核模式Windows操作系统下CPU跟据所执行代码的不同会在两种模式下进行切换。CPU执行应用程序代码如我们开发的.NET程序时一般运行在用户模式下执行操作系统核心代码内核函数或者某些设备驱动程序时CPU则切换到内核模式。用户模式的代码只能访问自身进程的专有地址空间代码异常不会影响到其他程序或者操作系统内核模式的所有代码共享单个地址空间代码异常将可能导致系统崩溃。CPU的模式切换是为了保证应用程序和操作系统的稳定性。应用程序中线程可以通过Windows API调用操作系统内核函数这时候执行线程的CPU将从用户模式切换到内核模式执行完操作系统函数后再由内核模式切换到用户模式。CPU的模式切换是很耗时的据《Windows核心编程》中的描述CPU模式的切换要占用1000个以上的CPU周期。因此在我们的.NET程序中应该尽可能地避免CPU的模式切换。用户模式线程同步用户模式下利用特殊的CPU指令来协调线程使同一时间只有一个线程能访问某内存地址这种协调在硬件中发生速度很快。这种模式下CPU指令对线程的阻塞很短暂操作系统调度线程时不会认为该线程已被阻塞这种情况下线程池不会创建新的线程来替换该线程。用户模式下等待资源的线程会一直被操作系统调度导致线程的“自旋”并因此浪费很多的CPU资源。如果某线程一直占着资源不释放等待该资源的线程将一直处于自旋状态这样就造成了“活锁”活锁除了浪费内存外还会浪费大量CPU。.NET提供两种用户模式的线程同步volatile和interlocked即易变和互锁。volatile关键字和Volatile上面我们遗留了一个问题只有一个线程更新数据其他线程读取数据会不会出现问题先看一个例子private static bool _stop;public static void Run(){ Task.Run(() {int number 1;while (!_stop) { number; } Console.WriteLine($increase stopped,value {number}); }); Thread.Sleep(1000); _stop true;}编译器和CPU会对上面的代码进行优化调试模式不会优化任务线程在执行时会把_stop读取到CPU寄存器中while循环的时候每次都从当前CPU寄存器中读取_stop同样主线程执行的时候CPU也会把_stop读取到寄存器更新_stop时先更新是CPU寄存器中的_stop值再把值存到变量_stop;在并行环境中主线程和任务线程独立执行主线程对_stop的更新并不会公开到任务线程这样任务线程的while循环便不会停止永远无法得到输出。把变量读到寄存器只是CPU优化代码的一种方式CPU还可能调整代码的执行顺序当前CPU任务这种调整不会改变代码的意图。上面的代码说明由于编译器和CPU的优化只有一个线程更新数据也可能存在问题。这种情况我们可以使用volatile关键字或者类System.Threading.Volatile来阻止编译器和CPU的优化这种阻止利用的是内存屏障MemoryBarrier它告诉CPU在执行完屏障之前的内存存取后才能执行屏障后面的内存存取。上面代码的问题在于while循环读取到的值总是CPU寄存器中的false。我们把while循环的条件改成!Volatile.Read(ref _stop)或者把用volatile声明变量_stopwhile条件直接读取内存中的值问题就能得到解决。Interlocked原子访问.NET提供的另一种用户模式线程同步方式是System.Threading.Interlocked。Interlocked的工作依赖于代码运行的CPU平台如果是X86的CPU,Interlocked函数会在总线上维持一个硬件信号来阻止其他CPU访问同一内存地址《Windows核心编程第五版》。计算机对变量的修改一般来说并不是原子性的而是分为3个步骤将变量值加载到CPU寄存器改变值将更新后的值存储到内存中假如执行了前两个步骤后CPU被抢占变量在之前线程中的修改将丢失。Interlocked函数保证对值的修改是原子性的一个线程完成变量的修改和存储后另一个线程才能修改变量。System.Threading.Interlocked提供了很多方法例如递增、递减、求和等下面用Interlocked的递增方法展示其线程同步功能。public static void Run(){ DoIncrease(100000);}private static void DoIncrease(int incrementPerThread){int number1 0;int number2 0; Console.WriteLine($use two threads to increase zero. each thread increase {incrementPerThread}.); IListTask increaseTasks new ListTask(); increaseTasks.Add(Task.Run(() { Console.WriteLine($thread #{Thread.CurrentThread.ManagedThreadId} increasing number1.);for (int i 0; i incrementPerThread; i) { Interlocked.Increment(ref number1); } })); increaseTasks.Add(Task.Run(() { Console.WriteLine($thread #{Thread.CurrentThread.ManagedThreadId} increasing number1.);for (int i 0; i incrementPerThread; i) { Interlocked.Increment(ref number1); } })); increaseTasks.Add(Task.Run(() { Console.WriteLine($thread #{Thread.CurrentThread.ManagedThreadId} increasing number2.);for (int i 0; i incrementPerThread; i) { number2; } })); increaseTasks.Add(Task.Run(() { Console.WriteLine($thread #{Thread.CurrentThread.ManagedThreadId} increasing number2.);for (int i 0; i incrementPerThread; i) { number2; } })); Task.WaitAll(increaseTasks.ToArray()); Console.WriteLine($use interlocked: number1 result {number1}); Console.WriteLine($normal increase: number2 result {number2});}运行上面的代码多次每个线程增加的数量尽量大否则不容易体现结果每次number1的结果都一样number2的结果都不同足以体现Interlocked的线程同步功能。SpinLock自旋锁System.Threading.SpinLock是基于InterLocked和SpinWait实现的轻量级自旋锁具体的实现方式这里不去关心。SpinLock的简单用法如下private static SpinLock _spinlock new SpinLock();public static void DoWork(){bool lockTaken false;try { _spinlock.Enter(ref lockTaken); }finally {if (lockTaken) { _spinlock.Exit(false); } }}SpinLock很轻量级性能较高但由于是自旋锁锁定的操作应该是很快完成否则会因线程自旋而浪费CPU。内核模式线程同步除了用户模式的两种线程同步方式我们还会利用Windows系统的内核对象实现线程的同步。使用系统内核对象将会导致执行线程的CPU运行模式的切换这会有很大的消耗所以能够使用用户模式的线程同步就尽量避免使用内核模式。内核模式下线程在等待资源时会被系统阻塞避免了CPU的浪费这是内核模式优势。假如线程等待的资源一直被占用则线程将一直处于阻塞状态造成“死锁”。相对于活锁死锁只会浪费内存资源。我们使用系统内核中的事件、信号量和互斥量进行内核模式的线程同步。利用内核事件实现线程同步事件实际上是由系统内核维护的一个布尔值。.NET提供System.Threading.EventWaitHandle进行线程的信号交互。EventWaitHandle继承WaitHandle封装等待对共享资源独占访问的操作系统特定的对象有三个关键方法Set():将事件状态设置为终止状态允许一个或多个等待线程继续。Reset():将事件状态设置为非终止状态导致线程阻塞WaitOne():阻塞线程直到收到事件状态信号线程交互事件有自动重置和手动重置两种类型分别由AutoResetEvent和ManualResetEvent继承EventWaitHandle得到。自动重置事件在Set唤醒第一个阻塞线程之后会自动Reset事件其他阻塞线程仍保持阻塞状态而手动重置事件Set时会唤醒所有被该事件阻塞的线程手动Reset后事件才会继续起作用。手动重置事件的这种性质导致它不能用于线程同步因为不能保证同一时间只有一个线程访问资源;相反自动重置时间则很适合用来处理线程同步。下面的例子演示了利用自动重置时间进行的线程同步。public static void Run(){ DoIncrease(100000);}private static void DoIncrease(int incrementPerThread){int number 0; Console.WriteLine($use two threads to increase zero. each thread increase {incrementPerThread}.); AutoResetEvent are new AutoResetEvent(true); IListTask increaseTasks new ListTask(); increaseTasks.Add(Task.Run(() { Console.WriteLine($thread #{Thread.CurrentThread.ManagedThreadId} is increasing the number.);for (int i 0; i incrementPerThread; i) { are.WaitOne(); number; are.Set(); } })); increaseTasks.Add(Task.Run(() { Console.WriteLine($thread #{Thread.CurrentThread.ManagedThreadId} is increasing the number.);for (int i 0; i incrementPerThread; i) { are.WaitOne(); number; are.Set(); } })); Task.WaitAll(increaseTasks.ToArray()); are.Dispose(); Console.WriteLine($use AutoResetEvent: result {number});}利用信号量进行线程同步信号量是系统内核维护的一个整型变量。信号量值为0时所有等待信号量的线程会被阻塞信号量值大于零0等待的线程会被解除阻塞每唤醒一个阻塞的线程系统内核就会把信号量的值减1。此外我们能够对信号量进行最大值限制从而控制访问同一资源的最大线程数量。.Net中利用System.Threading.Semaphore进行信号量操作。下面时利用信号量实现线程同步的一个例子。public static void Run(){ DoIncrease(100000);}private static void DoIncrease(int incrementPerThread){int number 0; Console.WriteLine($use two threads to increase zero. each thread increase {incrementPerThread}.); Semaphore semaphore new Semaphore(1,1); IListTask increaseTasks new ListTask(); increaseTasks.Add(Task.Run(() { Console.WriteLine($thread #{Thread.CurrentThread.ManagedThreadId} is increasing the number.);for (int i 0; i incrementPerThread; i) { semaphore.WaitOne(); number; semaphore.Release(1); } })); increaseTasks.Add(Task.Run(() { Console.WriteLine($thread #{Thread.CurrentThread.ManagedThreadId} is increasing the number.);for (int i 0; i incrementPerThread; i) { semaphore.WaitOne(); number; semaphore.Release(1); } })); Task.WaitAll(increaseTasks.ToArray()); semaphore.Dispose(); Console.WriteLine($use Semaphore: result {number});}利用互斥体进程线程同步互斥体Mutex的使用与自动重置事件和信号量类似这里不再进行详细的总结。互斥体常被用来保证应用程序只有一个实例运行具体用法如下bool createNew;using (new Mutex(true, Assembly.GetExecutingAssembly().FullName, out createNew)){if (!createNew) { Environment.Exit(0); }else { }}线程同步的混合模式通过上面的总结我们知道用户模式和内核模式由各自的优缺点需要有一种模式既能兼顾用户和内核模式的优点又能避免他们的缺点这就是混合模式。混合模式会优先使用用户模式的线程同步处理当多个线程竞争同步锁的时候才会使用内核对象进行处理。如果多个线程一直不产生资源竞争就不会发生CPU用户模式到内核模式的转换开始资源竞争时又会通过线程阻塞来防止CPU资源的浪费。.NET中提供了多种混合模式的线程同步方式。例如手工重置事件和信号量的简化版本ManualResetEventSlim及SemaphoreSlim他们是线程在用户模式中自旋直到发生资源竞争。具体使用与各自的内核模式一样这里不再赘述。lock关键字和Monitor相信lock加锁是很多人做常用的线程同步方式。lock的使用很简单如下private static readonly object _syncObject new object();public static void DoWork(){lock (_syncObject) { }}实际上lock语法是对System.Threading.Monitor使用的一种简化Monitor的用法如下private static readonly object _syncObject new object();public static void DoWork(){ Monitor.Enter(_syncObject); Monitor.Exit(_syncObject);}使用Monitor的可能会出先一些意象不到的问题。例如如果不相关的业务代码在使用Monitor进行线程同步的时候锁定了同一字符串将会造成不相关业务代码的同步执行此外需要注意的是Monitor不能使用值类型作为锁对象值类型会被装箱装箱后的对象不同将导致无法同步。读写锁ReaderWriterLockSlimReaderWriterLockSlim可以用来实现多线程读取或独占写入的资源访问。读写锁的线程控制逻辑如下一个线程写数据时其他请求资源的线程全部被阻塞一个线程读数据时写线程被阻塞其他读线程能继续运行写结束时解除其他某个写线程的阻塞或者解除所有读线程的阻塞读结束时解除一个写线程的阻塞。下面是读写锁的简单用法详细用法可参考msdn文档。private static readonly ReaderWriterLockSlim _rwlock new ReaderWriterLockSlim();public static void DoWork(){_rwlock.EnterWriteLock();_rwlock.ExitWriteLock();}ReaderWriterLockSlim还有一个比较老的版本ReaderWriterLock据说存在较多问题应尽量避免使用。线程安全集合.NET除了提供包含上面总结到的各种线程同步的诸多方式外还封装了一些线程安全集合。这些集合在内部实现了线程同步我们直接使用即可很友好。线程安全集合在命名空间System.Collections.Concurrent下包括ConcurrentQueue (T),ConcurrentStackT,ConcurrentDictionaryTKey,TValue,ConcurrentBagT,BlockingCollectionT具体可阅读《何时使用线程安全集合》。各种线程同步性能对比下面我们对整数零进行多线程递增操作每个线程固定递增量来测试以下各种同步方式的性能对比。测试代码如下。private static int _numberToIncrease;public static void Run(){int increment 100000;int threadCount 4; DoIncrease(increment, threadCount, DoIncreaseByInterLocked); DoIncrease(increment, threadCount, DoIncreaseWithSpinLock); DoIncrease(increment, threadCount, DoIncreaseWithEvent); DoIncrease(increment, threadCount, DoIncreaseWithSemaphore); DoIncrease(increment, threadCount, DoIncreaseWithMonitor); DoIncrease(increment, threadCount, DoIncreaseWithReaderWriterLockSlim);}public static void DoIncrease(int increment, int threadCount, Actionint action){ _numberToIncrease 0; IListTask increaseTasks new ListTask(threadCount); Stopwatch watch Stopwatch.StartNew();for (int i 0; i threadCount; i) { increaseTasks.Add(Task.Run(() action(increment))); } Task.WaitAll(increaseTasks.ToArray()); Console.WriteLine(${action.Method.Name} Result: {_numberToIncrease} , Time: {watch.ElapsedMilliseconds} ms.);}#region 使用Interlocked用户模式public static void DoIncreaseByInterLocked(int increment){for (int i 0; i increment; i) { Interlocked.Increment(ref _numberToIncrease); }}#endregion#region 使用SpinLock,用户模式private static SpinLock _spinlock new SpinLock();public static void DoIncreaseWithSpinLock(int increment){for (int i 0; i increment; i) {bool lockTaken false;try { _spinlock.Enter(ref lockTaken); _numberToIncrease; }finally {if (lockTaken) { _spinlock.Exit(false); } } }}#endregion#region 使用信号量Semaphore内核模式private static readonly Semaphore _semaphore new Semaphore(1, 10);public static void DoIncreaseWithSemaphore(int increment){for (int i 0; i increment; i) { _semaphore.WaitOne(); _numberToIncrease; _semaphore.Release(1); }}#endregion#region 使用事件AutoResetEvent内核模式private static readonly AutoResetEvent _are new AutoResetEvent(true);public static void DoIncreaseWithEvent(int increment){for (int i 0; i increment; i) { _are.WaitOne(); _numberToIncrease; _are.Set(); }}#endregion#region 使用Monitor混合模式private static readonly object _monitorLocker new object();public static void DoIncreaseWithMonitor(int increment){for (int i 0; i increment; i) {bool lockTaken false;try { Monitor.Enter(_monitorLocker, ref lockTaken); _numberToIncrease; }finally {if (lockTaken) { Monitor.Exit(_monitorLocker); } } }}#endregion#region 使用ReaderWriterLockSlim混合模式private static readonly ReaderWriterLockSlim _rwlock new ReaderWriterLockSlim();public static void DoIncreaseWithReaderWriterLockSlim(int increment){for (int i 0; i increment; i) { _rwlock.EnterWriteLock(); _numberToIncrease; _rwlock.ExitWriteLock(); }}#endregion下面是一组测试结果可以很明显地看出内核模式是相当耗时的应尽量避免使用。而用户模式和混合模式也需要根据具体的场景进行选择。这个测试过于简单不具有普遍性。DoIncreaseByInterLocked Result: 400000 , Time: 15 ms.DoIncreaseWithSpinLock Result: 400000 , Time: 75 ms.DoIncreaseWithEvent Result: 400000 , Time: 1892 ms.DoIncreaseWithSemaphore Result: 400000 , Time: 1779 ms.DoIncreaseWithMonitor Result: 400000 , Time: 14 ms.DoIncreaseWithReaderWriterLockSlim Result: 400000 , Time: 22 ms.小结本文对C#/.NET中的线程同步进行了尽量详尽的总结并行环境中在追求程序的高性能、响应性的同时务必要保证数据的安全性。C#并行编程系列的文章暂时就告一段落了。刚开始写博客文章肯定存在不少问题欢迎各位博友指出。原文地址https://www.cnblogs.com/chenbaoshun/p/10695343.html.NET社区新闻深度好文欢迎访问公众号文章汇总 http://www.csharpkit.com
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/910611.shtml
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!