文档章节

PhotoShop算法原理解析系列 - 风格化---》查找边缘。

abcijkxyz
 abcijkxyz
发布于 2016/11/22 16:40
字数 3703
阅读 18
收藏 0

      之所以不写系列文章一、系列文章二这样的标题,是因为我不知道我能坚持多久。我知道我对事情的表达能力和语言的丰富性方面的天赋不高。而一段代码需要我去用心的把他从基本原理--》初步实现--》优化速度 等过程用文字的方式表述清楚,恐怕不是一件很容易的事情。 

      我所掌握的一些Photoshop中的算法,不能说百分之一百就是正确的,但是从执行的效果中,大的方向肯定是没有问题的。

      目前,从别人的文章、开源的代码以及自己的思考中我掌握的PS的算法可能有近100个吧。如果时间容许、自身的耐心容许,我会将这些东西慢慢的整理开来,虽然在很多人看来,这些算法并不具有什么研究的价值了,毕竟人家都已经商业化了。说的也有道理,我姑且把他作为自我欣赏和自我满足的一种方式吧。

      今天,我们讲讲查找边缘算法。可能我说了原理,很多人就不会看下去了,可有几人层仔细的研究过呢。

  先贴个效果图吧:

    

  原理:常见的Sobel边缘算子的结果进行反色即可

      为了能吸引你继续看下去,我先给出我的代码的执行速度: 针对3000*4000*3的数码图片,处理时间300ms

      何为Sobel,从百度抄几张图过来了并修改地址后: 

                            

  对上面两个式子不做过多解释,你只需要知道其中A为输入图像,把G作为A的输出图像就可以了,最后还要做一步: G=255-G,就是查找边缘算法。

      查找边缘类算法都有个问题,对图像物理边缘处的像素如何处理,在平日的处理代码中,很多人就是忽略四个边缘的像素,作为专业的图像处理软件,这可是违反最基本的原则的。对边缘进行的单独的代码处理,又会给编码带来冗余和繁琐的问题。解决问题的最简单又高效的方式就是采用哨兵边界。

      写多了特效类算法的都应该知道,除了那种对单个像素进行处理的算法不需要对原始图像做个备份(不一定去全局备份),那些需要领域信息的算法由于算法的前一步修改了一个像素,而算法的当前步需要未修改的像素值,因此,一般这种算法都会在开始前对原始图像做个克隆,在计算时,需要的领域信息从克隆的数据中读取。如果这个克隆的过程不是完完全全的克隆,而是扩展适当边界后再克隆,就有可能解决上述的边界处理问题。

  比如对下面的一个图,19×14像素大小,我们的备份图为上下左右各扩展一个像素的大小,并用边缘的值填充,变为21*16大小:

           

  这样,在计算原图的3*3领域像素时,从扩展后的克隆图对应点取样,就不会出现不在图像范围内的问题了,编码中即可以少很多判断,可读性也加强了。

      在计算速度方面,注意到上面的计算式G中有个开方运算,这是个耗时的过程,由于图像数据的特殊性,都必须是整数,可以采用查找表的方式优化速度,这就需要考虑表的建立。

       针对本文的具体问题,我们分两步讨论,第一:针对根号下的所有可能情况建立查找表。看看GX和GY的计算公式,考虑下两者的平方和的最大值是多少,可能要考虑一会吧。第二:就是只建立0^2到255^2范围内的查找表,然后确保根号下的数字不大于255^2。为什么可以这样做,就是因为图像数据的最大值就是255,如果根号下的数字大于255^2,在求出开方值后,还是需要规整为255的。因此,本算法中应该取后者。

      贴出代码:

