当前位置: 首页 > article >正文

C# Sleep() vs Wait():到底用哪个?

一、引言

嘿,各位 C# 编程的小伙伴们!在多线程编程的世界里,我们常常会遇到需要让线程暂时 “休息” 一下的情况。这时候,Sleep()和Wait()这两个方法就如同我们的得力助手,随时准备为我们服务。但是,你真的了解它们吗?它们之间又有着怎样的区别呢?今天,就让我们一起深入探讨C#中Sleep()和Wait()的奥秘,看看在不同的场景下,究竟该如何选择,才能让我们的线程 “休息” 得恰到好处。

二、理论基础

2.1 Sleep () 原理剖析

Sleep()是System.Threading.Thread类的一个静态方法 ,它的主要作用是让当前正在执行的线程暂停一段时间。其暂停的时长由传入的参数决定,单位为毫秒。例如,当我们调用Thread.Sleep(2000)时,当前线程就会暂停 2 秒钟。在这 2 秒内,该线程不会执行任何代码,CPU 也不会分配时间片给它。需要注意的是,Sleep()方法并不会释放当前线程所持有的锁。假设我们有一个多线程程序,其中一个线程在获取了某个对象的锁之后调用了Sleep()方法,那么在它睡眠的这段时间里,其他线程是无法获取该锁的,即便它们尝试访问被该锁保护的资源,也只能处于等待状态。

2.2 Wait () 原理详解

Wait()是Object类的实例方法,它用于线程间的通信和协作。通常情况下,Wait()方法需要在同步代码块(lock语句块)或同步方法中使用,并且它依赖于一个对象锁。当一个线程调用Wait()方法后,它会释放当前持有的对象锁,并进入等待状态。此时,该线程会被放入到对象的等待队列中,直到其他线程调用了同一个对象的Notify()或NotifyAll()方法,才有可能被唤醒。例如,在一个生产者 - 消费者模型中,当消费者线程发现队列中没有数据时,可以调用Wait()方法进入等待状态,同时释放锁,让生产者线程能够将数据放入队列。当生产者线程完成数据生产后,调用Notify()或NotifyAll()方法唤醒等待的消费者线程。

三、深入对比

3.1 基本用法差异

Sleep()方法的使用非常简单直接,只需在需要暂停线程的地方调用Thread.Sleep(毫秒数) 即可。比如,我们想要让当前线程暂停 3 秒钟,可以这样写:

using System;
using System.Threading;

class Program
{
    static void Main()
    {
        Console.WriteLine("线程开始执行");
        Thread.Sleep(3000); 
        Console.WriteLine("线程暂停3秒后继续执行");
    }
}

在上述代码中,当执行到Thread.Sleep(3000)时,当前线程会暂停 3000 毫秒(即 3 秒),然后再继续执行后面的代码。

而Wait()方法的使用则相对复杂一些,它需要与lock语句配合使用,并且通常在一个循环中调用,以确保在合适的条件下等待。例如:

using System;
using System.Threading;

class Program
{
    private static object _lockObject = new object();
    private static bool _flag = false;

    static void Main()
    {
        new Thread(() =>
        {
            lock (_lockObject)
            {
                while (!_flag)
                {
                    Console.WriteLine("等待条件满足...");
                    Monitor.Wait(_lockObject); 
                }
                Console.WriteLine("条件满足,继续执行");
            }
        }).Start();

        new Thread(() =>
        {
            Thread.Sleep(2000); 
            lock (_lockObject)
            {
                _flag = true;
                Console.WriteLine("设置条件为真,并通知等待线程");
                Monitor.Pulse(_lockObject); 
            }
        }).Start();
    }
}

在这段代码中,第一个线程在获取锁后,检查_flag是否为true,如果不是则调用Monitor.Wait(_lockObject)进入等待状态,并释放锁。第二个线程在 2 秒后获取锁,设置_flag为true,然后调用Monitor.Pulse(_lockObject)通知等待的线程。被通知的线程重新获取锁后,继续执行后续代码。

3.2 锁处理的不同

Sleep()方法在暂停线程时,不会释放当前线程所持有的锁。这意味着,如果一个线程在持有锁的情况下调用了Sleep(),其他线程想要获取该锁就必须等待该线程睡眠结束并释放锁。假设有一个银行转账的场景,我们用代码模拟如下:

