关于C#多线程易失域锁的分享

Posted guhuazhen

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了关于C#多线程易失域锁的分享相关的知识,希望对你有一定的参考价值。

一、多线程

  windows系统是一个多线程的操作系统。一个程序至少有一个进程,一个进程至少有一个线程。进程是线程的容器,一个C#客户端程序开始于一个单独的线程,CLR(公共语言运行库)为该进程创建了一个线程,该线程称为主线程。例如当我们创建一个C#控制台程序,程序的入口是Main()函数,Main()函数是始于一个主线程的。它的功能主要 是产生新的线程和执行程序。

  在软件中,如果有一种操作可以被多人同时调用,我们就可以创建多个线程同时处理,以提高任务执行效率。这时,操作就被分配到各个线程中分别执行。

在C#中我们可以使用Thread类和ThreadStart委托,他们都定义在System.Threading命名空间中。

  ThreadStart委托类型用于定义在线程中的工作,就像我们在使用其他的委托类型一样,可以使用方法名来创建此委托类型对象,如“new ThreadStart(test)”

多线程优点:
(1)多线程技术使程序的响应速度更快 ,因为用户界面可以在进行其它工作的同时一直处于活动状态;
(2)多线程可以提高CPU的利用率,因为当一个线程处于等待状态的时候,CPU会去执行另外的线程;
(3)占用大量处理时间的任务可以定期将处理器时间让给其它任务;
(4)可以随时停止任务;
(5)可以分别设置各个任务的优先级以优化性能。

多线程缺点:
(1)等候使用共享资源时造成程序的运行速度变慢。这些共享资源主要是独占性的资源 ,如写文件等。
(2)对线程进行管理要求额外的 CPU开销。线程的使用会给系统带来上下文切换的额外负担。当这种负担超过一定程度时,多线程的特点主要表现在其缺点上,比如用独立的线程来更新数组内每个元素。
(3)线程的死锁。即较长时间的等待或资源竞争以及死锁等多线程症状。
(4)对公有变量的同时读或写。当多个线程需要对公有变量进行写操作时,后一个线程往往会修改掉前一个线程存放的数据,从而使前一个线程的参数被修改;另外 ,当公用变量的读写操作是非原子性时,在不同的机器上,中断时间的不确定性,会导致数据在一个线程内的操作产生错误,从而产生莫名其妙的错误,而这种错误是程序员无法预知的。

线程生命周期

线程生命周期开始于 System.Threading.Thread 类的对象被创建时,结束于线程被终止或完成执行时。

下面列出了线程生命周期中的各种状态:

    • 未启动状态:当线程实例被创建但 Start 方法未被调用时的状况。
    • 就绪状态:当线程准备好运行并等待 CPU 周期时的状况。
    • 不可运行状态:下面的几种情况下线程是不可运行的:
      • 已经调用 Sleep 方法
      • 已经调用 Wait 方法
      • 通过 I/O 操作阻塞
    • 死亡状态:当线程已完成执行或已中止时的状况

Thread 类常用的属性和方法

  技术图片

技术图片

最简单的多线程例子,代码如下:

static void Main(string[] agrs)
        
            ThreadStart threadWork = new ThreadStart(test);
            Thread t1 = new Thread(threadWork);
            t1.Name = "t1";
            Thread t2 = new Thread(threadWork);
            t2.Name = "t2";
            Thread t3 = new Thread(threadWork);
            t3.Name = "t3";
            //开始执行
            t1.Start();
            t2.Start();
            t3.Start();
            Console.ReadKey();
        
        static public void  test()
            Console.WriteLine("0,hello,小菜鸟",Thread.CurrentThread.Name);
        

使用多线程另一个重要的问题就是对于公共资源分配的控制,比如,火车的座位是有限的,在不同购票点买票时,就需要对座位资源进行合理分配;在电影院看电影也是这样的,座位只有那么多,我们不可能100个座位卖出200张票,这样是不可以的也是不应该的,那么接下来我们就要看看该如何解决这个问题。

二、易失域

对于类中的成员使用volatile修饰符,它就会被声明为易失域。对于易失域,在多线程环境中,每个线程中对此域的读取(易失读取,volatile read)和写入(易失写入,volatile write)操作都会观察其他线程中的操作,并进行操作的顺序执行,这样就保持易失域使用的一致性了。

volatile的作用是: 作为指令关键字,确保本条指令不会因编译器的优化而省略,且要求每次直接读值。多线程的程序,共同访问的内存当中,多个线程都可以操纵,从而无法判定何时这个变量会发生变化

可以这样简单理解:线程是并行的,但对volatile的访问是顺序排除的,避免出现脏值。

理解:

Volatile 字面的意思时易变的,不稳定的。在C#中也差不多可以这样理解。

编译器在优化代码时,可能会把经常用到的代码存在Cache里面,然后下一次调用就直接读取Cache而不是内存,这样就大大提高了效率。但是问题也随之而来了。