private void CmdFindEdgesArray_Click(object sender, EventArgs e) { int X, Y; int Width, Height, Stride, StrideC, HeightC; int Speed, SpeedOne, SpeedTwo, SpeedThree; int BlueOne, BlueTwo, GreenOne, GreenTwo, RedOne, RedTwo; int PowerRed, PowerGreen, PowerBlue; Bitmap Bmp = (Bitmap)Pic.Image; if (Bmp.PixelFormat != PixelFormat.Format24bppRgb) throw new Exception("不支持的图像格式."); byte[] SqrValue = new byte[65026]; for (Y = 0; Y < 65026; Y++) SqrValue[Y] = (byte)(255 - (int)Math.Sqrt(Y));      // 计算查找表,注意已经砸查找表里进行了反色
 Width = Bmp.Width; Height = Bmp.Height; Stride = (int)((Bmp.Width * 3 + 3) & 0XFFFFFFFC); StrideC = (Width + 2) * 3; HeightC = Height + 2;                                 // 宽度和高度都扩展2个像素

    byte[] ImageData = new byte[Stride * Height];                                    // 用于保存图像数据,(处理前后的都为他)
    byte[] ImageDataC = new byte[StrideC * HeightC];                                // 用于保存扩展后的图像数据

    fixed (byte* Scan0 = &ImageData[0]) { BitmapData BmpData = new BitmapData(); BmpData.Scan0 = (IntPtr)Scan0;                                              // 设置为字节数组的的第一个元素在内存中的地址
        BmpData.Stride = Stride; Bmp.LockBits(new Rectangle(0, 0, Bmp.Width, Bmp.Height), ImageLockMode.ReadWrite | ImageLockMode.UserInputBuffer, PixelFormat.Format24bppRgb, BmpData); Stopwatch Sw = new Stopwatch();                                             // 只获取计算用时
 Sw.Start(); for (Y = 0; Y < Height; Y++) { System.Buffer.BlockCopy(ImageData, Stride * Y, ImageDataC, StrideC * (Y + 1), 3);        // 填充扩展图的左侧第一列像素(不包括第一个和最后一个点)
            System.Buffer.BlockCopy(ImageData, Stride * Y + (Width - 1) * 3, ImageDataC, StrideC * (Y + 1) + (Width + 1) * 3, 3);  // 填充最右侧那一列的数据
            System.Buffer.BlockCopy(ImageData, Stride * Y, ImageDataC, StrideC * (Y + 1) + 3, Width * 3); } System.Buffer.BlockCopy(ImageDataC, StrideC, ImageDataC, 0, StrideC);              // 第一行
        System.Buffer.BlockCopy(ImageDataC, (HeightC - 2) * StrideC, ImageDataC, (HeightC - 1) * StrideC, StrideC);    // 最后一行 

        for (Y = 0; Y < Height; Y++) { Speed = Y * Stride; SpeedOne = StrideC * Y; for (X = 0; X < Width; X++) { SpeedTwo = SpeedOne + StrideC;          // 尽量减少计算
                SpeedThree = SpeedTwo + StrideC;        // 下面的就是严格的按照Sobel算字进行计算,代码中的*2一般会优化为移位或者两个Add指令的,如果你不放心,当然可以直接改成移位
                BlueOne = ImageDataC[SpeedOne] + 2 * ImageDataC[SpeedTwo] + ImageDataC[SpeedThree] - ImageDataC[SpeedOne + 6] - 2 * ImageDataC[SpeedTwo + 6] - ImageDataC[SpeedThree + 6]; GreenOne = ImageDataC[SpeedOne + 1] + 2 * ImageDataC[SpeedTwo + 1] + ImageDataC[SpeedThree + 1] - ImageDataC[SpeedOne + 7] - 2 * ImageDataC[SpeedTwo + 7] - ImageDataC[SpeedThree + 7]; RedOne = ImageDataC[SpeedOne + 2] + 2 * ImageDataC[SpeedTwo + 2] + ImageDataC[SpeedThree + 2] - ImageDataC[SpeedOne + 8] - 2 * ImageDataC[SpeedTwo + 8] - ImageDataC[SpeedThree + 8]; BlueTwo = ImageDataC[SpeedOne] + 2 * ImageDataC[SpeedOne + 3] + ImageDataC[SpeedOne + 6] - ImageDataC[SpeedThree] - 2 * ImageDataC[SpeedThree + 3] - ImageDataC[SpeedThree + 6]; GreenTwo = ImageDataC[SpeedOne + 1] + 2 * ImageDataC[SpeedOne + 4] + ImageDataC[SpeedOne + 7] - ImageDataC[SpeedThree + 1] - 2 * ImageDataC[SpeedThree + 4] - ImageDataC[SpeedThree + 7]; RedTwo = ImageDataC[SpeedOne + 2] + 2 * ImageDataC[SpeedOne + 5] + ImageDataC[SpeedOne + 8] - ImageDataC[SpeedThree + 2] - 2 * ImageDataC[SpeedThree + 5] - ImageDataC[SpeedThree + 8]; PowerBlue = BlueOne * BlueOne + BlueTwo * BlueTwo; PowerGreen = GreenOne * GreenOne + GreenTwo * GreenTwo; PowerRed = RedOne * RedOne + RedTwo * RedTwo; if (PowerBlue > 65025) PowerBlue = 65025;           // 处理掉溢出值
                if (PowerGreen > 65025) PowerGreen = 65025; if (PowerRed > 65025) PowerRed = 65025; ImageData[Speed] = SqrValue[PowerBlue];             // 查表
                ImageData[Speed + 1] = SqrValue[PowerGreen]; ImageData[Speed + 2] = SqrValue[PowerRed]; Speed += 3;                                  // 跳往下一个像素
                SpeedOne += 3; } } Sw.Stop(); this.Text = "计算用时: " + Sw.ElapsedMilliseconds.ToString() + " ms"; Bmp.UnlockBits(BmpData); // 必须先解锁,否则Invalidate失败 
 } Pic.Invalidate(); }

  为简单的起见,这里先是用的C#的一维数组实现的,并且计时部分未考虑图像数据的获取和更新, 因为真正的图像处理过程中图像数据肯定是已经获得的了。

     针对上述代码,编译为Release模式后,执行编译后的EXE,对于3000*4000*3的彩色图像,耗时约480ms,如果你是在IDE的模式先运行,记得一定要在选项--》调试--》常规里不勾选    在模块加载时取消JIT优化(仅限托管)一栏。    

       

     上述代码中的填充克隆图数据时并没有新建一副图,然后再填充其中的图像数据,而是直接填充一个数组,图像其实不就是一片连续内存加一点头信息吗,头信息已经有了,所以只要一片内存就够了。

     克隆数据的填充采用了系统Buffer.BlockCopy函数,该函数类似于我们以前常用CopyMemory,速度非常快。

     为进一步调高执行速度,我们首先来看看算法的关键耗时部位的代码,即for (X = 0; X < Width; X++)内部的代码,我们取一行代码的反编译码来看看:

 BlueOne = ImageDataC[SpeedOne] + 2 * ImageDataC[SpeedTwo] + ImageDataC[SpeedThree] - ImageDataC[SpeedOne + 6] - 2 * ImageDataC[SpeedTwo + 6] - ImageDataC[SpeedThree + 6]; 

