• 关于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,今天的分享就到这里了,如有错误的地方请指出,谢谢。

  • 相关阅读:
    Cache与主存的映射方式 例题
    软件测试实验三
    指令的寻址方式
    指令系统
    Cache与主存的映射方式
    指令周期与数据通路
    SQL语句中Cast的用法
    详解SQL Server STUFF的用法
    使用adb卸载系统APP
    第四章:(1)本地运行模式
  • 原文地址:https://www.cnblogs.com/guhuazhen/p/11239636.html
Copyright © 2020-2023  润新知