面试常考:C#用两个线程交替打印1-100的五种方法


    “C#用两个线程交替打印1-100的五种方法”是.NET工程师面试多线程常考的试题之一,主要考察对C#语法和对多线程的熟悉程度。本文将用5种方法实现这个面试题。

方法1:使用Mutex或lock

    这种方法涉及使用Mutex或lock对象来同步两个线程。其中一个线程负责打印偶数,另一个线程负责打印奇数。线程在执行任务之前会锁定共享的Mutex或lock对象,以确保每个线程执行任务时只有一个线程能够访问共享资源。代码如下:

class Program
{
    static Mutex mutex = new Mutex();
    static int count = 1;
    static void Main(string[] args)
    {
        Thread t1 = new Thread(PrintOddNumbers);
        Thread t2 = new Thread(PrintEvenNumbers);
        t1.Start();
        t2.Start();
        t1.Join();
        t2.Join();
        Console.ReadLine();
    }
    static void PrintOddNumbers()
    {
        while (count <= 100)
        {
            mutex.WaitOne();
            if (count % 2 == 1)
            {
                Console.WriteLine("Thread 1: " + count);
                count++;
            }
            mutex.ReleaseMutex();
        }
    }
    static void PrintEvenNumbers()
    {
        while (count <= 100)
        {
            mutex.WaitOne();
            if (count % 2 == 0)
            {
                Console.WriteLine("Thread 2: " + count);
                count++;
            }
            mutex.ReleaseMutex();
        }
    }
}

方法2:使用AutoResetEvent

AutoResetEvent是一种线程同步机制,允许一个线程等待另一个线程发出信号来继续执行。其中一个线程负责打印奇数,另一个线程负责打印偶数。当一个线程完成打印任务时,它发出信号以唤醒另一个线程来继续执行。

class Program
{
    static AutoResetEvent oddEvent = new AutoResetEvent(false);
    static AutoResetEvent evenEvent = new AutoResetEvent(false);
    static int count = 1;
    static void Main(string[] args)
    {
        Thread t1 = new Thread(PrintOddNumbers);
        Thread t2 = new Thread(PrintEvenNumbers);
        t1.Start();
        t2.Start();
        t1.Join();
        t2.Join();
        Console.ReadLine();
    }

    static void PrintOddNumbers()
    {
        while (count <= 100)
        {
            if (count % 2 == 1)
            {
                Console.WriteLine("Thread 1: " + count);
                count++;
                evenEvent.Set();
                oddEvent.WaitOne();
            }
        }
    }

    static void PrintEvenNumbers()
    {
        while (count <= 100)
        {
            if (count % 2 == 0)
            {
                Console.WriteLine("Thread 2: " + count);
                count++;
                oddEvent.Set();
                evenEvent.WaitOne();
            }
        }
    }
//欢迎关注公众号“DOTNET开发跳槽”,关注可获得海量面试题

方法3:使用Monitor

Monitor是C#中的一种同步机制,类似于Mutex。其中一个线程负责打印奇数,另一个线程负责打印偶数。线程在执行任务之前会锁定共享的Monitor对象,以确保每个线程执行任务时只有一个线程能够访问共享资源。

class Program
{
    static object lockObj = new object();
    static int count = 1;
    static void Main(string[] args)
    {
        Thread t1 = new Thread(PrintOddNumbers);
        Thread t2 = new Thread(PrintEvenNumbers);
        t1.Start();
        t2.Start();
        t1.Join();
        t2.Join();
        Console.ReadLine();
    }

    static void PrintOddNumbers()
    {
        while (count <= 100)
        {
            lock (lockObj)
            {
                if (count % 2 == 1)
                {
                    Console.WriteLine("Thread 1: " + count);
                    count++;
                }
            }
        }
    }

    static void PrintEvenNumbers()
    {
        while (count <= 100)
        {
            lock (lockObj)
            {
                if (count % 2 == 0)
                {
                    Console.WriteLine("Thread 2: " + count);
                    count++;
                }
            }
        }
    }
}

方法4:使用信号量Semaphore

Semaphore是一种同步机制,允许多个线程同时访问共享资源。其中一个线程负责打印奇数,另一个线程负责打印偶数。线程在执行任务之前会等待信号量,以确保每个线程只有在获得信号量之后才能访问共享资源。

class Program
{
    static Semaphore semaphore = new Semaphore(1, 1);
    static int count = 1;
    static void Main(string[] args)
    {
        Thread t1 = new Thread(PrintOddNumbers);
        Thread t2 = new Thread(PrintEvenNumbers);
        t1.Start();
        t2.Start();
        t1.Join();
        t2.Join();

        Console.ReadLine();
    }

