多线程--学习篇

  多线程编程是现代软件技术中很重要的一个环节。要弄懂多线程,这就要牵涉到多进程?当然,要了解到多进程,就要涉及到操作系统。不过大家也不要紧张,听我慢慢道来。这其中的环节其实并不复杂。

    (1)单CPU下的多线程

     在没有出现多核CPU之前,我们的计算资源是唯一的。如果系统中有多个任务要处理的话,那么就需要按照某种规则依次调度这些任务进行处理。什么规则呢?可以是一些简单的调度方法,比如说

    1)按照优先级调度

    2)按照FIFO调度

    3)按照时间片调度等等

    当然,除了CPU资源之外,系统中还有一些其他的资源需要共享,比如说内存、文件、端口、socket等。既然前面说到系统中的资源是有限的,那么获取这些资源的最小单元体是什么呢,其实就是进程。

    举个例子来说,在linux上面每一个享有资源的个体称为task_struct,实际上和我们说的进程是一样的。我们可以看看task_structlinux 0.11代码)都包括哪些内容,

[cpp]  view plain copy
  1. struct task_struct {  
  2. /* these are hardcoded - don't touch */  
  3.     long state; /* -1 unrunnable, 0 runnable, >0 stopped */  
  4.     long counter;  
  5.     long priority;  
  6.     long signal;  
  7.     struct sigaction sigaction[32];  
  8.     long blocked;   /* bitmap of masked signals */  
  9. /* various fields */  
  10.     int exit_code;  
  11.     unsigned long start_code,end_code,end_data,brk,start_stack;  
  12.     long pid,father,pgrp,session,leader;  
  13.     unsigned short uid,euid,suid;  
  14.     unsigned short gid,egid,sgid;  
  15.     long alarm;  
  16.     long utime,stime,cutime,cstime,start_time;  
  17.     unsigned short used_math;  
  18. /* file system info */  
  19.     int tty;        /* -1 if no tty, so it must be signed */  
  20.     unsigned short umask;  
  21.     struct m_inode * pwd;  
  22.     struct m_inode * root;  
  23.     struct m_inode * executable;  
  24.     unsigned long close_on_exec;  
  25.     struct file * filp[NR_OPEN];  
  26. /* ldt for this task 0 - zero 1 - cs 2 - ds&ss */  
  27.     struct desc_struct ldt[3];  
  28. /* tss for this task */  
  29.     struct tss_struct tss;  
  30. };  

    每一个task都有自己的pid,在系统中资源的分配都是按照pid进行处理的。这也就说明,进程确实是资源分配的主体。

    这时候,可能有朋友会问了,既然task_struct是资源分配的主体,那为什么又出来thread?为什么系统调度的时候是按照thread调度,而不是按照进程调度呢?原因其实很简单,进程之间的数据沟通非常麻烦,因为我们之所以把这些进程分开,不正是希望它们之间不要相互影响嘛。

    假设是两个进程之间数据传输,那么需要如果需要对共享数据进行访问需要哪些步骤呢,

    1)创建共享内存

    2)访问共享内存->系统调用->读取数据

    3)写入共享内存->系统调用->写入数据

    要是写个代码,大家可能就更明白了,

[cpp]  view plain copy
  1. #include <unistd.h>  
  2. #include <stdio.h>  
  3.   
  4. int value = 10;  
  5.   
  6. int main(int argc, char* argv[])  
  7. {  
  8.     int pid = fork();  
  9.     if(!pid){  
  10.         Value = 12;  
  11.         return 0;  
  12.     }  
  13.     printf("value = %d\n", value);  
  14.     return 1;  
  15. }  

    上面的代码是一个创建子进程的代码,我们发现打印的value数值还是10。尽管中间创建了子进程,修改了value的数值,但是我们发现打印下来的数值并没有发生改变,这就说明了不同的进程之间内存上是不共享的。

    那么,如果修改成thread有什么好处呢?其实最大的好处就是每个thread除了享受单独cpu调度的机会,还能共享每个进程下的所有资源。要是调度的单位是进程,那么每个进程只能干一件事情,但是进程之间是需要相互交互数据的,而进程之间的数据都需要系统调用才能应用,这在无形之中就降低了数据的处理效率。


    (2)多核CPU下的多线程

    没有出现多核之前,我们的CPU实际上是按照某种规则对线程依次进行调度的。在某一个特定的时刻,CPU执行的还是某一个特定的线程。然而,现在有了多核CPU,一切变得不一样了,因为在某一时刻很有可能确实是n个任务在n个核上运行。我们可以编写一个简单的open mp测试一下,如果还是一个核,运行的时间就应该是一样的。

