.NET/C#⾯试题汇总系列:多线程

1.根据线程安全的相关知识,分析以下代码,当调⽤test⽅法时i>10时是否会引起死锁?并简 要说明理由。

public void test(int i)
{
    lock(this)
    {
        if (i>10)
        {
            i--;
            test(i);
        }
    }
}

不会发⽣死锁,(但有⼀点int是按值传递的,所以每次改变的都只是⼀个副本,因此不会出现死锁。但如 果把int换做⼀个object,那么死锁会发⽣)

lock 关键字:

用于确保当多个线程同时访问共享资源时,不会发生数据竞争(race condition)或数据不一致的问题。

lock 语句通过获取给定对象的互斥锁来同步代码块,以确保一次只有一个线程可以执行该代码块。

  • 示例 : 线程安全的集合操作
public class ThreadSafeList<T>
{
    private readonly List<T> _list = new List<T>();
    private readonly object _lockObject = new object();

    public void Add(T item)
    {
        lock (_lockObject)
        {
            _list.Add(item);
        }
    }

    public void Remove(T item)
    {
        lock (_lockObject)
        {
            _list.Remove(item);
        }
    }

    public List<T> GetAll()
    {
        lock (_lockObject)
        {
            return new List<T>(_list);
        }
    }
}

在这个例子中,我们创建了一个线程安全的列表类 ThreadSafeList<T>,其中 Add 和 Remove 方法使用 lock 来确保对列表的修改是线程安全的。GetAll 方法也使用 lock 来确保在返回列表的副本时,列表的状态是一致的。

2.描述线程与进程的区别?

  1. 定义与区别‌:

    • 进程‌(Process)是系统进行资源分配和调度的一个独立单元,是操作系统结构的基础。它是应用程序的一次动态执行过程,是程序代码及其数据在运行时的一个实例。它拥有独立的内存空间,包含一组系统资源(如代码、数据和打开的文件等)。
    • 线程‌(Thread)是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的独立运行的单位。一个进程可以拥有多个线程,这些线程共享该进程的所有资源(如内存和打开的文件),但每个线程都拥有自己独立的执行栈和程序计数器。
  2. 资源与开销‌:

    • 进程拥有独立的内存空间和系统资源,因此进程间的通信相对复杂,开销也较大。
    • 线程共享进程的资源,因此线程间的通信相对简单,开销也较小。
  3. 独立性‌:

    • 进程拥有更高的独立性,一个进程的崩溃通常不会影响到其他进程。
    • 线程则不然,一个线程的崩溃可能会导致整个进程的崩溃(取决于线程的异常处理机制)。

3.Windows单个进程所能访问的最⼤内存量是多少?它与系统的最⼤虚拟内存⼀样吗?这对 于系统设计有什么影响?

这个需要针对硬件平台,公式为单个进程能访问的最⼤内存量=2的处理器位数次⽅/2,⽐如通常情况下, 32位处理器下,单个进程所能访问的最⼤内存量为:232 /2 = 2G 。

单个进程能访问的最⼤内存量是最⼤ 虚拟内存的1/2,因为要分配给操作系统⼀半虚拟内存。

4.using() 语法有⽤吗?什么是IDisposable?

有⽤

实现了IDisposiable的类在using中创建,using结束后会⾃定调⽤该对象的Dispose⽅法,释放资 源。

  • using() 语法的用途

using 语句在C#中非常有用,特别是在处理实现了IDisposable接口的对象时。它确保了在代码块执行完毕后,会自动调用对象的Dispose方法,以释放非托管资源(如文件句柄、数据库连接等)。这不仅简化了资源管理,还避免了资源泄露的风险。

  • 什么是IDisposable

IDisposable是.NET中定义的一个接口,它包含一个Dispose方法。当一个类实现了IDisposable接口时,就意味着这个类拥有非托管资源,需要在不再需要时显式释放。Dispose方法通常用于释放这些非托管资源,并可能还包含对托管资源的清理逻辑(如关闭文件流、数据库连接等)。

通过使用using语句,我们可以确保即使发生异常,Dispose方法也会被调用,从而安全地释放资源。