在多线程程序中,如果把一个变量放入Cache后,又有其他线程改变了变量的值,那么本线程是无法知道这个变化的。它可能会直接读Cache里的数据。但是很不幸,Cache里的数据已经过期了,读出来的是不合时宜的脏数据。这时就会出现bug。

用Volatile声明变量可以解决这个问题。用Volatile声明的变量就相当于告诉编译器,我不要把这个变量写Cache,因为这个变量是可能发生改变的。

下面贴栗子代码:

using System;
using System.Threading;
 
namespace demoVolatile

    class Program
    
        //多个线程访问的变量,标记为Volatile
        //在这里如果不标记可能会卖出不止10张票
        volatile static int TicketCount = 10;
        static void SellTicket()
        
            while (TicketCount > 0)
            
                TicketCount--;
                Console.WriteLine("0 卖出了一张票", Thread.CurrentThread.Name);
            
            Console.WriteLine("0 下班了", Thread.CurrentThread.Name);
        
        static void Main(string[] args)
        
            ThreadStart threadWork = new ThreadStart(SellTicket);
            Thread t1 = new Thread(threadWork);
            t1.Name = "t1";
            Thread t2 = new Thread(threadWork);
            t2.Name = "t2";
            Thread t3 = new Thread(threadWork);
            t3.Name = "t3";
            //开始执行
            t1.Start();
            t2.Start();
            t3.Start();
            Console.ReadKey();
        
    

技术图片

三、锁

我们都知道,lock 关键字可以用来确保代码块完成运行,而不会被其他线程中断。也就是,说在多线程中,使用lock关键字,可以让被lock的对象,一次只被一个线程使用。

lock语句根本使用的就是Monitor.Enter和Monitor.Exit,也就是说lock(this)时执行Monitor.Enter(this),大括号结束时执行Monitor.Exit(this). 也就是说,Lock关键字,就是一个语法糖而已。

使用lock需要注意的地方:
1.lock不能锁定空值某一对象可以指向Null,但Null是不需要被释放的。(请参考:认识全面的null)
2.lock不能锁定string类型,虽然它也是引用类型的。因为字符串类型被CLR“暂留”
3.lock锁定的对象是一个程序块的内存边界
4.值类型不能被lock,因为前文标红字的“对象被释放”,值类型不是引用类型的
5.lock就避免锁定public 类型或不受程序控制的对象。

using System;
using System.Threading.Tasks;

public class Account

    private readonly object balanceLock = new object();
    private decimal balance;

    public Account(decimal initialBalance)
    
        balance = initialBalance;
    

    public decimal Debit(decimal amount)
    
        lock (balanceLock)
        
            if (balance >= amount)
            
                Console.WriteLine($"Balance before debit :balance, 5");
                Console.WriteLine($"Amount to remove     :amount, 5");
                balance = balance - amount;
                Console.WriteLine($"Balance after debit  :balance, 5");
                return amount;
            
            else
            
                return 0;
            
        
    

    public void Credit(decimal amount)
    
        lock (balanceLock)
        
            Console.WriteLine($"Balance before credit:balance, 5");
            Console.WriteLine($"Amount to add        :amount, 5");
            balance = balance + amount;
            Console.WriteLine($"Balance after credit :balance, 5");
        
    


class AccountTest

    static void Main()
    
        var account = new Account(1000);
        var tasks = new Task[100];
        for (int i = 0; i < tasks.Length; i++)
        
            tasks[i] = Task.Run(() => RandomlyUpdate(account));
        
        Task.WaitAll(tasks);
    

    static void RandomlyUpdate(Account account)
    
        var rnd = new Random();
        for (int i = 0; i < 10; i++)
        
            var amount = rnd.Next(1, 100);
            bool doCredit = rnd.NextDouble() < 0.5;
            if (doCredit)
            
                account.Credit(amount);
            
            else
            
                account.Debit(amount);
            
        
    

作用:当同一个资源被多个线程读,少个线程写的时候,使用读写锁

引用:https://blog.csdn.net/weixin_40839342/article/details/81189596 

问题: 既然读读不互斥,为何还要加读锁

答:     如果只是读,是不需要加锁的,加锁本身就有性能上的损耗

            如果读可以不是最新数据,也不需要加锁

            如果读必须是最新数据,必须加读写锁

            读写锁相较于互斥锁的优点仅仅是允许读读的并发,除此之外并无其他。

注意:不要使用ReaderWriterLock,该类有问题

 

ok,今天的分享就到这里了,如有错误的地方请指出,谢谢。

 

以上是关于关于C#多线程易失域锁的分享的主要内容,如果未能解决你的问题,请参考以下文章

C# 内存模型和非易失性变量在其他线程创建之前初始化

C# 线程 - 锁 - 如何检查锁的出现

关于C#的多线程方面

关于Java多线程的线程同步和线程通信的一些小问题(顺便分享几篇质量高的博文)

C# 关于多线程和CPU的问题

多线程下解决资源竞争的7种方法