00000302 cmp ebx,edi 00000304 jae 0000073C             // 数组是否越界? 0000030a movzx eax,byte ptr [esi+ebx+8]    //  将ImageDataC[SpeedOne]中的数据传送的eax寄存器 0000030f mov dword ptr [ebp-80h],eax 00000312 mov edx,dword ptr [ebp-2Ch] 00000315 cmp edx,edi 00000317 jae 0000073C            // 数组是否越界?            0000031d movzx edx,byte ptr [esi+edx+8]   //   将ImageDataC[SpeedTwo]中的数据传送到edx寄存器 00000322 add edx,edx             // 计算2*ImageDataC[SpeedTwo]     00000324 add eax,edx             // 计算ImageDataC[SpeedOne]+2*ImageDataC[SpeedTwo],并保存在eax寄存器中            00000326 cmp ecx,edi 00000328 jae 0000073C 0000032e movzx edx,byte ptr [esi+ecx+8]   // 将ImageDataC[SpeedThree]中的数据传送到edx寄存器 00000333 mov dword ptr [ebp+FFFFFF78h],edx 00000339 add eax,edx 0000033b lea edx,[ebx+6] 0000033e cmp edx,edi 00000340 jae 0000073C 00000346 movzx edx,byte ptr [esi+edx+8] 0000034b mov dword ptr [ebp+FFFFFF7Ch],edx 00000351 sub eax,edx 00000353 mov edx,dword ptr [ebp-2Ch] 00000356 add edx,6 00000359 cmp edx,edi 0000035b jae 0000073C 00000361 movzx edx,byte ptr [esi+edx+8] 00000366 add edx,edx 00000368 sub eax,edx 0000036a lea edx,[ecx+6] 0000036d cmp edx,edi 0000036f jae 0000073C 00000375 movzx edx,byte ptr [esi+edx+8] 0000037a mov dword ptr [ebp+FFFFFF74h],edx 00000380 sub eax,edx 00000382 mov dword ptr [ebp-30h],eax

   上述汇编码我只注释一点点,其中最0000073c  标号,我们跟踪后返现是调用了另外一个函数:

             0000073c  call        685172A4 

      我们看到在获取每一个数组元素前,都必须执行一个cmp 和 jae指令,从分析我认为这里是做类似于判断数组的下标是否越界之类的工作的。如果我们能确保我们的算法那不会产生越界,这部分代码有很用呢,不是耽误我做正事吗。

      为此,我认为需要在C#中直接利用指针来实现算法,C#中有unsafe模式,也有指针,所以很方便,而且指针的表达即可以用*,也可以用[],比如*(P+4) 和P[4]是一个意思。那么只要做很少的修改就可以将上述代码修改为指针版。

    

