C#多线程编程序--聊聊线程
这篇文章主要给您讲解几个基本问题,什么是线程?为什么要用线程?线程万能?这几个问题。我这篇博客是在该系列另外几篇博客之后写的,之所以不往下继续写,而是回到最初的问题,是因为我感觉到我没有很好的讲解开头的部分,没有很好的介绍线程的基础知识,因此有了写这篇博客的想法。本文不会一五一十的介绍线程,那是百度百科和维基百科的事,我的目的是和您坐下来聊一聊,以我自己粗浅的理解,给您简单讲解一下线程的相关内容。读完,您会对线程有个基本的了解,然后你再去看本系列的其他部分,就会简单些,不会一头雾水。
什么是线程?为什么要用线程?
在一个pc中有很多应用,每一个应用都被分配了一块内存,这些内存就是进程。当你在桌面点击不同的应用时,cpu会在这些应用中切换,以保证尽快的响应你的操作。这些进程能够独立运行,彼此互不影响,一个进程出了问题不会影响其他的进程。很多程序为了提升用户的操作体验,会有好几个进程,要开启一个新进程要消耗很多的时间(大概几秒),有没有一个“轻量级”的进程,能让cpu执行它,且不需要那么多的内存?这就是线程。线程相对进程要简单的多,分配一个新线程很快。现在的cpu执行都是以线程为单位来调度的,而不是进程,并且多核处理器可以并行执行多个线程。
线程万能?
线程怎么看都比进程要好很多,那么线程有什么缺点?相对于进程,线程的调度有很多的优势。这就导致有人对线程过分高估,干点什么事都新开个线程,期待它会提高用户体验。殊不知线程的创建和切换也会消耗资源。一个线程栈大概1m,切换线程需要cpu从一个线程上下文保存起来,以备以后再次调用该线程。然后读取想要切换的线程的上下文,如果新建线程,还要分配线程的内存。当频繁创建和切换线程很频繁,或者切换线程后执行的人无很短,这些消耗就必须要考虑了。
线程的消耗主要集中在创建和线程切换。非常快的任务是不适合调派其他线程来执行的。而是那些需要等待的任务才适合交给其他线程来执行。如链接其他服务,文件读写等IO操作,再者就是长时间的计算,这些任务如果都在主线程执行,会造成“卡顿”,这样的卡顿是让人沮丧的,如果卡顿的时间过长,我们会以为我们的电脑死机了。此时如果将这样的任务交给另一个线程去执行,而主线程能够继续响应用户其他的操作,这就能提升用户体验,线程的消耗才是“物超所值”。另一个消耗是创建线程的消耗。当创建一个新线程,并执行结束后,这个线程就再也不会唤醒。可以把这些死线程利用我来,当真正需要新线程的时候再创建新线程。做法是把线程统一管理,重复利用,这就是线程池。
线程的两大消耗已经有了解释,线程还有一个问题就是数据共享的问题。我们说过,线程很轻量级,为了使线程够轻,采用的办法是多个线程共享一个进程的内存,这就会导致多个线程执行时,数据会产生不同步的问题。这个线程已经修改了某一个数据,另一个线程并没有读取到该数据,还是按老值来操作。
线程同步的解决办法有两个,一是原子操作,另一个是锁。原子操作是一组API,该API能够保证该操作的“原子性”,所谓原子性,就是其他线程对同样资源的访问都会在之前或者之后发生,比如对某一数据的读操作,如果该操作是原子的,那么就是说所有对该资源的写操作都是在该读操作的之前或者之后发生,而不会发生正在读该数据时,其他线程完成了对该资源的写操作,对该数据的读总是读到最新的。这样的操作的优点很明显,简单,且不阻塞线程,不会造成死锁。其缺点是能够实现的功能有限。
另一个线程同步的方法是锁,锁的原理是在不想被并发执行的代码的周围加上一个锁,类似这样:
var m_lock = new SomeLock(); m_lock.Enter(); //some code m_lock.Leave();
当有线程执行到m_lock.Enter()时,其内部是一个变量(比如bool型),会将该变量置为true,当其他线程走到m_lock.Enter时,看到变量为true,要么不断尝试,要么挂起,等待锁被释放后,被唤醒。当执行完some code后,m_lock.Leave()执行,会将变量置为false,这样其他线程就可以访问该段代码。锁的种类大致分为三种,1是自旋锁,当执行到m_lock.Enter()时,发现被锁定,会不断尝试获得锁,像这样:
//比如这个i就是那个bool型变量
//注意,本例是简单讲解,实际的锁,此处应调用原子操作,如Interlocked中的方法,或者将变量i设为volatile。 while(i == true){ //一些黑科技 }
线程会不断的while,一直循环,直到i==false,表示锁被释放了,他才继续执行。这种锁的优点是单线程时执行非常块,但是在等待锁释放时,会不停的自旋,以求最快的进入锁,这会白白的浪费CPU。
2 内核锁,内核锁也是一个变量,只不过这个变量不用你手动的去改变,而是由系统内核来对其进行维护。这种锁的优点是当其他线程尝试进入锁,而该锁已被锁定时,会阻塞该线程,然后内核负责在锁释放后,唤醒该线程,这会节约CPU资源,在锁被释放之前,线程一直挂起,用来执行其他操作。但是其缺点就是当单线程时,由于内核锁是内核构造,因此用户线程需要切换到内核中,读取该变量,然后再切换回用户线程,最后才能得到返回值,这一系列的操作造成了资源的消耗,所以很慢。如果该资源的大部分时间都是单线程,并且通常锁定的时间都很短,那么用自旋锁就比内核锁更合适,相反就用内核锁。
3 混合锁,混合锁结合了12两种锁的优点,单线程时很快,多线程时阻塞,还有多线程时先自旋一小段时间,如果被锁定的资源很快被释放,那么久不需要调用内核锁,否则调用内核锁。我们经常会见到 lock(objec)语句,其内部是调用Monitor.Enter和Monitor.Leave,这个 Monitor就是个混合锁。
上面讲了这几种锁,各有优缺点。在多线程编程中,基本原则是尽量将代码设计的合理些,减少锁的使用,若必须使用锁,则根据自身的情况,选择合理的锁。
以上,我介绍了线程的基本知识和线程安全的相关知识,若您对其中的内容有任何不懂得地方,欢迎在评论区与我互动。