C#代码中的线程安全问题(ThreadSafety)

    这节讲一下线程安全的例子,以及如何解决线程安全问题。

    上节提到了线程安全的问题,说了一个例子,1000个人抢100张票,这节就从此案例着手,下面先看一下代码实现:

private static int tickets = 100;
static void Main(string[] args)
{
    Thread thread = BuyTicket();
    Thread thread2 = BuyTicket();
    Thread thread3 = BuyTicket();
    thread.Name = "Thread1";
    thread2.Name = "Thread2";
    thread3.Name = "Thread3";
    thread.Start();
    thread2.Start();
    thread3.Start();
    Thread.Sleep(5000);
}
private static Thread BuyTicket()
{
    Thread thread = new Thread(() =>
      {
          while (tickets > 0)
          {
                Console.WriteLine($"{Thread.CurrentThread.Name}---------------->买了一张票,票号为:{tickets}");
                tickets--;
          }
      });
    thread.IsBackground = true;
    return thread;
}

    现有三个线程,同时访问共享资源tickets ,我们先来看一下运行结果:

    

    100卖出了三次,这就是很明显的线程安全问题,也就是说,他们都同时进入到了while块中,同时拿到了tickets为100的值,所以我们解决线程安全问题,就要从此处下手,让线程访问共享数据的时候,同一时刻只能有一个线程去访问。

 

    lock锁

    解决线程安全的方法就是加锁(同步锁,互斥锁),现在将代码改一下,使其线程安全:

private static object o = new object();
private static int tickets = 100;
static void Main(string[] args)
{
    Thread thread = BuyTicket();
    Thread thread2 = BuyTicket();
    Thread thread3 = BuyTicket();
    thread.Name = "Thread1";
    thread2.Name = "Thread2";
    thread3.Name = "Thread3";
    thread.Start();
    thread2.Start();
    thread3.Start();
    Thread.Sleep(5000);
}
private static Thread BuyTicket()
{
    Thread thread = new Thread(() =>
      {
          while (true)
          {
              lock (o)
              {
                  if (tickets > 0)
                  {
                      Console.WriteLine($"{Thread.CurrentThread.Name}---------------->买了一张票,票号为:{tickets}");
                      tickets--;
                  }
              }
          }
      });
    thread.IsBackground = true;
    return thread;
}

 

     在while块中,我加上了一个lock块,它需要一个Object类型的参数作为同步对象,被lock块包住的代码,在同一时间只能有一个线程访问,看一下运行结果(方便查看,我将数量改为了30):

   

   可以看到,线程安全问题已经解决。我们再来看一下同步对象:

lock (object obj){}

    lock块,它需要一个object类型的参数作为同步对象,也就是说,线程走到这里,会先看看这个同步对象是不是被占用着,如未被占用,则进入,否则线程阻塞,直到同步对象被解除占用,注意,多个线程,要使用一个同步对象,不然,一个线程访问一个单独的同步对象,那跟没加锁一样,另外,根据多态性,这个同步对象可以是任意对象,因为object是所有类的父类,但是string类型不可用,这点要注意。

 

Monitor锁

    monitor锁的用法跟lock差不多,请看如下代码:

while (true)
{
    Monitor.Enter(o);
    if (tickets > 0)
    {
        Console.WriteLine($"{Thread.CurrentThread.Name}---------------->买了一张票,票号为:{tickets}");
        tickets--;
    }
    Monitor.Exit(o);
}

   monitor将代码块改为了enter和exit两个方法,也是需要同步对象。

 

Mutex互斥锁

    互斥锁是一个互斥的同步对象,同一时间有且仅有一个线程可以获取它。跟monitor一样,也是通过两个方法控制的,具体用法请看下面的代码:

private static Mutex mutex = new Mutex();
private static Thread BuyTicket3()
{
    Thread thread = new Thread(() =>
      {
          while (true)
          {
              mutex.WaitOne();//等待
              if (tickets > 0)
              {
                  Console.WriteLine($"{Thread.CurrentThread.Name}---------------->买了一张票,票号为:{tickets}");
                  tickets--;
              }
              mutex.ReleaseMutex();//解除
          }
      });
    thread.IsBackground = true;
    return thread;
}

死锁

    如果滥用线程锁,容易出现死锁的问题,什么是死锁呢?比如有两个线程T1,T2,它们共用两个同步锁L1,L2,T1先走L1,T2先走L2,T1下一步走L2,T2下一步走l1,这样这两个线程各种握着对方的下一步锁,一直阻塞最后谁也走不了。或者使用像monitor这样的锁,突然出现异常,Exit方法来不及执行,也会死锁,其它的线程也会一直阻塞。下面来演示一下:

private static Thread BuyTicket2()
{
    Thread thread = new Thread(() =>
      {
          try
          {
              while (true)
              {
                  Monitor.Enter(o);
                  throw new Exception("THREAD DEAD!");
                  if (tickets > 0)
                  {
                      Console.WriteLine($"{Thread.CurrentThread.Name}---------------->买了一张票,票号为:{tickets}");
                      tickets--;
                  }
                  Monitor.Exit(o);
              }
          }
          catch{}          
      });
    thread.IsBackground = true;
    return thread;
}

    运行结果如下:

    

    因为一开始线程就直接死锁,其它的线程无法继续执行,会一直阻塞。

 

    个人公众号:DotNet 致知,热爱分享,知识无价。

   

 

  • 6
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在编写C#代码时,有几个常见的问题需要注意: 1. 命名规范:遵循命名规范可以提高代码的可读性。使用有意义的变量、方法和类名,并遵循驼峰命名法或Pascal命名法等约定。 2. 异常处理:在适当的情况下,使用try-catch语句来捕获和处理异常。异常处理可以防止程序崩溃,并提供错误信息以供调试和修复。 3. 内存管理:确保在不再需要时及时释放资源,如关闭文件、释放数据库连接等。使用using语句可以自动管理一些资源的释放。 4. 空引用检查:避免使用空引用,可以使用条件语句或空值合并操作符(??)来检查和处理可能为空的引用。 5. 字符串处理:注意字符串的拼接和格式化,尽量使用StringBuilder类来处理大量字符串拼接,以提高性能。 6. 数据类型转换:在进行数据类型转换时,确保数据的有效性和安全性。使用类型转换方法(如Convert类或强制类型转换)时,注意可能出现的异常情况。 7. 代码注释:为了增加代码的可读性和可维护性,使用注释来解释代码的意图、算法和重要细节。 8. 安全性考虑:在处理用户输入或外部数据时,要进行有效的输入验证和防御性编程,以防止安全漏洞和恶意攻击。 9. 性能优化:在需要的情况下,进行性能优化,避免不必要的循环、重复计算和内存占用等问题。 10. 代码复用:尽量遵循面向对象编程的原则,提取可复用的代码片段并封装成类或方法,以便在不同的地方重用。 这些问题只是C#代码需要注意的一些方面,根据具体的项目和需求,可能还会有其他需要关注的问题
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值