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

  • 2020 年 3 月 12 日
  • 笔记

一、多线程

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