[cpp]  view plain copy
  1. #include <omp.h>  
  2. #define MAX_VALUE 10000000  
  3.   
  4. double _test(int value)  
  5. {  
  6.     int index;  
  7.     double result;  
  8.   
  9.     result = 0.0;  
  10.     for(index = value + 1; index < MAX_VALUE; index +=2 )  
  11.         result += 1.0 / index;  
  12.   
  13.     return result;  
  14. }  
  15.   
  16. void test()  
  17. {  
  18.     int index;  
  19.     int time1;  
  20.     int time2;  
  21.     double value1,value2;  
  22.     double result[2];  
  23.   
  24.     time1 = 0;  
  25.     time2 = 0;  
  26.   
  27.     value1 = 0.0;  
  28.     time1 = GetTickCount();  
  29.     for(index = 1; index < MAX_VALUE; index ++)  
  30.         value1 += 1.0 / index;  
  31.   
  32.     time1 = GetTickCount() - time1;  
  33.   
  34.     value2 = 0.0;  
  35.     memset(result , 0, sizeof(double) * 2);  
  36.     time2 = GetTickCount();  
  37.   
  38. #pragma omp parallel for  
  39.     for(index = 0; index < 2; index++)  
  40.         result[index] = _test(index);  
  41.   
  42.     value2 = result[0] + result[1];  
  43.     time2 = GetTickCount() - time2;  
  44.   
  45.     printf("time1 = %d,time2 = %d\n",time1,time2);  
  46.     return;  
  47. }  

    (3)多线程编程

为什么要多线程编程呢?这其中的原因很多,我们可以举例解决

    1)有的是为了提高运行的速度,比如多核cpu下的多线程

    2)有的是为了提高资源的利用率,比如在网络环境下下载资源时,时延常常很高,我们可以通过不同的thread从不同的地方获取资源,这样可以提高效率

    3)有的为了提供更好的服务,比如说是服务器

    4)其他需要多线程编程的地方等等


数据同步

多线程创建其实十分简单,在windows系统下面有很多函数可以创建多线程,比如说_beginthread。我们就可以利用它为我们编写一段简单的多线程代码,

  1. #include <windows.h>  
  2. #include <process.h>  
  3. #include <stdio.h>  
  4.   
  5. unsigned int value = 0;  
  6.   
  7. void print(void* argv)  
  8. {  
  9.     while(1){  
  10.         printf("&value = %x, value = %d\n", &value, value);  
  11.         value ++;  
  12.         Sleep(1000);  
  13.     }  
  14. }  
  15.   
  16. int main()  
  17. {  
  18.     _beginthread( print, 0, NULL );  
  19.     _beginthread( print, 0, NULL);  
  20.   
  21.     while(1)   
  22.         Sleep(0);  
  23.   
  24.     return 1;  
  25. }  

    注意,在VC上面编译的时候,需要打开/MD开关。具体操作为,【project->setting->c/c++->CategoryCode Generation->Use run-time library->Debug Multithreaded】即可。

    通过上面的示例,我们看到作为共享变量的value事实上是可以被所有的线程访问的。这就是线程数据同步的最大优势——方便,直接。因为线程之间除了堆栈空间不一样之外,代码段和数据段都是在一个空间里面的。所以,线程想访问公共数据,就可以访问公共数据,没有任何的限制。

    当然,事物都有其两面性。这种对公共资源的访问模式也会导致一些问题。什么问题呢?我们看了就知道了。

    现在假设有一个池塘,我们雇两个人来喂鱼。两个人不停地对池塘里面的鱼进行喂食。我们规定在一个人喂鱼的时候,另外一个人不需要再喂鱼,否则鱼一次喂两回就要撑死了。为此,我们安装了一个牌子作为警示。如果一个人在喂鱼,他会把牌子设置为FALSE,那么另外一个人看到这个牌子,就不会继续喂鱼了。等到这个人喂完后,他再把牌子继续设置为TRUE

    如果我们需要把这个故事写成代码,那么怎么写呢?朋友们试试看,

  1. while(1){  
  2.     if( flag == true){  
  3.         flag = false;  
  4.         do_give_fish_food();  
  5.         flag = true;  
  6.     }  
  7.   
  8.     Sleep(0);  
  9. }  

    上面的代码看上去没有问题了,但是大家看看代码的汇编代码,看看是不是存在隐患。因为还会出现两个人同时喂食的情况,

  1. 23:       while(1){  
  2. 004010E8   mov         eax,1  
  3. 004010ED   test        eax,eax  
  4. 004010EF   je          do_action+56h (00401126)  
  5. 24:           if( flag == true){  
  6. 004010F1   cmp         dword ptr [flag (00433e04)],1  
  7. 004010F8   jne         do_action+43h (00401113)  
  8. 25:               flag = false;  
  9. 004010FA   mov         dword ptr [flag (00433e04)],0  
  10. 26:               do_give_fish_food();  
  11. 00401104   call        @ILT+15(do_give_fish_food) (00401014)  
  12. 27:               flag = true;  
  13. 00401109   mov         dword ptr [flag (00433e04)],1  
  14. 28:           }  
  15. 29:  
  16. 30:           Sleep(0);  
  17. 00401113   mov         esi,esp  
  18. 00401115   push        0  
  19. 00401117   call        dword ptr [__imp__Sleep@4 (004361c4)]  
  20. 0040111D   cmp         esi,esp  
  21. 0040111F   call        __chkesp (004011e0)  
  22. 31:       }  
  23. 00401124   jmp         do_action+18h (004010e8)  
  24. 32:   }  

    我们此时假设有两个线程ab在不停地进行判断和喂食操作。设置当前flag = true,此时线程a执行到004010F8处时,判断鱼还没有喂食,正准备执行指令004010F8,但是还没有来得及对falg进行设置,此时出现了线程调度。线程b运行到004010F8时,发现当前没有人喂食,所以执行喂食操作。等到b线程喂食结束,运行到00401113的时候,此时又出现了调度。线程a有继续运行,因为之前已经判断了当前还没有喂食,所以线程a继续进行了喂食了操作。所以,可怜的鱼,这一次就连续经历了两次喂食操作,估计有一部分鱼要撑死了。

    当然鱼在这里之所以会出现撑死的情况,主要是因为line 24line 25之间出现了系统调度。所以,我们在编写程序的时候必须有一个牢固的思想意识,如果缺少必须要的手段,程序可以任何时刻任何地点被调度,那此时公共数据的计算就会出现错误。