    static void PrintOddNumbers()
    { //注意 这里是99,否则会出现101
        while (count <= 99)
        {
            semaphore.WaitOne();
            if (count % 2 == 1)
            {
                Console.WriteLine("Thread 1: " + count);
                count++;
            }
            semaphore.Release();
        }
    }

    static void PrintEvenNumbers()
    {
        while (count <= 100)
        {
            semaphore.WaitOne();
            if (count % 2 == 0)
            {
                Console.WriteLine("Thread 2: " + count);
                count++;
            }
            semaphore.Release();
        }
    }
}

方法5:使用Task和async/await

在C#中,使用Task和async/await关键字可以轻松地在两个线程之间切换执行。其中一个线程负责打印奇数,另一个线程负责打印偶数。线程在执行任务之前使用async/await等待异步任务完成,以确保每个线程只在异步任务完成后才访问共享资源。

class Program
{
    static int count = 1;
    static void Main(string[] args)
    {
        Task.Run(PrintOddNumbers);
      // 这里改成这个也可以
      // var thread1 = new Thread(PrintOddNumbers);
        Task.Run(PrintEvenNumbers);
        Console.ReadLine();
    }
  //如果用Thread改成同步方法
    static async Task PrintOddNumbers()
    {
        while (count <= 100)
        {
            if (count % 2 == 1)
            {
                Console.WriteLine("Thread 1: " + count);
                count++;
                //如果用Thread这里改成 Thread.Sleep(1);
                await Task.Delay(1);
            }
        }
    }
    static async Task PrintEvenNumbers()
    {
        while (count <= 100)
        {
            if (count % 2 == 0)
            {
                Console.WriteLine("Thread 2: " + count);
                count++;
                await Task.Delay(1);
            }
        }
    }
//欢迎关注公众号“DOTNET开发跳槽”,关注可获得海量面试题

五种效果如下:

 

 

以上五种方法各有优缺点,没有一种方法是绝对最优的,取决于应用场景和要求。以下是对五种方法的简单比较和说明:1、使用ManualResetEventWaitHandle:这种方法在实现上较为简单,但是由于线程必须互斥地访问共享资源,因此会导致性能瓶颈。此外,使用ManualResetEventWaitHandle需要频繁调用WaitOne和Set方法,可能会降低应用程序的响应能力。2、使用AutoResetEventWaitHandle:这种方法在实现上比较简单,而且使用AutoResetEventWaitHandle可以避免性能瓶颈问题。然而,它仍然需要频繁调用WaitOne和Set方法,可能会降低应用程序的响应能力。3、使用锁:使用锁可以避免性能瓶颈问题,因为同一时间只有一个线程可以访问共享资源。但是,锁可能会导致线程死锁和性能下降的问题,因此需要小心使用。4、使用信号量Semaphore:这种方法可以避免性能瓶颈问题,并允许多个线程同时访问共享资源。Semaphore还可以设置多个许可证,以控制并发线程的数量。然而,使用Semaphore可能会使代码变得更加复杂,因此需要小心使用。5、使用Task和async/await:这种方法可以避免性能瓶颈问题,并且使用Task和async/await可以使代码更加简洁易懂。但是,它可能会对内存和CPU产生额外的开销,因为需要在任务之间频繁地切换上下文。综上所述,选择哪种方法取决于应用程序的要求和程序员的个人偏好。如果应用程序需要更好的性能,则应该使用锁或信号量;如果应用程序需要更简洁易懂的代码,则应该使用Task和async/await。

考察的知识点

1、C#编程语言和语法:实现多线程程序需要熟悉C#编程语言和语法,包括线程的创建和管理、共享资源的访问和同步等方面的知识。2、多线程编程:多线程编程是指同时运行多个线程的编程模型,它可以提高应用程序的性能和响应能力。多线程编程需要考虑线程的同步、共享资源的访问、线程间的通信等问题。3、线程同步机制:线程同步机制是指用于控制多个线程访问共享资源的机制,常用的线程同步机制包括锁、信号量、事件等。4、异步编程:异步编程是指不阻塞线程并且在完成任务后通知线程的编程模型,它可以提高应用程序的响应能力和性能。异步编程需要熟悉异步和await关键字、Task和Task<T>等类型、async和await方法等概念。参考:chatGPT