using System;
using System.Threading;

class BankAccount
{
    private int balance = 1000;
    private object lockObject = new object();

    public void Withdraw(int amount)
    {
        lock (lockObject)
        {
            Console.WriteLine("开始取款操作,当前余额: " + balance);
            if (amount <= balance)
            {
                Thread.Sleep(2000); 
                balance -= amount;
                Console.WriteLine("取款成功,剩余余额: " + balance);
            }
            else
            {
                Console.WriteLine("余额不足,取款失败");
            }
        }
    }
}

class Program
{
    static void Main()
    {
        BankAccount account = new BankAccount();
        new Thread(() => account.Withdraw(500)).Start();
        new Thread(() => account.Withdraw(300)).Start();
    }
}

在这个例子中,Withdraw方法使用了锁来保证线程安全。当一个线程进入Withdraw方法并获取锁后,调用Thread.Sleep(2000)模拟业务处理的耗时操作,在这 2 秒内,锁不会被释放,其他线程无法进入该方法,只能等待。

与之相反,Wait()方法在调用时会释放当前线程持有的锁,使得其他线程有机会获取该锁并执行同步代码块中的内容。当等待的线程被唤醒时,它会重新尝试获取锁,只有获取到锁后才会继续执行。下面是一个简单的生产者 - 消费者模型示例:

using System;
using System.Collections.Generic;
using System.Threading;

class ProducerConsumer
{
    private Queue<int> queue = new Queue<int>();
    private object lockObject = new object();
    private const int MaxQueueSize = 5;

    public void Produce(int item)
    {
        lock (lockObject)
        {
            while (queue.Count >= MaxQueueSize)
            {
                Console.WriteLine("队列已满,生产者等待");
                Monitor.Wait(lockObject); 
            }
            queue.Enqueue(item);
            Console.WriteLine("生产了: " + item);
            Monitor.PulseAll(lockObject); 
        }
    }

    public void Consume()
    {
        lock (lockObject)
        {
            while (queue.Count == 0)
            {
                Console.WriteLine("队列已空,消费者等待");
                Monitor.Wait(lockObject); 
            }
            int item = queue.Dequeue();
            Console.WriteLine("消费了: " + item);
            Monitor.PulseAll(lockObject); 
        }
    }
}

class Program
{
    static void Main()
    {
        ProducerConsumer pc = new ProducerConsumer();
        new Thread(() =>
        {
            for (int i = 1; i <= 10; i++)
            {
                pc.Produce(i);
                Thread.Sleep(1000); 
            }
        }).Start();

        new Thread(() =>
        {
            for (int i = 1; i <= 10; i++)
            {
                pc.Consume();
                Thread.Sleep(1500); 
            }
        }).Start();
    }
}

在这个例子中,当队列满时,生产者线程调用Monitor.Wait(lockObject)释放锁并进入等待状态;当队列空时,消费者线程调用Monitor.Wait(lockObject)释放锁并进入等待状态。当生产者生产了数据或者消费者消费了数据后,会调用Monitor.PulseAll(lockObject)通知等待的线程。

3.3 线程状态变化

当线程调用Sleep()方法后,会进入阻塞状态(Blocked),在指定的时间内,该线程不会参与 CPU 的调度,也不会执行任何代码。当睡眠的时间结束后,线程会从阻塞状态转变为就绪状态(Ready),等待 CPU 分配时间片来继续执行。例如,在一个游戏开发中,我们需要控制某个动画的播放速度,假设每帧动画需要暂停 50 毫秒,代码如下:

using System;
using System.Threading;

class Animation
{
    public void Play()
    {
        for (int i = 0; i < 100; i++)
        {
            Console.WriteLine("播放第 " + (i + 1) + " 帧动画");
            Thread.Sleep(50); 
        }
    }
}

class Program
{
    static void Main()
    {
        Animation animation = new Animation();
        new Thread(animation.Play).Start();
    }
}

在这个例子中,每播放一帧动画,线程就会调用Thread.Sleep(50)进入阻塞状态 50 毫秒,50 毫秒后进入就绪状态,等待 CPU 分配时间片继续播放下一帧。