例如:

using (var fileStream = new FileStream("example.txt", FileMode.OpenOrCreate))
{
    // 使用 fileStream 进行操作
}
// fileStream 的 Dispose 方法会在此处自动调用

FileStream类实现了IDisposable接口,因此我们可以使用using语句来确保fileStream在使用完毕后会被正确关闭和释放资源。

5.前台线程和后台线程有什么区别?

  • 前台线程(Foreground Thread)‌:在.NET中,默认创建的线程都是前台线程。只要应用程序中有任何前台线程在运行,应用程序就会保持运行状态。只有当所有的前台线程都终止时,应用程序才会结束。

  • 后台线程(Background Thread)‌:后台线程不会影响应用程序的终止。一旦所有的前台线程都终止,无论后台线程是否还在运行,应用程序都会自动结束。后台线程通常用于处理那些非关键的、可以异步执行的任务比如数据加载文件操作等。

6.什么是互斥?

当多个线程访问同⼀个全局变量,或者同⼀个资源(⽐如打印机)的时候,需要进⾏线程间的互斥操作来保证 访问的安全性。

7.如何查看和设置线程池的上下限?

  • 线程池的线程数是有限制的,通常情况下,我们⽆需修改默认的配置。但在⼀些场合,我们可能需要了解 线程池的上下限和剩余的线程数。线程池作为⼀个缓冲池,有着其上下限。在通常情况下,当线程池中的 线程数⼩于线程池设置的下限时,线程池会设法创建新的线程,⽽当线程池中的线程数⼤于线程池设置的 上限时,线程池将销毁多余的线程。
  • PS:在.NET Framework 4.0中,每个CPU默认的⼯作者线程数量最⼤值为250个,最⼩值为2个。⽽IO 线程的默认最⼤值为1000个,最⼩值为2个。

在.NET中,通过 ThreadPool 类型提供的5个静态⽅法可以获取和设置线程池的上限和下限,同时它还额 外地提供了⼀个⽅法来让程序员获知当前可⽤的线程数量,

下⾯是这五个⽅法的签名:

① static void GetMaxThreads(out int workerThreads, out int completionPortThreads)

② static void GetMinThreads(out int workerThreads, out int completionPortThreads)

③ static bool SetMaxThreads(int workerThreads, int completionPortThreads)

④ static bool SetMinThreads(int workerThreads, int completionPortThreads)

⑤ static void GetAvailableThreads(out int workerThreads, out int completionPortThreads)

8. Task状态机的实现和⼯作机制是什么?

在.NET中,它会⾃动编译为:

  • 1. 将所有引⽤的局部变量做成 闭包,放到⼀个隐藏的状态机的类中;
  • 2. 将所有的await展开成⼀个状态号,有⼏个await就有⼏个状态 号;
  • 3. 每次执⾏完⼀个状态,都重复回调状态机的MoveNext⽅法,同时指定下⼀个状态号;
  • 4. MoveNext⽅法还需处理线程和异常等问题。

9.await的作⽤和原理,并说明和GetResult()有什么区别?

  • 从状态机的⻆度出发,await的本质是调⽤Task.GetAwaiter()的UnsafeOnCompleted(Action)回调,并 指定下⼀个状态号。
  • 从多线程的⻆度出发,如果await的Task需要在新的线程上执⾏,该状态机的MoveNext()⽅法会⽴即返 回,此时,主线程被释放出来了,然后在UnsafeOnCompleted回调的action指定的线程上下⽂中继续 MoveNext()和下⼀个状态的代码。
  • ⽽相⽐之下,GetResult()就是在当前线程上⽴即等待Task的完成,在Task完成前,当前线程不会释放。 注意:Task也可能不⼀定在新的线程上执⾏,此时⽤GetResult()或者await就只有会不会创建状态机的区 别了。