数据互斥

在多线程存在的环境中,除了堆栈中的临时数据之外,所有的数据都是共享的。如果我们需要线程之间正确地运行,那么务必需要保证公共数据的执行和计算是正确的。简单一点说,就是保证数据在执行的时候必须是互斥的。否则,如果两个或者多个线程在同一时刻对数据进行了操作,那么后果是不可想象的。

    也许有的朋友会说,不光数据需要保护,代码也需要保护。提出这个观点的朋友只看到了数据访问互斥的表象。在程序的运行空间里面,什么最重要的呢?代码吗?当然不是。代码只是为了数据的访问存在的。数据才是我们一切工作的出发点和落脚点。

    那么,有什么办法可以保证在某一时刻只有一个线程对数据进行操作呢?四个基本方法:

    (1)关中断

    (2)数学互斥方法

    (3)操作系统提供的互斥方法

    (4)cpu原子操作

    为了让大家可以对这四种方法有详细的认识,我们可以进行详细的介绍。

  

    (1)关中断

    要让数据在某一时刻只被一个线程访问,方法之一就是停止线程调度就可以了。那么怎样停止线程调度呢?那么关掉时钟中断就可以了啊。在X86里面的确存在这样的两个指令,

[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. #include <stdio.h>  
  2.   
  3. int main()  
  4. {  
  5.     __asm{  
  6.         cli  
  7.         sti  
  8.     }  
  9.     return 1;  
  10. }  

    其中cli是关中断,sti是开中断。这段代码没有什么问题,可以编过,当然也可以生成执行文件。但是在执行的时候会出现一个异常告警:Unhandled exception in test.exe: 0xC0000096:  Privileged Instruction。告警已经说的很清楚了,这是一个特权指令。只有系统或者内核本身才可以使用这个指令。

    不过,大家也可以想象一下。因为平常我们编写的程序都是应用级别的程序,要是每个程序都是用这些代码,那不乱了套了。比如说,你不小心安装一个低质量的软件,说不定什么时候把你的中断关了,这样你的网络就断了,你的输入就没有回应了,你的音乐什么都没有了,这样的环境你受的了吗?应用层的软件是千差万别的,软件的水平也是参差不齐的,所以系统不可能相信任何一个私有软件,它相信的只是它自己。

 

    (2)数学方法

    假设有两个线程(a、b)正要对一个共享数据进行访问,那么怎么做到他们之间的互斥的呢?其实我们可以这么做,

[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. unsigned int flag[2] = {0};  
  2. unsigned int turn = 0;  
  3.   
  4. void process(unsigned int index)  
  5. {  
  6.     flag[index] = 1;  
  7.     turn =  1 - index;  
  8.   
  9.     while(flag[1 - index] && (turn == (1 - index)));  
  10.     do_something();  
  11.     flag[index] = 0;  
  12. }  

    其实,学过操作系统的朋友都知道,上面的算法其实就是Peterson算法,可惜它只能用于两个线程的数据互斥。当然,这个算法还可以推广到更多线程之间的互斥,那就是bakery算法。但是数学算法有两个缺点:

    a)占有空间多,两个线程就要flag占两个单位空间,那么n个线程就要n个flag空间,

    b)代码编写复杂,考虑的情况比较复杂

 

    (3)系统提供的互斥算法

    系统提供的互斥算法其实是我们平时开发中用的最多的互斥工具。就拿windows来说,关于互斥的工具就有临界区、互斥量、信号量等等。这类算法有一个特点,那就是都是依据系统提高的互斥资源,那么系统又是怎么完成这些功能的呢?其实也不难。

    系统加锁过程,