而线程调用Wait()方法后,会进入等待状态(Waiting),并且释放持有的锁。此时,该线程会被放入对象的等待队列中,直到被其他线程通过Notify()或NotifyAll()方法唤醒。唤醒后,线程会从等待状态转变为就绪状态,竞争锁资源,获取到锁后才会继续执行。例如,在一个多线程协作的文件处理系统中,假设有一个主线程负责读取文件内容,一个辅助线程负责对读取的内容进行解析,代码如下:

using System;
using System.IO;
using System.Threading;

class FileProcessor
{
    private object lockObject = new object();
    private string fileContent;
    private bool isContentReady = false;

    public void ReadFile(string filePath)
    {
        lock (lockObject)
        {
            using (StreamReader reader = new StreamReader(filePath))
            {
                fileContent = reader.ReadToEnd();
            }
            isContentReady = true;
            Console.WriteLine("文件读取完成,通知解析线程");
            Monitor.Pulse(lockObject); 
        }
    }

    public void ParseContent()
    {
        lock (lockObject)
        {
            while (!isContentReady)
            {
                Console.WriteLine("等待文件内容读取完成");
                Monitor.Wait(lockObject); 
            }
            Console.WriteLine("开始解析文件内容: " + fileContent);
        }
    }
}

class Program
{
    static void Main()
    {
        FileProcessor processor = new FileProcessor();
        new Thread(() => processor.ReadFile("test.txt")).Start();
        new Thread(processor.ParseContent).Start();
    }
}

在这个例子中,解析线程调用Monitor.Wait(lockObject)进入等待状态,当读取线程读取完文件内容后,调用Monitor.Pulse(lockObject)唤醒解析线程,解析线程从等待状态变为就绪状态,竞争锁资源,获取到锁后开始解析文件内容。

3.4 响应中断能力

Sleep()方法在执行期间,线程处于阻塞状态,它不会响应中断请求。如果在一个线程调用Sleep()期间,另一个线程对其发出中断请求,该线程会忽略这个中断信号,继续睡眠直到指定的时间结束。例如,我们有一个定时任务线程,每隔一段时间执行一次任务,在任务执行过程中,不希望被中断干扰,代码如下:

using System;
using System.Threading;

class ScheduledTask
{
    public void Run()
    {
        while (true)
        {
            Console.WriteLine("开始执行定时任务");
            Thread.Sleep(5000); 
            Console.WriteLine("定时任务执行完成");
        }
    }
}

class Program
{
    static void Main()
    {
        Thread taskThread = new Thread(new ScheduledTask().Run);
        taskThread.Start();

        Thread.Sleep(3000); 
        taskThread.Interrupt(); 
        Console.WriteLine("尝试中断定时任务线程");
    }
}

在这个例子中,定时任务线程调用Thread.Sleep(5000)进入睡眠状态,3 秒后主线程尝试中断它,但定时任务线程会忽略中断请求,继续睡眠直到 5 秒结束。

相比之下,Wait()方法可以响应中断请求。当一个线程在等待状态时,如果被其他线程中断,它会抛出InterruptedException异常,从而可以在捕获异常时进行相应的处理。例如,在一个多线程的网络通信程序中,一个线程在等待服务器响应时,可能需要在某些情况下提前中断等待,代码如下:

using System;
using System.Threading;

class NetworkClient
{
    private object lockObject = new object();
    private bool isResponseReceived = false;

    public void WaitForResponse()
    {
        lock (lockObject)
        {
            try
            {
                while (!isResponseReceived)
                {
                    Console.WriteLine("等待服务器响应");
                    Monitor.Wait(lockObject); 
                }
                Console.WriteLine("收到服务器响应");
            }
            catch (InterruptedException e)
            {
                Console.WriteLine("等待被中断: " + e.Message);
            }
        }
    }

    public void SimulateResponse()
    {
        lock (lockObject)
        {
            isResponseReceived = true;
            Console.WriteLine("模拟服务器响应,通知等待线程");
            Monitor.Pulse(lockObject); 
        }
    }
}

class Program
{
    static void Main()
    {
        NetworkClient client = new NetworkClient();
        Thread waitThread = new Thread(client.WaitForResponse);
        waitThread.Start();

        Thread.Sleep(2000); 
        waitThread.Interrupt(); 
        Console.WriteLine("中断等待线程");
    }
}