10.Task和Thread有区别吗?

  1. 抽象层次‌:Task 提供了比 Thread 更高的抽象层次。它代表了异步操作,可以很容易地与C#的异步编程模式(async/await)集成。Thread 则更底层,直接表示操作系统线程。

  2. 资源管理‌:Task 池管理了任务的执行,可以更有效地重用线程,减少线程创建和销毁的开销。相比之下,直接使用 Thread 需要程序员手动管理线程的创建、执行和销毁。

  3. 异步编程支持‌:Task 天然支持C#的异步编程模式,使得编写异步代码更加简单和直观。而 Thread 需要通过额外的机制(如回调函数、轮询等)来实现类似的功能。

11.多线程有什么⽤?

  1. 提高CPU利用率:多线程使得在等待某些任务完成时,如I/O操作或用户输入,CPU可以转而执行其他线程的任务,从而大大提高了CPU的利用率。
  2. 提升系统性能:通过并行处理多个任务,多线程可以显著提高系统的处理能力和性能,尤其是在多核处理器系统中更为明显。
  3. 改善用户体验:在图形用户界面(GUI)应用中,多线程可以实现界面的响应式设计,即使后台正在进行耗时的操作,用户界面也能保持活跃,提升用户体验。
  4. 简化资源共享:同一进程下的所有线程共享进程的资源,如内存空间、文件句柄等,这使得不同任务之间的协调操作与数据交互更加简单高效。
  5. 实现复杂任务:对于需要同时处理多个独立但相关任务的场景,多线程提供了一个有效的解决方案。例如,在服务器端并发处理用户请求时,每个请求可以在一个独立的线程中处理,互不干扰,提高了服务的响应速度和效率。
  6. 优化资源分配:在多线程编程中,可以通过线程同步机制来控制对共享资源的访问,防止数据竞争和死锁的发生,确保系统的稳定性和可靠性。
  7. 增强程序灵活性:多线程技术使得开发者能够更灵活地安排程序的执行流程,通过创建和管理不同的线程,可以有效地应对各种复杂的程序需求。
  8. 支持实时系统:在需要快速响应外部事件的应用中,如实时数据处理或游戏开发,多线程技术可以提供必要的并发执行能力,以满足严格的时间要求。

12. 两个线程交替打印0~100的奇偶数

这道题就是说有两个线程,⼀个名为偶数线程,⼀个名为奇数线程,偶数线程只打印偶数,奇数线程只打 印奇数,两个线程按顺序交替打印。

static AutoResetEvent oddReady = new AutoResetEvent(false);
static AutoResetEvent evenReady = new AutoResetEvent(true);//偶数线程先开始
static void Main(string[] args)
{
    Thread oddThread = new Thread(PrintOddNumbers);
    Thread evenThread = new Thread(PrintEvenNumbers);

    oddThread.Start();
    evenThread.Start();

    oddThread.Join();
    evenThread.Join();

    Console.WriteLine("打印完成。");
}
static void PrintOddNumbers()
{
    for (int i = 1; i <= 10; i+=2) { 
        evenReady.WaitOne();// 等待偶数线程完成
        Console.WriteLine(i);
        oddReady.Set(); // 通知奇数线程可以运行
    }
}
static void PrintEvenNumbers()
{
    for (int i = 0; i <= 100; i += 2)
    {
        oddReady.WaitOne(); // 等待奇数线程完成
        Console.WriteLine(i);
        evenReady.Set(); // 通知偶数线程可以运行
    }
}

AutoResetEvent 是 C# 中的一个类,用于实现线程同步。它属于 System.Threading 命名空间。

AutoResetEvent 有两种状态:有信号(true)和无信号(false)。

当一个线程调用 WaitOne() 方法时,如果事件处于无信号状态,那么该线程将被阻塞,直到另一个线程调用 Set() 方法将事件设置为有信号状态

一旦事件被设置为有信号状态,WaitOne() 方法将返回,并且事件将自动重置为无信号状态。

13.为什么GUI不⽀持跨线程调⽤?有什么解决⽅法?

因为GUI应⽤程序引⼊了⼀个特殊的线程处理模型,为了保证UI控件的线程安全,这个线程处理模型不允许 其他⼦线程跨线程访问UI元素。