private void CmdFindEdgesPointer_Click(object sender, EventArgs e) { int X, Y; int Width, Height, Stride, StrideC, HeightC; int Speed, SpeedOne, SpeedTwo, SpeedThree; int BlueOne, BlueTwo, GreenOne, GreenTwo, RedOne, RedTwo; int PowerRed, PowerGreen, PowerBlue; Bitmap Bmp = (Bitmap)Pic.Image; if (Bmp.PixelFormat != PixelFormat.Format24bppRgb) throw new Exception("不支持的图像格式."); byte[] SqrValue = new byte[65026]; for (Y = 0; Y < 65026; Y++) SqrValue[Y] = (byte)(255 - (int)Math.Sqrt(Y));      // 计算查找表,注意已经砸查找表里进行了反色
 Width = Bmp.Width; Height = Bmp.Height; Stride = (int)((Bmp.Width * 3 + 3) & 0XFFFFFFFC); StrideC = (Width + 2) * 3; HeightC = Height + 2;                                 // 宽度和高度都扩展2个像素

        byte[] ImageData = new byte[Stride * Height];                                    // 用于保存图像数据,(处理前后的都为他)
        byte[] ImageDataC = new byte[StrideC * HeightC];                                 // 用于保存扩展后的图像数据

        fixed (byte* P = &ImageData[0], CP = &ImageDataC[0], LP = &SqrValue[0]) { byte* DataP = P, DataCP = CP, LutP = LP; BitmapData BmpData = new BitmapData(); BmpData.Scan0 = (IntPtr)DataP;                                              // 设置为字节数组的的第一个元素在内存中的地址
            BmpData.Stride = Stride; Bmp.LockBits(new Rectangle(0, 0, Bmp.Width, Bmp.Height), ImageLockMode.ReadWrite | ImageLockMode.UserInputBuffer, PixelFormat.Format24bppRgb, BmpData); Stopwatch Sw = new Stopwatch();                                             // 只获取计算用时
 Sw.Start(); for (Y = 0; Y < Height; Y++) { System.Buffer.BlockCopy(ImageData, Stride * Y, ImageDataC, StrideC * (Y + 1), 3);        // 填充扩展图的左侧第一列像素(不包括第一个和最后一个点)
                System.Buffer.BlockCopy(ImageData, Stride * Y + (Width - 1) * 3, ImageDataC, StrideC * (Y + 1) + (Width + 1) * 3, 3);  // 填充最右侧那一列的数据
                System.Buffer.BlockCopy(ImageData, Stride * Y, ImageDataC, StrideC * (Y + 1) + 3, Width * 3); } System.Buffer.BlockCopy(ImageDataC, StrideC, ImageDataC, 0, StrideC);              // 第一行
            System.Buffer.BlockCopy(ImageDataC, (HeightC - 2) * StrideC, ImageDataC, (HeightC - 1) * StrideC, StrideC);    // 最后一行 

            for (Y = 0; Y < Height; Y++) { Speed = Y * Stride; SpeedOne = StrideC * Y; for (X = 0; X < Width; X++) { SpeedTwo = SpeedOne + StrideC;          // 尽量减少计算
                    SpeedThree = SpeedTwo + StrideC;        // 下面的就是严格的按照Sobel算字进行计算,代码中的*2一般会优化为移位或者两个Add指令的,如果你不放心,当然可以直接改成移位
                    BlueOne = DataCP[SpeedOne] + 2 * DataCP[SpeedTwo] + DataCP[SpeedThree] - DataCP[SpeedOne + 6] - 2 * DataCP[SpeedTwo + 6] - DataCP[SpeedThree + 6]; GreenOne = DataCP[SpeedOne + 1] + 2 * DataCP[SpeedTwo + 1] + DataCP[SpeedThree + 1] - DataCP[SpeedOne + 7] - 2 * DataCP[SpeedTwo + 7] - DataCP[SpeedThree + 7]; RedOne = DataCP[SpeedOne + 2] + 2 * DataCP[SpeedTwo + 2] + DataCP[SpeedThree + 2] - DataCP[SpeedOne + 8] - 2 * DataCP[SpeedTwo + 8] - DataCP[SpeedThree + 8]; BlueTwo = DataCP[SpeedOne] + 2 * DataCP[SpeedOne + 3] + DataCP[SpeedOne + 6] - DataCP[SpeedThree] - 2 * DataCP[SpeedThree + 3] - DataCP[SpeedThree + 6]; GreenTwo = DataCP[SpeedOne + 1] + 2 * DataCP[SpeedOne + 4] + DataCP[SpeedOne + 7] - DataCP[SpeedThree + 1] - 2 * DataCP[SpeedThree + 4] - DataCP[SpeedThree + 7]; RedTwo = DataCP[SpeedOne + 2] + 2 * DataCP[SpeedOne + 5] + DataCP[SpeedOne + 8] - DataCP[SpeedThree + 2] - 2 * DataCP[SpeedThree + 5] - DataCP[SpeedThree + 8]; PowerBlue = BlueOne * BlueOne + BlueTwo * BlueTwo; PowerGreen = GreenOne * GreenOne + GreenTwo * GreenTwo; PowerRed = RedOne * RedOne + RedTwo * RedTwo; if (PowerBlue > 65025) PowerBlue = 65025;           // 处理掉溢出值
                    if (PowerGreen > 65025) PowerGreen = 65025; if (PowerRed > 65025) PowerRed = 65025; DataP[Speed] = LutP[PowerBlue];                     // 查表
                    DataP[Speed + 1] = LutP[PowerGreen]; DataP[Speed + 2] = LutP[PowerRed]; Speed += 3;                                         // 跳往下一个像素
                    SpeedOne += 3; } } Sw.Stop(); this.Text = "计算用时: " + Sw.ElapsedMilliseconds.ToString() + " ms"; Bmp.UnlockBits(BmpData); // 必须先解锁,否则Invalidate失败 
 } Pic.Invalidate(); }

     同样的效果,同样的图像,计算用时330ms。

     我们在来看看相同代码的汇编码:

