zoukankan      html  css  js  c++  java
  • VC++ 线程同步 总结

    注:所谓同步,并不是多个线程一起同时执行,而是他们协同步调,按预定的先后次序执行。

    与线程相关的基本函数包括:
    CreateThread:创建线程
    CloseHandle:关闭线程句柄。注意,这只会使指定的线程句柄无效(减少该句柄的引用计数),启动句柄的检查操作,如果一个对象所关联的最后一个句柄被关闭了,那么这个对象会从系统中被删除。关闭句柄不会终止相关的线程。

    线程是如何运行的呢?这又与你的CPU有关系了,如果你是一个单核CPU,那么系统会采用时间片轮询的方式运行每个线程;如果你是多核CPU,那么线程之间就有可能并发运行了。这样就会出现很多问题,比如两个线程同时访问一个全局变量之类的。它们需要线程的同步来解决。所谓同步,并不是多个线程一起同时执行,而是他们协同步调,按预定的先后次序执行。
    Windows下线程同步的基本方法有3种:互斥对象、事件对象、关键代码段(临界区),下面一一介绍:

    互斥对象属于内核对象,包含3个成员:
    1.使用数量:记录了有多少个线程在调用该对象
    2.一个线程ID:记录互斥对象维护的线程的ID
    3.一个计数器:前线程调用该对象的次数
    与之相关的函数包括:
    创建互斥对象:CreateMutex
    判断能否获得互斥对象:WaitForSingleObject
    对于WaitForSingleObject,如果互斥对象为有信号状态,则获取成功,函数将互斥对象设置为无信号状态,程序将继续往下执行;如果互斥对象为无信号状态,则获取失败,线程会停留在这里等待。等待的时间可以由参数控制。
    释放互斥对象:ReleaseMutex
    当要保护的代码执行完毕后,通过它来释放互斥对象,使得互斥对象变为有信号状态,以便于其他线程可以获取这个互斥对象。注意,只有当某个线程拥有互斥对象时,才能够释放互斥对象,在其他线程调用这个函数不得达到释放的效果,这可以通过互斥对象的线程ID来判断。

     1 #include <Windows.h>
     2 #include <stdio.h>
     3 
     4 //线程函数声明
     5 DWORD WINAPI Thread1Proc(  LPVOID lpParameter);
     6 DWORD WINAPI Thread2Proc(  LPVOID lpParameter);
     7 
     8 //全局变量
     9 int tickets = 100;
    10 HANDLE hMutex;
    11 
    12 int main()
    13 {
    14     HANDLE hThread1;
    15     HANDLE hThread2;
    16     //创建互斥对象
    17     hMutex = CreateMutex( NULL,            //默认安全级别
    18                           FALSE,        //创建它的线程不拥有互斥对象
    19                           NULL);        //没有名字
    20     //创建线程1
    21     hThread1 = CreateThread(NULL,        //默认安全级别
    22                             0,            //默认栈大小
    23                             Thread1Proc,//线程函数 
    24                             NULL,        //函数没有参数
    25                             0,            //创建后直接运行
    26                             NULL);        //线程标识,不需要
    27 
    28     //创建线程2
    29     hThread2 = CreateThread(NULL,        //默认安全级别
    30                             0,            //默认栈大小
    31                             Thread2Proc,//线程函数 
    32                             NULL,        //函数没有参数
    33                             0,            //创建后直接运行
    34                             NULL);        //线程标识,不需要
    35 
    36     //主线程休眠4秒
    37     Sleep(4000);
    38     //主线程休眠4秒
    39     Sleep(4000);
    40     //关闭线程句柄
    41     CloseHandle(hThread1);
    42     CloseHandle(hThread2);
    43 
    44     //释放互斥对象
    45     ReleaseMutex(hMutex);
    46     return 0;
    47 }
    48 
    49 //线程1入口函数
    50 DWORD WINAPI Thread1Proc(  LPVOID lpParameter)
    51 {
    52     while(TRUE)
    53     {
    54         WaitForSingleObject(hMutex,INFINITE);
    55         if(tickets > 0)
    56         {
    57             Sleep(10);
    58             printf("thread1 sell ticket : %d
    ",tickets--);
    59             ReleaseMutex(hMutex);
    60         }
    61         else
    62         {
    63             ReleaseMutex(hMutex);
    64             break;
    65         }
    66     }
    67 
    68     return 0;
    69 }
    70 
    71 //线程2入口函数
    72 DWORD WINAPI Thread2Proc(  LPVOID lpParameter)
    73 {
    74     while(TRUE)
    75     {
    76         WaitForSingleObject(hMutex,INFINITE);
    77         if(tickets > 0)
    78         {
    79             Sleep(10);
    80             printf("thread2 sell ticket : %d
    ",tickets--);
    81             ReleaseMutex(hMutex);
    82         }
    83         else
    84         {
    85             ReleaseMutex(hMutex);
    86             break;
    87         }
    88     }
    89 
    90     return 0;
    91 }
    1 使用互斥对象时需要小心:
    2 调用假如一个线程本身已经拥有该互斥对象,则如果它继续调用WaitForSingleObject,则会增加互斥对象的引用计数,此时,你必须多次调用ReleaseMutex来释放互斥对象,以便让其他线程可以获取:
    1     //创建互斥对象
    2     hMutex = CreateMutex( NULL,            //默认安全级别
    3                           TRUE,            //创建它的线程拥有互斥对象
    4                           NULL);        //没有名字
    5     WaitForSingleObject(hMutex,INFINITE);
    6     //释放互斥对象
    7     ReleaseMutex(hMutex);
    8     //释放互斥对象
    9     ReleaseMutex(hMutex);

    下面看事件对象,它也属于内核对象,包含3各成员:
    1.使用计数
    2.用于指明该事件是自动重置事件还是人工重置事件的布尔值
    3.用于指明该事件处于已通知状态还是未通知状态。
    自动重置和人工重置的事件对象有一个重要的区别:当人工重置的事件对象得到通知时,等待该事件对象的所有线程均变为可调度线程;而当一个自动重置的事件对象得到通知时,等待该事件对象的线程中只有一个线程变为可调度线程。
    与事件对象相关的函数包括:
    创建事件对象:CreateEvent
    HANDLE CreateEvent(  LPSECURITY_ATTRIBUTES lpEventAttributes, BOOL bManualReset, BOOL bInitialState,LPCTSTR lpName);
    设置事件对象:SetEvent:将一个这件对象设为有信号状态
    BOOL SetEvent(  HANDLE hEvent  );
    重置事件对象状态:ResetEvent:将指定的事件对象设为无信号状态
    BOOL ResetEvent(  HANDLE hEvent );

    下面仍然使用买火车票的例子:

     1 #include <Windows.h>
     2 #include <stdio.h>
     3 
     4 //线程函数声明
     5 DWORD WINAPI Thread1Proc(  LPVOID lpParameter);
     6 DWORD WINAPI Thread2Proc(  LPVOID lpParameter);
     7 
     8 //全局变量
     9 int tickets = 100;
    10 HANDLE g_hEvent;
    11 
    12 int main()
    13 {
    14     HANDLE hThread1;
    15     HANDLE hThread2;
    16     //创建事件对象
    17     g_hEvent = CreateEvent( NULL,    //默认安全级别
    18                             TRUE,    //人工重置
    19                             FALSE,    //初始为无信号
    20                             NULL );    //没有名字
    21     //创建线程1
    22     hThread1 = CreateThread(NULL,        //默认安全级别
    23                             0,            //默认栈大小
    24                             Thread1Proc,//线程函数 
    25                             NULL,        //函数没有参数
    26                             0,            //创建后直接运行
    27                             NULL);        //线程标识,不需要
    28 
    29     //创建线程2
    30     hThread2 = CreateThread(NULL,        //默认安全级别
    31                             0,            //默认栈大小
    32                             Thread2Proc,//线程函数 
    33                             NULL,        //函数没有参数
    34                             0,            //创建后直接运行
    35                             NULL);        //线程标识,不需要
    36 
    37 
    38     //主线程休眠4秒
    39     Sleep(4000);
    40     //关闭线程句柄
    41     //当不再引用这个句柄时,立即将其关闭,减少其引用计数
    42     CloseHandle(hThread1);
    43     CloseHandle(hThread2);
    44     //关闭事件对象句柄
    45     CloseHandle(g_hEvent);
    46     return 0;
    47 }
    48 
    49 //线程1入口函数
    50 DWORD WINAPI Thread1Proc(  LPVOID lpParameter)
    51 {
    52     while(TRUE)
    53     {
    54         WaitForSingleObject(g_hEvent,INFINITE);
    55         if(tickets > 0)
    56         {
    57             Sleep(1);
    58             printf("thread1 sell ticket : %d
    ",tickets--);
    59         }
    60         else
    61             break;
    62     }
    63 
    64     return 0;
    65 }
    66 
    67 //线程2入口函数
    68 DWORD WINAPI Thread2Proc(  LPVOID lpParameter)
    69 {
    70     while(TRUE)
    71     {
    72         WaitForSingleObject(g_hEvent,INFINITE);
    73         if(tickets > 0)
    74         {
    75             Sleep(1);
    76             printf("thread2 sell ticket : %d
    ",tickets--);
    77         }
    78         else
    79             break;
    80     }
    81 
    82     return 0;
    83 }

    程序运行后并没有出现两个线程买票的情况,而是等待了4秒之后直接退出了,这是什么原因呢?问题出在了我们创建的事件对象一开始就是无信号状态的,因此2个线程线程运行到WaitForSingleObject时就会一直等待,直到自己的时间片结束。所以什么也不会输出。
    如果想让线程能够执行,可以在创建线程时将第3个参数设为TRUE,或者在创建完成后调用

    1     SetEvent(g_hEvent);

    程序的确可以实现买票了,但是有些时候,会打印出某个线程卖出第0张票的情况,这是因为当人工重置的事件对象得到通知时,等待该对象的所有线程均可变为可调度线程,两个线程同时运行,线程的同步失败了。

    也许有人会想到,在线程得到CPU之后,能否使用ResetEvent是得线程将事件对象设为无信号状态,然后当所保护的代码运行完成后,再将事件对象设为有信号状态?我们可以试试:

     1 //线程1入口函数
     2 DWORD WINAPI Thread1Proc(  LPVOID lpParameter)
     3 {
     4     while(TRUE)
     5     {
     6         WaitForSingleObject(g_hEvent,INFINITE);
     7         ResetEvent(g_hEvent);
     8         if(tickets > 0)
     9         {
    10             Sleep(10);
    11             printf("thread1 sell ticket : %d
    ",tickets--);
    12             SetEvent(g_hEvent);
    13         }
    14         else
    15         {
    16             SetEvent(g_hEvent);
    17             break;
    18         }
    19     }
    20 
    21     return 0;
    22 }

    线程2的类似,这里就省略了。运行程序,发现依然会出现卖出第0张票的情况。这是为什么呢?我们仔细思考一下:单核CPU下,可能线程1执行完WaitForSingleObject,还没来得及执行ResetEvent时,就切换到线程2了,此时,由于线程1并没有执行ResetEvent,所以线程2也可以得到事件对象了。而在多CPU平台下,假如两个线程同时执行,则有可能都执行到本应被保护的代码区域。
    所以,为了实现线程间的同步,不应该使用人工重置的事件对象,而应该使用自动重置的事件对象:

    1     hThread2 = CreateThread(NULL,0,Thread2Proc,NULL0,NULL);

    并将原来写的ResetEvent和SetEvent全都注释起来。我们发现程序只打印了一次买票过程。我们分析一下原因:
    当一个自动重置的事件得到通知后,等待该该事件的线程中只有一个变为可调度线程。在这里,线程1变为可调度线程后,操作系统将事件设为了无信号状态,当线程1休眠时,所以线程2只能等待,时间片结束以后,又轮到线程1运行,输出thread1 sell ticket :100。然后循环,又去WaitForSingleObject,而此时事件对象处于无信号状态,所以线程不能继续往下执行,只能一直等待,等到自己时间片结束,直到主线程睡醒了,结束整个程序。
    正确的使用方法是:当访问完对保护的代码段后,立即调用SetEvent将其设为有信号状态。允许其他等待该对象的线程变为可调度状态:

     1 DWORD WINAPI Thread1Proc(  LPVOID lpParameter)
     2 {
     3     while(TRUE)
     4     {
     5         WaitForSingleObject(g_hEvent,INFINITE);
     6         if(tickets > 0)
     7         {
     8             Sleep(10);
     9             printf("thread1 sell ticket : %d
    ",tickets--);
    10             SetEvent(g_hEvent);
    11         }
    12         else
    13         {
    14             SetEvent(g_hEvent);
    15             break;
    16         }
    17     }
    18 
    19     return 0;
    20 }

    总结一下:事件对象要区分人工重置事件还是自动重置事件。如果是人工重置的事件对象得到通知,则等待该事件对象的所有线程均变为可调度线程;当一个自动重置的事件对象得到通知时,只有一个等待该事件对象的线程变为可调度线程,且操作系统会将该事件对象设为无信号状态。因此,当执行完受保护的代码后,需要调用SetEvent将事件对象设为有信号状态。

    下面介绍另一种线程同步的方法:关键代码段。
    关键代码段又称为临界区,工作在用户方式下。它是一小段代码,但是在代码执行之前,必须独占某些资源的访问权限。
    我们先介绍与之先关的API函数:
    使用InitializeCriticalSection初始化关键代码段
    使用EnterCriticalSection进入关键代码段:
    使用LeaveCriticalSection离开关键代码段:
    使用DeleteCriticalSection删除关键代码段,释放资源
    我们看一个例子:

     1 #include <Windows.h>
     2 #include <stdio.h>
     3 
     4 //线程函数声明
     5 DWORD WINAPI Thread1Proc(  LPVOID lpParameter);
     6 DWORD WINAPI Thread2Proc(  LPVOID lpParameter);
     7 
     8 //全局变量
     9 int tickets = 100;
    10 CRITICAL_SECTION g_cs;
    11 
    12 int main()
    13 {
    14     HANDLE hThread1;
    15     HANDLE hThread2;
    16     //初始化关键代码段
    17     InitializeCriticalSection(&g_cs);
    18     //创建线程1
    19     hThread1 = CreateThread(NULL,        //默认安全级别
    20                             0,            //默认栈大小
    21                             Thread1Proc,//线程函数 
    22                             NULL,        //函数没有参数
    23                             0,            //创建后直接运行
    24                             NULL);        //线程标识,不需要
    25 
    26     //创建线程2
    27     hThread2 = CreateThread(NULL,        //默认安全级别
    28                             0,            //默认栈大小
    29                             Thread2Proc,//线程函数 
    30                             NULL,        //函数没有参数
    31                             0,            //创建后直接运行
    32                             NULL);        //线程标识,不需要
    33 
    34 
    35     //主线程休眠4秒
    36     Sleep(4000);
    37     //关闭线程句柄
    38     CloseHandle(hThread1);
    39     CloseHandle(hThread2);
    40     //关闭事件对象句柄
    41     DeleteCriticalSection(&g_cs);
    42     return 0;
    43 }
    44 
    45 //线程1入口函数
    46 DWORD WINAPI Thread1Proc(  LPVOID lpParameter)
    47 {
    48     while(TRUE)
    49     {
    50         //进入关键代码段前调用该函数判断否能得到临界区的使用权
    51         EnterCriticalSection(&g_cs);
    52         Sleep(1);
    53         if(tickets > 0)
    54         {
    55             Sleep(1);
    56             printf("thread1 sell ticket : %d
    ",tickets--);
    57             //访问结束后释放临界区对象的使用权
    58             LeaveCriticalSection(&g_cs);
    59             Sleep(1);
    60         }
    61         else
    62         {
    63             LeaveCriticalSection(&g_cs);
    64             break;
    65         }
    66     }
    67 
    68     return 0;
    69 }
    70 
    71 //线程2入口函数
    72 DWORD WINAPI Thread2Proc(  LPVOID lpParameter)
    73 {
    74     while(TRUE)
    75     {
    76         //进入关键代码段前调用该函数判断否能得到临界区的使用权
    77         EnterCriticalSection(&g_cs);
    78         Sleep(1);
    79         if(tickets > 0)
    80         {
    81             Sleep(1);
    82             printf("thread2 sell ticket : %d
    ",tickets--);
    83             //访问结束后释放临界区对象的使用权
    84             LeaveCriticalSection(&g_cs);
    85             Sleep(1);
    86         }
    87         else
    88         {
    89             LeaveCriticalSection(&g_cs);
    90             break;
    91         }
    92     }
    93 
    94     return 0;
    95 }

    在这个例子中,通过在放弃临界区资源后,立即睡眠引起另一个线程被调用,导致两个线程交替售票。
    下面看一个多线程程序中常犯的一个错误-线程死锁。死锁产生的原因,举个例子:线程1拥有临界区资源A,正在等待临界区资源B;而线程2拥有临界区资源B,正在等待临界区资源A。它俩各不相让,结果谁也执行不了。我们看看程序:

      1 #include <Windows.h>
      2 #include <stdio.h>
      3 
      4 //线程函数声明
      5 DWORD WINAPI Thread1Proc(  LPVOID lpParameter);
      6 DWORD WINAPI Thread2Proc(  LPVOID lpParameter);
      7 
      8 //全局变量
      9 int tickets = 100;
     10 CRITICAL_SECTION g_csA;
     11 CRITICAL_SECTION g_csB;
     12 int main()
     13 {
     14     HANDLE hThread1;
     15     HANDLE hThread2;
     16     //初始化关键代码段
     17     InitializeCriticalSection(&g_csA);
     18     InitializeCriticalSection(&g_csB);
     19     //创建线程1
     20     hThread1 = CreateThread(NULL,        //默认安全级别
     21                             0,            //默认栈大小
     22                             Thread1Proc,//线程函数 
     23                             NULL,        //函数没有参数
     24                             0,            //创建后直接运行
     25                             NULL);        //线程标识,不需要
     26 
     27     //创建线程2
     28     hThread2 = CreateThread(NULL,        //默认安全级别
     29                             0,            //默认栈大小
     30                             Thread2Proc,//线程函数 
     31                             NULL,        //函数没有参数
     32                             0,            //创建后直接运行
     33                             NULL);        //线程标识,不需要
     34     //关闭线程句柄
     35     //当不再引用这个句柄时,立即将其关闭,减少其引用计数
     36     CloseHandle(hThread1);
     37     CloseHandle(hThread2);
     38 
     39     //主线程休眠4秒
     40     Sleep(4000);
     41 
     42     //关闭事件对象句柄
     43     DeleteCriticalSection(&g_csA);
     44     DeleteCriticalSection(&g_csB);
     45     return 0;
     46 }
     47 
     48 //线程1入口函数
     49 DWORD WINAPI Thread1Proc(  LPVOID lpParameter)
     50 {
     51     while(TRUE)
     52     {
     53         EnterCriticalSection(&g_csA);
     54         Sleep(1);
     55         EnterCriticalSection(&g_csB);
     56         if(tickets > 0)
     57         {
     58             Sleep(1);
     59             printf("thread1 sell ticket : %d
    ",tickets--);
     60             LeaveCriticalSection(&g_csB);
     61             LeaveCriticalSection(&g_csA);
     62             Sleep(1);
     63         }
     64         else
     65         {
     66             LeaveCriticalSection(&g_csB);
     67             LeaveCriticalSection(&g_csA);
     68             break;
     69         }
     70     }
     71 
     72     return 0;
     73 }
     74 
     75 //线程2入口函数
     76 DWORD WINAPI Thread2Proc(  LPVOID lpParameter)
     77 {
     78     while(TRUE)
     79     {
     80         EnterCriticalSection(&g_csB);
     81         Sleep(1);
     82         EnterCriticalSection(&g_csA);
     83         if(tickets > 0)
     84         {
     85             Sleep(1);
     86             printf("thread2 sell ticket : %d
    ",tickets--);
     87             LeaveCriticalSection(&g_csA);
     88             LeaveCriticalSection(&g_csB);
     89             Sleep(1);
     90         }
     91         else
     92         {
     93             LeaveCriticalSection(&g_csA);
     94             LeaveCriticalSection(&g_csB);
     95             break;
     96         }
     97     }
     98 
     99     return 0;
    100 }

    在程序中,创建了两个临界区对象g_csA和g_csB。线程1中先尝试获取g_csA,获取成功后休眠,线程2尝试获取g_csB,成功后休眠,切换回线程1,然后线程1试图获取g_csB,因为g_csB已经被线程2获取,所以它线程1的获取不会成功,一直等待,直到自己的时间片结束后,转到线程2,线程2获取g_csB后,试图获取g_csA,当然也不会成功,转回线程1……这样交替等待,直到主线程睡醒,执行完毕,程序结束。

  • 相关阅读:
    C#.Net Winform 应用程序莫名其妙崩溃。
    不小心点击安装了搜狗手机助手,顿时有一种草搜狗全体人员的感觉。
    家乐福张江店班车时刻表
    为什么学习设计模式
    同一端口如何区分不同的Socket
    用命令行CMD .bat 相关操作 如: 创建快捷方式 复制文件等
    C++ 时间获取和时间测量
    get all ODBC drivers 驱动
    命令行 编译C#.NET项目
    如何打开.hlp文件指定的topic
  • 原文地址:https://www.cnblogs.com/chechen/p/5358451.html
Copyright © 2011-2022 走看看