在这个例子中,等待线程在调用Monitor.Wait(lockObject)进入等待状态后,2 秒后被主线程中断,会捕获InterruptedException异常并进行相应的处理。

四、适用场景

4.1 Sleep () 适用场景

  • 定时任务执行:在需要定时执行某项任务的场景中,Sleep()方法能发挥重要作用。例如,一个监控系统需要每隔一段时间(如 5 分钟)检查一次服务器的状态,我们可以使用Sleep()方法来控制检查的时间间隔。示例代码如下:
using System;
using System.Threading;

class ServerMonitor
{
    public void StartMonitoring()
    {
        while (true)
        {
            Console.WriteLine("开始检查服务器状态");
            // 模拟检查服务器状态的操作
            // ......
            Thread.Sleep(5 * 60 * 1000); 
        }
    }
}

class Program
{
    static void Main()
    {
        ServerMonitor monitor = new ServerMonitor();
        new Thread(monitor.StartMonitoring).Start();
    }
}

在这段代码中,Thread.Sleep(5 * 60 * 1000)使得线程每 5 分钟执行一次服务器状态检查操作。

  • 模拟延迟效果:在一些需要模拟延迟的场景,如网络请求延迟、动画效果延迟等,Sleep()方法可以简单地实现延迟功能。比如,在一个简单的登录验证功能中,为了模拟网络延迟,我们可以在验证逻辑执行后添加一个短暂的延迟,让用户感受到更真实的网络请求过程。示例代码如下:
using System;
using System.Threading;

class LoginSystem
{
    public void Login(string username, string password)
    {
        Console.WriteLine("正在验证用户名和密码...");
        // 模拟验证逻辑
        bool isValid = ValidateCredentials(username, password);
        if (isValid)
        {
            Thread.Sleep(2000); 
            Console.WriteLine("登录成功");
        }
        else
        {
            Thread.Sleep(2000); 
            Console.WriteLine("用户名或密码错误");
        }
    }

    private bool ValidateCredentials(string username, string password)
    {
        // 简单的验证逻辑示例
        return username == "admin" && password == "123456";
    }
}

class Program
{
    static void Main()
    {
        LoginSystem loginSystem = new LoginSystem();
        loginSystem.Login("admin", "123456");
    }
}

在上述代码中,无论登录成功与否,都会通过Thread.Sleep(2000)模拟 2 秒的延迟,增强用户体验。

4.2 Wait () 适用场景

  • 生产者 - 消费者模型:这是Wait()方法最典型的应用场景。在该模型中,生产者线程负责生产数据并将其放入共享队列中,消费者线程则从队列中取出数据进行处理。当队列已满时,生产者线程需要等待;当队列已空时,消费者线程需要等待。通过Wait()和Notify()(或NotifyAll())方法,生产者和消费者线程可以实现高效的协作。例如,我们有一个生产包子和消费包子的场景,代码如下:
using System;
using System.Collections.Generic;
using System.Threading;

class BaoziShop
{
    private Queue<string> baoziQueue = new Queue<string>();
    private object lockObject = new object();
    private const int MaxQueueSize = 10;

    public void ProduceBaozi()
    {
        while (true)
        {
            lock (lockObject)
            {
                while (baoziQueue.Count >= MaxQueueSize)
                {
                    Console.WriteLine("包子队列已满,生产者等待");
                    Monitor.Wait(lockObject); 
                }
                string baozi = "新生产的包子";
                baoziQueue.Enqueue(baozi);
                Console.WriteLine("生产了一个包子,当前队列中有 " + baoziQueue.Count + " 个包子");
                Monitor.PulseAll(lockObject); 
            }
        }
    }

    public void ConsumeBaozi()
    {
        while (true)
        {
            lock (lockObject)
            {
                while (baoziQueue.Count == 0)
                {
                    Console.WriteLine("包子队列已空,消费者等待");
                    Monitor.Wait(lockObject); 
                }
                string baozi = baoziQueue.Dequeue();
                Console.WriteLine("消费了一个包子,当前队列中有 " + baoziQueue.Count + " 个包子");
                Monitor.PulseAll(lockObject); 
            }
        }
    }
}

