.NET面试题解析(07)-多线程编程与线程同步

关于线程的知识点其实是很多的,比如多线程编程、线程上下文、异步编程、线程同步构造、GUI的跨线程访问等等,本文只是从常见面试题的角度(也是开发过程中常用)去深入浅出线程相关的知识。如果想要系统的学习多线程,没有捷径的,也不要偷懒,还是去看专业书籍的比较好。

常见面试题目:

  1. 描述线程与进程的区别?
  2. 为什么GUI不支持跨线程访问控件?一般如何解决这个问题?
  3. 简述后台线程和前台线程的区别?
  4. 说说常用的锁,lock是一种什么样的锁?
  5. lock为什么要锁定一个参数,可不可锁定一个值类型?这个参数有什么要求?
  6. 多线程和异步有什么关系和区别?
  7. 线程池的优点有哪些?又有哪些不足?
  8. Mutex和lock有何不同?一般用哪一个作为锁使用更好?
  9. 下面的代码,调用方法DeadLockTest(20),是否会引起死锁?并说明理由。
public void DeadLockTest(int i)
{
   
    lock (this)   //或者lock一个静态object变量
    {
   
        if (i > 10)
        {
   
            Console.WriteLine(i--);
            DeadLockTest(i);
        }
    }
}
  1. 用双检锁实现一个单例模式Singleton。
    11.下面代码输出结果是什么?为什么?如何改进她?
int a = 0;
System.Threading.Tasks.Parallel.For(0, 100000, (i) =>
{
   
    a++; 
});
Console.Write(a);

线程基础
进程与线程
我们运行一个exe,就是一个进程实例,系统中有很多个进程。每一个进程都有自己的内存地址空间,每个进程相当于一个独立的边界,有自己的独占的资源,进程之间不能共享代码和数据空间。
在这里插入图片描述
每一个进程有一个或多个线程,进程内多个线程可以共享所属进程的资源和数据,线程是操作系统调度的基本单元。线程是由操作系统来调度和执行的,她的基本状态如下图。
在这里插入图片描述
线程的开销及调度
当我们创建了一个线程后,线程里面到底有些什么东西呢?主要包括线程内核对象、线程环境块、1M大小的用户模式栈、内核模式栈。其中用户模式栈对于普通的系统线程那1M是预留的,在需要的时候才会分配,但是对于CLR线程,那1M是一开始就分类了内存空间的。
在这里插入图片描述
在这里插入图片描述
还记得以前学校里学习计算机课程里讲到,计算机的核心计算资源就是CPU核心和CPU寄存器,这也就是线程运行的主要战场。操作系统中那么多线程(一般都有上千个线程,大部分都处于休眠状态),对于单核CPU,一次只能有一个线程被调度执行,那么多线程怎么分配的呢?Windows系统采用时间轮询机制,CPU计算资源以时间片(大约30ms)的形式分配给执行线程。
计算鸡资源(CPU核心和CPU寄存器)一次只能调度一个线程,具体的调度流程:

把CPU寄存器内的数据保存到当前线程内部(线程上下文等地方),给下一个线程腾地方;
线程调度:在线程集合里取出一个需要执行的线程;
加载新线程的上下文数据到CPU寄存器;
新线程执行,享受她自己的CPU时间片(大约30ms),完了之后继续回到第一步,继续轮回;

上面线程调度的过程,就是一次线程切换,一次切换就涉及到线程上下文等数据的搬入搬出,性能开销是很大的。因此线程不可滥用,线程的创建和消费也是很昂贵的,这也是为什么建议尽量使用线程池的一个主要原因。
对于Thread的使用太简单了,这里就不重复了,总结一下线程的主要几点性能影响:

线程的创建、销毁都是很昂贵的;
线程上下文切换有极大的性能开销,当然假如需要调度的新线程与当前是同一线程的话,就不需要线程上下文切换了,效率要快很多;
这一点需要注意,GC执行回收时,首先要(安全的)挂起所有线程,遍历所有线程栈(根),GC回收后更新所有线程的根地址,再恢复线程调用,线程越多,GC要干的活就越多;

当然现在硬件的发展,CPU的核心越来越多,多线程技术可以极大提高应用程序的效率。但这也必须在合理利用多线程技术的前提下,了线程的基本原理,然后根据实际需求,还要注意相关资源环境,如磁盘IO、网络等情况综合考虑。

多线程
单线程的使用这里就略过了,那太easy了。上面总结了线程的诸多不足,因此微软提供了可供多线程编程的各种技术,如线程池、任务、并行等等。
线程池ThreadPool
线程池的使用是非常简单的,如下面的代码,把需要执行的代码提交到线程池,线程池内部会安排一个空闲的线程来执行你的代码,完全不用管理内部是如何进行线程调度的。
ThreadPool.QueueUserWorkItem(t => Console.WriteLine(“Hello thread pool”));
每个CLR都有一个线程池,线程池在CLR内可以多个AppDomain共享,线程池是CLR内部管理的一个线程集合,初始是没有线程的,在需要的时候才会创建。线程池的主要结构图如下图所示,基本流程如下:

线程池内部维护一个请求列队,用于缓存用户请求需要执行的代码任务,就是ThreadPool.QueueUserWorkItem提交的请求;
有新任务后,线程池使用空闲线程或新线程来执行队列请求;
任务执行完后线程不会销毁,留着重复使用;
线程池自己负责维护线程的创建和销毁,当线程池中有大量闲置的线程时,线程池会自动结束一部分多余的线程来释放资源;

线程池是有一个容量的,因为他是一个池子嘛,可以设置线程池的最大活跃线程数,调用方法ThreadPool.SetMaxThreads可以设置相关参数。但很多编程实践里都不建议程序猿们自己去设置这些参数,其实微软为了提高线程池性能,做了大量的优化,线程池可以很智能的确定是否要创建或是消费线程,大多数情况都可以满足需求了。

线程池使得线程可以充分有效地被利用,减少了任务启动的延迟,也不用大量的去创建线程,避免了大量线程的创建和销毁对性能的极大影响。

上面了解了线程的基本原理和诸多优点后,如果你是一个爱思考的猿类,应该会很容易发现很多疑问,比如把任务添加到线程池队列后,怎么取消或挂起呢?如何知道她执行完了呢?下面来总结一下线程池的不足:

线程池内的线程不支持线程的挂起、取消等操作,如想要取消线程里的任务,.NET支持一种协作式方式取消,使用起来也不少很方便,而且有些场景并不满足需求;
线程内的任务没有返回值,也不知道何时执行完成;
不支持设置线程的优先级,还包括其他类似需要对线程有更多的控制的需求都不支持;

因此微软为我们提供了另外一个东西叫做Task来补充线程池的某些不足。
任务Task与并行Parallel
任务Task与并行Parallel本质上内部都是使用的线程池,提供了更丰富的并行编程的方式。任务Task基于线程池,可支持返回值,支持比较强大的任务执行计划定制等功能,下面是一个简单的示例。Task提供了很多方法和属性,通过这些方法和属性能够对Task的执行进行控制,并且能够获得其状态信息。Task的创建和执行都是独立的,因此可以对关联操作的执行拥有完全的控制权。

//创建一个任务
Task<int> t1 = new Task<int>(n =>
{
   
    System.Threading.Thread.
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值