[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. void Lock(HANDLE hLock)  
  2. {  
  3.     __asm {cli};  
  4.   
  5.     while(1){  
  6.         if(/* 锁可用*/){  
  7.             /* 设定标志,表明当前锁已被占用 */  
  8.             __asm {sti};  
  9.             return;  
  10.         }  
  11.   
  12.         __asm{sti};  
  13.         schedule();  
  14.         __asm{cli};  
  15.     }  
  16. }  

    系统解锁过程,

[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. void UnLock(HANDLE hLock)  
  2. {  
  3.     __asm {cli};  
  4.     /* 设定标志, 当前锁可用 */  
  5.     __asm{sti};  
  6. }  

    上面其实讨论的就是一种最简单的系统锁情况。中间没有涉及到就绪线程的压入和弹出过程,没有涉及到资源个数的问题,所以不是很复杂。朋友们仔细看看,应该都可以明白代码表达的是什么意思。

 

    (4)CPU的原子操作 
    因为在多线程操作当中,有很大一部分是比较、自增、自减等简单操作。因为需要互斥的代码很少,所以使用互斥量、信号量并不合算。因此,CPU厂商为了开发的方便,把一些常用的指令设计成了原子指令,在windows上面也被称为原子锁,常用的原子操作函数有

[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. InterLockedAdd  
  2.   
  3. InterLockedExchange  
  4.   
  5. InterLockedCompareExchange  
  6.   
  7. InterLockedIncrement  
  8.   
  9. InterLockedDecrement  
  10.   
  11. InterLockedAnd  
  12.   
  13. InterLockedOr  

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
提供的源码资源涵盖了Java应用等多个领域,每个领域都包含了丰富的实例和项目。这些源码都是基于各自平台的最新技术和标准编写,确保了在对应环境下能够无缝运行。同时,源码中配备了详细的注释和文档,帮助用户快速理解代码结构和实现逻辑。 适用人群: 适合毕业设计、课程设计作业。这些源码资源特别适合大学生群体。无论你是计算机相关专业的学生,还是对其他领域编程感兴趣的学生,这些资源都能为你提供宝贵的学习和实践机会。通过学习和运行这些源码,你可以掌握各平台开发的基础知识,提升编程能力和项目实战经验。 使用场景及目标: 在学习阶段,你可以利用这些源码资源进行课程实践、课外项目或毕业设计。通过分析和运行源码,你将深入了解各平台开发的技术细节和最佳实践,逐步培养起自己的项目开发和问题解决能力。此外,在求职或创业过程中,具备跨平台开发能力的大学生将更具竞争力。 其他说明: 为了确保源码资源的可运行性和易用性,特别注意了以下几点:首先,每份源码都提供了详细的运行环境和依赖说明,确保用户能够轻松搭建起开发环境;其次,源码中的注释和文档都非常完善,方便用户快速上手和理解代码;最后,我会定期更新这些源码资源,以适应各平台技术的最新发展和市场需求。 所有源码均经过严格测试,可以直接运行,可以放心下载使用。有任何使用问题欢迎随时与博主沟通,第一时间进行解答!

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值