class Program
{
    static void Main()
    {
        BaoziShop shop = new BaoziShop();
        new Thread(shop.ProduceBaozi).Start();
        new Thread(shop.ConsumeBaozi).Start();
    }
}

在这个例子中,当包子队列满时,生产者线程调用Monitor.Wait(lockObject)进入等待状态并释放锁;当包子队列空时,消费者线程调用Monitor.Wait(lockObject)进入等待状态并释放锁。当生产者生产了包子或者消费者消费了包子后,会调用Monitor.PulseAll(lockObject)通知等待的线程。

  • 线程间资源竞争与协作:在多个线程需要访问共享资源,并且需要根据资源的状态进行协作的场景中,Wait()方法能很好地协调线程间的执行顺序。例如,在一个多线程的文件读取和处理系统中,有一个线程负责读取文件内容,另一个线程负责对读取的内容进行解析。当读取线程还未完成读取时,解析线程需要等待;当读取线程完成读取后,通知解析线程开始工作。示例代码如下:
using System;
using System.IO;
using System.Threading;

class FileHandler
{
    private object lockObject = new object();
    private string fileContent;
    private bool isContentReady = false;

    public void ReadFile(string filePath)
    {
        lock (lockObject)
        {
            using (StreamReader reader = new StreamReader(filePath))
            {
                fileContent = reader.ReadToEnd();
            }
            isContentReady = true;
            Console.WriteLine("文件读取完成,通知解析线程");
            Monitor.Pulse(lockObject); 
        }
    }

    public void ParseContent()
    {
        lock (lockObject)
        {
            while (!isContentReady)
            {
                Console.WriteLine("等待文件内容读取完成");
                Monitor.Wait(lockObject); 
            }
            Console.WriteLine("开始解析文件内容: " + fileContent);
        }
    }
}

class Program
{
    static void Main()
    {
        FileHandler handler = new FileHandler();
        new Thread(() => handler.ReadFile("test.txt")).Start();
        new Thread(handler.ParseContent).Start();
    }
}

在这段代码中,解析线程调用Monitor.Wait(lockObject)进入等待状态,直到读取线程完成文件读取并调用Monitor.Pulse(lockObject)通知它。

五、注意事项

5.1 Sleep () 使用注意

在使用Sleep()方法时,需要注意避免因设置过长的睡眠时间导致程序假死或响应迟缓。例如,在一个图形用户界面(GUI)应用程序中,如果主线程调用了Sleep()方法且睡眠时间较长,那么界面将无法响应用户的操作,如点击按钮、拖动窗口等,严重影响用户体验 。

using System;
using System.Threading;
using System.Windows.Forms;

namespace SleepInGUI
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            // 错误示范:在主线程中长时间Sleep
            Thread.Sleep(10000); 
            MessageBox.Show("睡眠结束");
        }
    }
}

在上述代码中,当用户点击按钮时,主线程会进入 10 秒的睡眠状态,在此期间,整个应用程序的界面将处于无响应状态。为了避免这种情况,可以考虑将耗时的操作放在新的线程中执行,而不是在主线程中调用Sleep()。

5.2 Wait () 使用注意

  • 锁的正确管理:使用Wait()方法时,必须确保正确地获取和释放锁。如果在调用Wait()之前没有获取锁,或者在不适当的地方释放锁,都可能导致程序出现不可预测的结果,甚至死锁。例如:
using System;
using System.Threading;

class DeadlockExample
{
    private static object lockObject1 = new object();
    private static object lockObject2 = new object();

    public static void Thread1()
    {
        lock (lockObject1)
        {
            Console.WriteLine("Thread1 获取了 lockObject1");
            // 错误示范:在未获取lockObject2的情况下尝试等待
            Monitor.Wait(lockObject2); 
        }
    }

    public static void Thread2()
    {
        lock (lockObject2)
        {
            Console.WriteLine("Thread2 获取了 lockObject2");
            // 错误示范:在未获取lockObject1的情况下尝试等待
            Monitor.Wait(lockObject1); 
        }
    }
}

class Program
{
    static void Main()
    {
        new Thread(DeadlockExample.Thread1).Start();
        new Thread(DeadlockExample.Thread2).Start();
    }
}