BlueOne = DataCP[SpeedOne] + 2 * DataCP[SpeedTwo] + DataCP[SpeedThree] - DataCP[SpeedOne + 6] - 2 * DataCP[SpeedTwo + 6] - DataCP[SpeedThree + 6]; 

00000318 movzx eax,byte ptr [esi+edi] 0000031c mov dword ptr [ebp-74h],eax 0000031f movzx edx,byte ptr [esi+ebx] 00000323 add edx,edx 00000325 add eax,edx 00000327 movzx edx,byte ptr [esi+ecx] 0000032b mov dword ptr [ebp-7Ch],edx 0000032e add eax,edx 00000330 movzx edx,byte ptr [esi+edi+6] 00000335 mov dword ptr [ebp-78h],edx 00000338 sub eax,edx 0000033a movzx edx,byte ptr [esi+ebx+6] 0000033f add edx,edx 00000341 sub eax,edx 00000343 movzx edx,byte ptr [esi+ecx+6] 00000348 mov dword ptr [ebp-80h],edx 0000034b sub eax,edx 0000034d mov dword ptr [ebp-30h],eax

      生产的汇编码简洁,意义明确,对比下少了很多指令。当然速度会快很多。

      注意这一段代码:

  fixed (byte* P = &ImageData[0], CP = &ImageDataC[0], LP = &SqrValue[0]) { byte* DataP = P, DataCP = CP, LutP = LP;

     如果你把更换为:

  fixed (byte* DataP = &ImageData[0], DataCP = &ImageDataC[0], LutP = &SqrValue[0]) {

      代码的速度反而比纯数组版的还慢,至于为什么,实践为王吧,我也没有去分析,反正我知道有这个结果。你可以参考铁哥的一篇文章:

                 闲谈.Net类型之public的不public,fixed的不能fixed

     当然这个还可以进一步做小动作的的优化,比如movzx eax,byte ptr [esi+edi] 这句中,esi其实就是数组的基地址,向这样写DataCP[SpeedOne] ,每次都会有这个基址+偏移的计算的,如果能实时直接动态控制一个指针变量,使他直接指向索要的位置,则少了一次加法,虽然优化不是很明显,基本可以达到问中之前所提到的300ms的时间了。具体的代码可见附件。

      很多人可能对我这些东西不感冒,说这些东西丢给GPU比你现在的.......希望这些朋友也不要过分的打击吧,每个人都有自己的爱好,我只爱好CPU。

      完整工程下载地址:http://files.cnblogs.com/Imageshop/FindEdges.rar

      同一个图片,本例和PS所得结果有10%左右的差异。

 

 

 ***************************作者: laviewpbt   时间: 2013.7.4    联系QQ:  33184777  转载请保留本行信息*************************

 

 

本文转载自:http://www.cnblogs.com/Imageshop/p/3171425.html

共有 人打赏支持
abcijkxyz
粉丝 63
博文 6196
码字总数 1876
作品 0
深圳
项目经理
私信 提问
8- OpenCV+TensorFlow 入门人工智能图像处理-浮雕效果&油画效果

浮雕效果 浮雕效果与边缘检测类似,也是计算梯度的一个过程 计算公式: 加上150是为了增强图片的浮雕灰度等级。 相邻像素相减,是为了突出灰度的突片。边缘特征。 代码实现 150就是当前的灰度值...

天涯明月笙
2018/05/07
0
0
实时图像处理和机器学习库 - cv4j

The target is to set up a high quality and real-time image process and machine learning library which is implemented in pure java. The framework can run application on java desk......

匿名
2017/06/13
394
0
实景照片秒变新海诚风格漫画:清华大学提出CartoonGAN

  选自CVPR 2018   作者:Yang Chen、Yu-Kun Lai、刘永进   机器之心编译   参与:李泽南、李亚洲      使用漫画风格重现现实世界的场景对于画师来说是一项费时费力——很多时候...

机器之心
2018/06/16
0
0
使用Numpy和Opencv完成图像的基本数据分析(Part IV)

图像 本文是使用python进行图像基本处理系列的第四部分,在本人之前的文章里介绍了一些非常基本的图像分析操作,见文章《使用Numpy和Opencv完成图像的基本数据分析Part I》、《使用Numpy和O...

【方向】
2018/10/10
0
0
模拟油画和铅笔画的滤镜效果

油画效果 先上未经任何处理的原图 原图.png 然后使用油画风格的滤镜OilPaintFilter看看效果,OilPaintFilter的使用方式就一句话:) 油画效果.png OilPaintFilter在处理人物图片和风景图片时...

Tony沈哲
2017/05/12
0
0

没有更多内容

加载失败,请刷新页面

加载更多

Confluence 6 升级中的一些常见问题

升级的时候遇到了问题了吗? 如果你想尝试重新进行升级的话,你需要首先重新恢复老的备份。不要尝试再次对 Confluence 进行升级或者在升级失败后重新启动老的 Confluence。 在升级过程中的一...

honeymoose
今天
2
0
C++随笔(四)Nuget打包

首先把自己编译好的包全部准备到一个文件夹 像这样 接下来新建一个文本文档,后缀名叫.nuspec 填写内容 <?xml version="1.0"?><package xmlns="http://schemas.microsoft.com/packaging/201......

Pulsar-V
今天
2
0
再谈使用开源软件搭建数据分析平台

三年前,我写了这篇博客使用开源软件快速搭建数据分析平台, 当时收到了许多的反馈,有50个点赞和300+的收藏。到现在我还能收到一些关于dataplay2的问题。在过去的三年,开源社区和新技术的发...

naughty
今天
3
0
Python3的日期和时间

python 中处理日期时间数据通常使用datetime和time库 因为这两个库中的一些功能有些重复,所以,首先我们来比较一下这两个库的区别,这可以帮助我们在适当的情况下时候合适的库。 在Python文...

编程老陆
今天
2
0
分布式面试整理

并发和并行 并行是两个任务同时进行,而并发呢,则是一会做一个任务一会又切换做另一个任务。 临界区 临界区用来表示一种公共资源或者说是共享数据,可以被多个线程使用,但是每一次,只能有...

群星纪元
今天
3
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部