本文转载自微信公众号《码农的荒岛求生》,作者码农的荒岛求生。转载本文请联系码农荒岛求生公众号。相信很多同学在面对多线程代码的时候都会被吓倒,认为多线程代码就像一头难以驯服的怪物。如果你不能收服这只怪物,它会反过来吞噬你。太夸张了。总之,多线程程序有时候就像一滩烂泥,进不去,出不来。但这是为什么呢?为什么多线程代码这么难写正确?从根源上思考这个问题,本质上就是有一个词你没理解透。这个词就是所谓的threadsafe,线程安全。如果你不能理解线程安全,那么给你再多的解决方案也没用。接下来,我们就来看看什么是线程安全,以及如何实现线程安全。回答完这些问题,多线程这个大怪物自然就变成温顺的小猫了。不过上图是小猫的事!你的事业是什么?在生活中,我们经常说“这是你的事”。想一想,为什么我们的事跟人家没关系?原因很简单。这是我的私事!我的衣服,我的电脑,我的手机,我的车子,我的别墅和私人游泳池(你可以有,但不妨碍想象),我可以随心所欲,不碍事其他,事情而属于我一个人的东西,当然不关他们的事,就算是个屁。我们在自己家里想吃什么就吃什么,想上厕所就上厕所!因为这些都是我的隐私,只有我自己用。那么什么时候会和其他人有交集呢?答案是公共场所。在公共场所不能像在自己家一样想去哪儿就去哪儿,想上厕所就去哪儿,为什么呢?道理很简单,因为公共场所的餐厅和厕所不是你的??家,它们是公共资源,是每个人都可以使用的公共资源。如果你想去餐馆或公共厕所,你必须遵守规则。此规则正在排队。上一个人用完后,只有下一个人可以使用公共资源,不能同时使用。如果你想使用它们,你必须排队等候。上面的语句很简单。如果你能看懂这段话,那么驯服多线程这个小怪物就很容易了。在公共场所维护秩序如果把自己理解为一个线程,那么在自己家里使用私有资源就是所谓的线程安全。道理很简单,因为你自己的东西(资源)乱了,就不会妨碍别人;去公共场所就不一样了。公共资源用于公共场所。目前,您不能像在自己家里那样随心所欲地使用它们。您可以随时使用它们。公共场所必须有相应的规则,这里的规则通常是排队,只有这样公共场所的秩序才不会被破坏,线程才能以不妨碍其他线程的顺序使用共享资源来实现线程安全。所以我们可以看出这里有两种情况:线程私有资源,没有线程安全问题共享资源,线程间也可以通过一定顺序使用共享资源来实现线程安全。本文主要围绕以上两个核心点展开。现在我们可以正式谈谈编程中的线程安全了。什么是线程安全我们说一段代码是线程安全的,当且仅当我们在多个线程中同时多次调用这段代码时能够给出正确的结果,我们才可以说这样的代码是线程安全的代码,ThreadSafety,否则就不是线程安全的代码,thread-unsafe。运行非线程安全代码的结果由掷骰子决定。怎么样,线程安全的定义很简单,就是说不管你的代码是单线程执行还是多线程执行,都应该能够给出正确的运行结果,这样的代码就不会有多线程问题,就像下面这段代码:intfunc(){inta=1;intb=1;returna+b;}对于这样一段代码,不管你同时调用多少个线程,如何调用,调用时返回2。代码是线程安全的。那么我们如何编写线程安全的代码呢?要回答这个问题,我们需要知道我们的代码什么时候呆在自己家里使用私有资源,什么时候去公共场所浪费公共资源,也就是说你需要识别线程有哪些私有资源和线程的共享资源,这是解决线程安全问题的核心。线程私有资源线程运行的本质其实就是函数的执行。功能的执行总是有源头的。这个源就是所谓的入口函数。CPU从入口函数开始执行,形成一个执行流,但是我们人为地给这个执行流起了一个名字,这个名字叫做线程。既然线程运行的本质是函数的执行,那么函数运行时的信息存储在哪里呢?答案是栈区。每个线程都有一个私有的栈区,所以栈上分配的局部变量是线程私有的。不管我们如何使用这些局部变量,其他线程做什么并不重要。线程的私有栈区是线程自己的家。除了上节提到的区域外,线程间共享的数据就是公共区域,它包括:用于动态分配内存的堆区域。我们在C/C++中使用malloc或者new在堆区申请内存全局区,这里是全局变量文件。我们知道线程是共享进程打开的文件。有的同学可能会说,等等,上一篇文章不是说有代码区和动态链接库吗?注意这两个区域是不能修改的,也就是说这两个区域是只读的,多线程使用是没有问题的。刚才我们说的堆区、数据区、文件,这些都是所有线程可以共享的资源,也就是公共的地方。在这些公共场所线程不能大意。线程在使用这些共享资源时必须遵守顺序。这个顺序的核心是共享资源的使用不能妨碍其他线程。无论你使用各种锁还是信号量,目的都是为了维持公共场所的秩序。知道哪些是线程私有的,哪些是线程间共享的,下一步就简单了。值得注意的是,所有与线程安全相关的问题都是围绕线程私有数据和线程共享数据来处理的。抓住了线程私有资源和共享资源的主要矛盾,也就抓住了解决线程安全问题的核心。接下来我们看看在各种情况下如何实现线程安全。我们仍然以C/C++代码为例,但是这里讲解的方法适用于任何语言。请放心,这些代码非常简单。只使用线程私有资源看这段代码:intfunc(){inta=1;intb=1;returna+b;}这段代码前面有讲到,无论你调用多少线程when,func函数肯定会返回2.这个函数不依赖任何全局变量,不依赖任何函数参数,使用的局部变量都是线程私有资源。这样的代码也被称为无状态函数,stateless,很明显。代码是线程安全的。请在多线程中大胆使用这样的代码,没有任何问题。可能有同学会说,如果我们还是使用线程私有资源,只是传入函数参数呢?线程私有资源+函数参数等代码是线程安全的吗?先想想这个问题。答案是视情况而定,即视情况而定。情况如何?1.按值传参如果按值传参,那么就没有问题,代码还是线程安全的:intfunc(intnum){num++;returnnum;}这段代码不管调用多少个线程,如何call和何时调用,参数加1后的值会正确返回。原因很简单,这些传值的参数是线程私有的资源。2.按引用传递参数但是如果按引用传递参数,情况就不一样了:intfunc(int*num){++(*num);return*num;}如果调用函数的线程传入如果参数是线程私有资源,函数仍然是线程安全的,参数加1后可以正确返回值。但是如果传入的参数是一个全局变量,像这样:intglobal_num=1;intfunc(int*num){++(*num);return*num;}//Thread1voidthread1(){func(&global_num);}//Thread2voidthread1(){func(&global_num);}此时func函数将不再是线程安全的代码,因为传入的参数指向一个全局变量,它是所有线程共享的资源。在这种情况下如果不改变全局变量的使用方式,那么全局变量加1就必须强加一些顺序,比如加锁。可能有同学会说,如果我传入一个不是全局变量的指针(引用),是不是就没有问题了?答案还是itdependent,看情况。即使我们传入的参数是从堆上的malloc或者new中获取的,也可能还是会出现问题。为什么?答案很简单,因为堆上的资源也是所有线程共享的。如果两个线程调用func函数,传入的指针(引用)指向同一个堆上的一个变量,那么这个变量就成为两个线程的共享资源。在这种情况下,func函数仍然不是线程安全的。改进也很简单,就是每个线程调用func函数传入一个线程独有的资源地址,这样各个线程就不会互相妨碍了。因此,编写线程安全代码的一大原则就是能够在线程中使用私有资源,使用私有资源,尽量不要使用线程间的共享资源。如果线程必须使用全局资源怎么办?使用全局资源使用全局资源一定不是线程安全的代码?答案还是。.可能有的同学已经猜到了,但是答案还是要看情况。如果使用的全局资源在程序运行时只初始化一次,之后所有代码使用它都是只读的,那么就没有问题,像这样:intglobal_num=100;//初始化一次,没有其他代码修改它intfunc(){returnglobal_num;}之后的值我们看到即使func函数使用了全局变量,但是全局变量在运行前只初始化了一次,之后代码也不会修改,那么func函数还是线程安全的。但是,如果我们简单的修改func:intglobal_num=100;intfunc(){++global_num;returnglobal_num;}这时候func函数就不再是线程安全的了,对全局变量的修改必须加锁保护。线程本地存储接下来我们简单修改一下上面的func函数:__threadintglobal_num=100;intfunc(){++global_num;returnglobal_num;}我们看到全局变量global_num已经被关键字__thread修改了。这时候func的代码又是线程安全的了。为什么?其实我们在上一篇文章中说过,__thread关键字修饰的变量是放在线程私有存储ThreadLocalStorage中的,什么意思呢?表示这个变量是线程私有的全局变量:global_num是全局变量global_num是线程私有的,每个线程对global_num的修改不会影响其他线程,因为是线程私有的资源,所以func函数是线程安全的。说完局部变量、全局变量、函数参数,接下来就是函数的返回值了。函数返回值这里也有两种情况,一种是函数有返回值;另一个返回对变量的引用。1、返回的是一个值。我们看这样一段代码:intfunc(){inta=100;returna;}毫无疑问,这段代码是线程安全的,不管我们怎么调用这个函数,它都会返回某个值100。2,回报是一个参考。我们简单的改一下上面的代码:int*func(){staticinta=100;return&a;}如果我们在多线程中调用这样一个函数,那么等待你的可能是难以调试的bug和漫漫长夜的加班。.显然,这不是线程安全的代码,产生bug的原因很简单。变量的值可能在您使用它之前已被其他线程修改。因为这个函数使用的是静态全局变量,只要能获取到变量的地址,所有线程都可以修改变量的值,因为这是线程间的共享资源,除非绝对不要写上面的代码有必要,除非老板拿刀架在你脖子上。但是,请注意有一种特殊情况,这种使用方法可以用于在设计模式中实现单例模式,像这样:classS{public:staticS&getInstance(){staticSinstance;returninstance;}private:S(){}//其他省略}为什么?因为不管我们调用多少次func函数,static局部变量只会被初始化一次。这个特性让我们可以很方便的实现单例模式。最后我们看一下这种情况,就是如果我们调用了一个非线程安全的函数,那么我们的函数是线程安全的吗?调用非线程安全的代码如果一个函数A调用另一个函数B,但是B不是线程安全的,那么函数A是线程安全的吗?答案仍然是肯定的,这取决于。我们看这样一段代码,之前解释过:intglobal_num=0;intfunc(){++global_num;returnglobal_num;}我们认为func函数不是线程安全的,因为func函数使用的是全局变量,并执行它们修改了,但是如果我们这样调用func函数:intfuncA(){mutexl;l.lock();func();l.unlock();}function是用锁来保护的,那么此时funcA函数是线程安全的,其实质就是我们用锁间接的保护了全局变量。再看这段代码:intfunc(int*num){++(*num);return*num;}一般我们认为func函数不是线程安全的,因为我们不知道传入的是否指针指向一个全局变量,但是如果调用func函数的代码是这样的:voidfuncA(){inta=100;func(&a);},那么此时funcA函数仍然是线程安全的,因为传入的参数是线程私有的局部变量,无论有多少线程调用funcA都不会干扰其他线程。在了解了各种情况下的线程安全问题之后,让我们最后总结一下实现线程安全代码的措施。如何实现线程安全从以上几种情况分析,线程安全的实现无非就是围绕着线程私有资源和线程共享资源两点。您需要确定哪些是线程私有的,哪些是共享的。这是核心。然后就可以对症下药了。不使用任何全局资源,只使用线程私有资源,这种通常称为无状态代码线程局部存储,如果要使用全局资源,是否可以声明为线程局部存储,因为虽然这种变量是全局的,但是每个线程都有自己的副本,它的修改不会影响其他线程。只读。如果一定要使用全局资源,那么全局资源可以只读吗?多线程使用只读全局资源不会有线程安全问题。原子操作,原子操作是指在执行过程中不可能被其他线程打断,比如C++中std::atomic修饰的变量,这类变量的操作不需要传统的锁保护,因为C++会保证变量的修改不会被中断。我们常说的各种无锁数据结构,通常都是建立在这种原子操作的基础上的。同步互斥。这里确定你必须以某种形式使用全局资源。在这种情况下,公共场所的秩序是必须维护的,那么如何维护呢?通过同步或互斥的方式,这是一大类问题,我们将在《深入理解操作系统》系列文章中详细阐述。综上所述,写线程安全的并不容易。如果这篇文章你只能记住一句话,那我希望是这句话。这也是本文的核心:实现线程安全无非就是围绕着线程私有资源来和线程共享资源,需要分清哪些是线程私有的,哪些是共享的,然后对症下药.希望这篇文章对你编写多线程程序有所帮??助。本文转载自微信公众号《码农的荒岛求生》,可通过以下二维码关注。转载本文请联系码农荒岛求生公众号。