解决⽅法⽐较多的

  • 利⽤UI控件提供的⽅法,Winform是控件的Invoke⽅法,WPF中是控件的Dispatcher.Invoke⽅法;
  • 使⽤BackgroundWorker;
  • 使⽤GUI线程处理模型的同步上下⽂SynchronizationContext来提交UI更新操作

14.说说常⽤的锁,lock是⼀种什么样的锁?

常⽤的如如SemaphoreSlim、ManualResetEventSlim、Monitor、ReadWriteLockSlim,lock是⼀个混 合锁,其实质是Monitor

Lock是一种用于控制多个线程对共享资源的访问的同步机制,避免资源竞争和数据不一致的问题。

15.lock为什么要锁定⼀个参数(可否为值类型?)参数有什么要求?

lock关键字用于确保在同一时间只有一个线程可以访问特定的代码块,从而避免资源竞争和数据不一致的问题。

必须是一个引用类型,不能是值类型。这是因为lock的工作机制依赖于对象的内存地址(即引用),而不是对象本身的值。如果使用值类型作为互斥锁,那么每次进入lock语句块时都会创建一个新的值类型对象,这将导致无法正确同步线程。

16.多线程和异步的区别和联系?

  • 多线程‌:指的是程序同时执行多个线程,每个线程可以独立执行不同的任务。多线程主要用于提高程序的执行效率,特别是在多核CPU上,可以并行处理多个任务。
  • 异步编程‌:主要关注的是任务的执行顺序,即不按照代码书写的顺序立即执行,而是当某个操作(如I/O操作)完成时,再执行后续操作。异步编程可以使得在等待操作完成时,程序可以继续执行其他任务,从而提高程序的响应性和吞吐量

17.线程池的有点和不⾜?

  • 优点:减⼩线程创建和销毁的开销,可以复⽤线程;也从⽽减少了线程上下⽂切换的性能损失;在GC回收 时,较少的线程更有利于GC的回收效率。
  • 缺点:线程池⽆法对⼀个线程有更多的精确的控制,如了解其运 ⾏状态等;不能设置线程的优先级;加⼊到线程池的任务(⽅法)不能有返回值;对于需要⻓期运⾏的任 务就不适合线程池。

18.Mutex和lock有什么不同?⼀般⽤哪⼀种⽐较好?

Mutex是⼀个基于内核模式的互斥锁,⽀持锁的递归调⽤,⽽Lock是⼀个混合锁,⼀般建议使⽤Lock更 好,因为lock的性能更好。

20.Thread 类有哪些常⽤的属性和⽅法?

  1. Start:启动线程的执行。每个线程对象只能调用一次Start方法,重复调用会引发异常。
  2. Abort:终止线程的执行。但这种方法是不安全的,因为它可能导致资源未能正确释放或清理。
  3. Join:等待调用Join方法的线程结束执行。可以传递一个可选的时间参数,指定等待的最长时间。
  4. Sleep:使当前线程阻塞指定的时间。这不会释放任何锁。
  5. Yield:主动让出当前线程的执行权,允许其他线程获取CPU时间片。
  6. IsAlive:检查线程是否仍在运行。如果线程仍在执行或尚未开始执行,则返回true。
  7. ThreadState:获取表示线程状态的值。这个值是一个枚举类型,包括如Running、Stopped、Suspended等状态。
  8. ManagedThreadId:获取当前线程的唯一标识符。在同一进程中,每个线程的ID都是唯一的。
  9. Name:获取或设置线程的名称。线程名称通常用于调试目的。
  10. Priority:获取或设置线程的调度优先级。高优先级的线程有更大机会被调度器选中执行。
  11. CurrentThread:获取表示当前正在执行的线程的Thread对象。这是一个静态方法。
  12. CurrentContext:获取当前线程的上下文信息。
  13. CurrentPrincipal:获取或设置当前线程的安全上下文。
  14. CurrentCulture:获取或设置当前线程的区域性信息。
  15. CurrentUICulture:获取或设置当前线程的用户界面区域性信息。
  16. ExecutionContext:获取当前线程的执行上下文。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值