.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.描述线程与进程的区别?
定义与区别:
- 进程(Process)是系统进行资源分配和调度的一个独立单元,是操作系统结构的基础。它是应用程序的一次动态执行过程,是程序代码及其数据在运行时的一个实例。它拥有独立的内存空间,包含一组系统资源(如代码、数据和打开的文件等)。
- 线程(Thread)是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的独立运行的单位。一个进程可以拥有多个线程,这些线程共享该进程的所有资源(如内存和打开的文件),但每个线程都拥有自己独立的执行栈和程序计数器。
资源与开销:
- 进程拥有独立的内存空间和系统资源,因此进程间的通信相对复杂,开销也较大。
- 线程共享进程的资源,因此线程间的通信相对简单,开销也较小。
独立性:
- 进程拥有更高的独立性,一个进程的崩溃通常不会影响到其他进程。
- 线程则不然,一个线程的崩溃可能会导致整个进程的崩溃(取决于线程的异常处理机制)。
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有区别吗?
抽象层次:
Task
提供了比Thread
更高的抽象层次。它代表了异步操作,可以很容易地与C#的异步编程模式(async/await)集成。Thread
则更底层,直接表示操作系统线程。资源管理:
Task
池管理了任务的执行,可以更有效地重用线程,减少线程创建和销毁的开销。相比之下,直接使用Thread
需要程序员手动管理线程的创建、执行和销毁。异步编程支持:
Task
天然支持C#的异步编程模式,使得编写异步代码更加简单和直观。而Thread
需要通过额外的机制(如回调函数、轮询等)来实现类似的功能。
11.多线程有什么⽤?
- 提高CPU利用率:多线程使得在等待某些任务完成时,如I/O操作或用户输入,CPU可以转而执行其他线程的任务,从而大大提高了CPU的利用率。
- 提升系统性能:通过并行处理多个任务,多线程可以显著提高系统的处理能力和性能,尤其是在多核处理器系统中更为明显。
- 改善用户体验:在图形用户界面(GUI)应用中,多线程可以实现界面的响应式设计,即使后台正在进行耗时的操作,用户界面也能保持活跃,提升用户体验。
- 简化资源共享:同一进程下的所有线程共享进程的资源,如内存空间、文件句柄等,这使得不同任务之间的协调操作与数据交互更加简单高效。
- 实现复杂任务:对于需要同时处理多个独立但相关任务的场景,多线程提供了一个有效的解决方案。例如,在服务器端并发处理用户请求时,每个请求可以在一个独立的线程中处理,互不干扰,提高了服务的响应速度和效率。
- 优化资源分配:在多线程编程中,可以通过线程同步机制来控制对共享资源的访问,防止数据竞争和死锁的发生,确保系统的稳定性和可靠性。
- 增强程序灵活性:多线程技术使得开发者能够更灵活地安排程序的执行流程,通过创建和管理不同的线程,可以有效地应对各种复杂的程序需求。
- 支持实时系统:在需要快速响应外部事件的应用中,如实时数据处理或游戏开发,多线程技术可以提供必要的并发执行能力,以满足严格的时间要求。
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更新操作