在这段代码中,Thread1和Thread2分别获取了不同的锁,然后尝试在未获取的锁上调用Wait(),这将导致死锁。正确的做法是确保在调用Wait()之前,线程已经获取了对应的锁。

  • 避免死锁:除了正确管理锁之外,还需要注意避免因线程间的循环等待而导致死锁。在复杂的多线程场景中,多个线程可能会相互依赖,形成循环等待的情况。例如,假设有三个线程A、B、C,它们分别持有锁lockA、lockB、lockC,并且A等待B释放lockB,B等待C释放lockC,C等待A释放lockA,这样就会形成死锁。为了避免这种情况,需要仔细设计线程的同步逻辑,确保不会出现循环等待的情况。

  • 处理线程唤醒后的竞争问题:当一个线程被Notify()或NotifyAll()唤醒后,它会与其他线程竞争锁资源。在高并发的情况下,可能会出现竞争激烈的情况,导致性能下降。为了缓解这种情况,可以考虑使用一些并发控制的技巧,如信号量(Semaphore)、互斥锁(Mutex)等,来控制线程的访问顺序和并发度。例如,使用信号量来限制同时访问某个资源的线程数量:

using System;
using System.Threading;

class ResourceAccess
{
    private static Semaphore semaphore = new Semaphore(2, 2); 
    private static object lockObject = new object();

    public static void AccessResource()
    {
        semaphore.WaitOne(); 
        lock (lockObject)
        {
            try
            {
                Console.WriteLine("线程进入资源访问区");
                Thread.Sleep(2000); 
                Console.WriteLine("线程完成资源访问");
            }
            finally
            {
                semaphore.Release(); 
            }
        }
    }
}

class Program
{
    static void Main()
    {
        for (int i = 0; i < 5; i++)
        {
            new Thread(ResourceAccess.AccessResource).Start();
        }
    }
}

在这个例子中,Semaphore被初始化为允许最多 2 个线程同时访问资源,通过WaitOne()和Release()方法来控制线程的进入和离开,从而避免了过多线程同时竞争资源的情况。

六、总结

在 C# 多线程编程领域,Sleep()和Wait()作为控制线程执行流程的重要手段,各自有着独特的功能和适用场景。Sleep()简单直接,适用于需要线程暂停固定时长的场景,像定时任务、模拟延迟效果等,其不会释放锁的特性,在特定业务逻辑中能保证数据的一致性和操作的原子性。而Wait()则更侧重于线程间的协作与同步,通过释放锁,让多个线程能高效地访问共享资源,在生产者 - 消费者模型以及线程间资源竞争与协作场景中发挥着关键作用。

理解这两个方法的核心区别,并根据实际需求合理运用,是编写高效、稳定多线程程序的关键。希望通过本文的探讨,能帮助大家在多线程编程的道路上更加得心应手,编写出更健壮、更具扩展性的代码。


http://www.kler.cn/a/507804.html

相关文章:

  • Swift语言的多线程编程
  • Python编程与在线医疗平台数据挖掘与数据应用交互性研究
  • JavaScript,ES6,模块化,大程序文件拆分成小文件再组合起来
  • vue3学习三
  • 网络安全 | 防护技术与策略
  • 客户案例 | Ansys与索尼半导体解决方案公司合作推进自动驾驶汽车基于场景的感知测试
  • C# 获取PDF文档中的字体信息(字体名、大小、颜色、样式等
  • Android系统定制APP开发_如何对应用进行系统签名
  • Android 北斗与平台芯片相关
  • PLC(电力载波通信)网络机制介绍
  • Qt——QTableWidget 限制单元格输入范围的方法(正则表达式输入校验法、自定义代理类MyItemDelegrate)
  • Go语言strings包与字符串操作:从基础到高级的全面解析
  • C#深度神经网络(TensorFlow.NET)
  • MongoDB中游标的使用
  • 2019-Android-高级面试题总结-从java语言到AIDL使用与原理
  • ctfshow复现2024ciscn第一场web
  • Leetcode 91. 解码方法 动态规划
  • DATACOM-STP、RSTP、MSTP-复习-实验
  • 简历_使用优化的Redis自增ID策略生成分布式环境下全局唯一ID,用于用户上传数据的命名以及多种ID的生成
  • 【Python】Selenium根据网页页面长度,模拟向下滚动鼠标,直到网页底部的操作