Chinaunix首页 | 论坛 | 博客
  • 博客访问: 1614843
  • 博文数量: 441
  • 博客积分: 20087
  • 博客等级: 上将
  • 技术积分: 3562
  • 用 户 组: 普通用户
  • 注册时间: 2006-06-19 15:35
文章分类

全部博文(441)

文章存档

2014年(1)

2012年(1)

2011年(8)

2010年(16)

2009年(15)

2008年(152)

2007年(178)

2006年(70)

分类: C/C++

2008-04-18 10:07:08

临界区:多线程同步

对于多线程的程序来说,线程之间的同步永远是个重要的问题。如果多个线程都要存取同样的对象(如

存取同样的内存变量或读写同一个文件等),而一个线程操作的结果反过来又会影响另一个线程的运行

的时候,同步问题就变得异常重要。

产生同步问题的根源在于线程之间的切换是无法预测的,一个线程无法知道什么时候自己的时间片会结

束,也无法知道下一个时间片会被分配给哪个线程。事实上,线程可以在任何地方被Windows打断,惟

一可以确定的事实就是线程只能在两条指令之间被打断,因为指令是CPU执行的最小单位,线程不可能

在一条指令执行到一半的时候被打断。

对于单线程的程序来说,主线程在单个时间片结束的时候被Windows挂起,然后在轮到下一个时间片的

时候继续执行,这中间整个进程的环境不会有任何改变,因为进程中不存在其他线程。但对于多线程的

程序来说,在主线程挂起的过程中可能会有子线程被分配了时间片,如果子线程在执行中改变了主线程

正在存取的对象,就可能会引发错误的结果。举一个例子来说明,假定往银行账户里存钱的操作步骤有3

步:

(1)获取账户的余额。

(2)将用户存入的数额和余额相加,得到新的余额。

(3)将账户中的余额数据更新为新的数值。

现在从两个不同的储蓄所里同时向一个账户存钱,假设原来的余额是10 000元,储蓄所A要存入1 000

元,储蓄所B要存入2 000元,如果没有同步机制,就可能发生下面的情况:

① 蓄所A首先执行第(1)步,获得余额数据10 000,然后进行第(2)步运算得到结果11 000元。

② 这时储蓄所B的业务也刚好发生,在储蓄所A计算第(2)步的过程中,储蓄所B执行了第(1)步,由

于储蓄所A还没有执行到第(3)步,所以账户余额还没有被更新,储蓄所B得到的余额数据还是10 000

元。

③ 储蓄所B计算新余额,得到结果12 000元。

④ 在储蓄所B计算新余额的过程中,储蓄所A执行了第(3)步,将余额更新为11 000元。

⑤ 最后,储蓄所B执行了第(3)步,将自己的计算结果12 000元更新到余额数据中。

结果就是储蓄所A的业务实际上是丢失了;另一种情况,假如储蓄所B的动作很快,在上面的第④步骤发

生之前,在第③步骤中就将计算结果12 000更新到余额数据中了,那么在第④步骤中储蓄所A的计算结

果11 000就会将12 000覆盖,这时的结果就是储蓄所B的业务丢失了,所以同步问题产生的错误结果是

很难预测的。

将这个比喻引伸到两个线程的同步问题上就表现在:假如线程A将某个内存变量的值取到eax寄存器中,

准备在经过运算后将结果写回去,这时被Windows切换到线程B中,线程B在这个时间片中对同一个内

存变量进行了修改,当切换回线程A的时候,线程A在上一个时间片中刚取到eax中的数值和内存变量中

的值就不同步了,计算结果当然就是错误的。

有人可能会认为出现这种情况的概率是很低的,线程中有这么多条指令要执行,难道偏偏就在程序取完

数据还没开始处理的时候被系统打断吗?通过下面的例子就可以发现发生这种事情的可能性有多大。

VC6++新建一个win32应用程序,代码如下:

// ThreadSyn.c

#include
#include "resource.h"

HWND        hWinMain = NULL;
HWND        hWinCount = NULL;
DWORD        dwThreads = 0;

DWORD        dwOption = 0;
#define        F_STOP    0x0001

DWORD        dwCounter1 = 0;
DWORD        dwCounter2 = 0;

char        szStop[] = TEXT("停止计数");
char        szStart[] = TEXT("计数");

DWORD WINAPI Counter(LPVOID lpParameter);

LRESULT CALLBACK DialogProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM

lParam);

int WINAPI WinMain(IN HINSTANCE hInstance, IN HINSTANCE hPrevInstance, IN

LPSTR lpCmdLine, IN int nShowCmd )
{
    DialogBoxParam(hInstance, MAKEINTRESOURCE(IDD_MAIN), NULL,

DialogProc, 0);
    return 0;
}

LRESULT CALLBACK DialogProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM

lParam)
{
    int i = 0;
    HANDLE hThread = NULL;
    switch ( uMsg )
    {
    case WM_TIMER:
        SetDlgItemInt(hWinMain, IDC_COUNTER1, dwCounter1, FALSE);
        SetDlgItemInt(hWinMain, IDC_COUNTER2, dwCounter2, FALSE);
        break;
    case WM_COMMAND:
        if ( LOWORD(wParam) == IDOK )
        {
            if ( dwThreads )
            {
                dwOption |= F_STOP;
                KillTimer(hWnd, 1);
            }
            else
            {
                dwCounter1 = 0;
                dwCounter2 = 0;
                while ( i < 10 )    
                {
                    hThread = CreateThread(NULL, 0,

Counter, NULL, 0, NULL);
                    CloseHandle(hThread);
                    i++;
                }
                SetTimer(hWnd, 1, 500, NULL);
            }
        }
        break;
    case WM_CLOSE:
        if ( !dwThreads )
            EndDialog(hWnd, 0);
        break;
    case WM_INITDIALOG:
        hWinMain = hWnd;
        hWinCount = GetDlgItem(hWinMain, IDOK);        
        break;
    default:
        return FALSE;
    }
    return TRUE;
}

DWORD WINAPI Counter(LPVOID lpParameter)
{
    DWORD dwTmp;
    dwThreads++;
    SetWindowText(hWinCount, szStop);
    dwOption &= ~F_STOP;

    while ( !(dwOption & F_STOP) )
    {
        ++dwCounter1;
        dwTmp = dwCounter2;
        ++dwTmp;
        dwCounter2 = dwTmp;
    }
    dwThreads--;
    SetWindowText(hWinCount, szStart);

    return 0;
}


// resource.h
#define IDD_MAIN                        101
#define IDC_COUNTER1                    1000
#define IDC_COUNTER2                    1001
#define IDC_STATIC                      -1


// ThreadSyn.rc
#include "resource.h"
#include "afxres.h"
//
// Dialog
//

IDD_MAIN DIALOG DISCARDABLE  0, 0, 187, 69
STYLE DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU
CAPTION "多线程同步"
FONT 10, "宋体"
BEGIN
    DEFPUSHBUTTON   "计数",IDOK,66,48,50,14
    LTEXT           "计数器一:",IDC_STATIC,7,7,42,8
    LTEXT           "计数器二:",IDC_STATIC,7,27,43,8
    EDITTEXT        IDC_COUNTER1,69,7,88,14,ES_AUTOHSCROLL | ES_READONLY
    EDITTEXT        IDC_COUNTER2,69,25,89,14,ES_AUTOHSCROLL | ES_READONLY
END

上面的程序中使用了2个计数器,dwCounter1 和dwCounter2,这2个计数器的值每隔半秒就会被更

新到文本框中,计数器一dwCounter1增加计数只使用了一条指令:

++dwCounter1;

所以,即使多个线程也不存在同步问题,不会产生错误,可是第二个计数器dwCounter2则使用了3条

指令来增加计数:

dwTmp = dwCounter2;
++dwTmp;
dwCounter2 = dwTmp;

在这3条指令执行的过程当中,如果在一个线程中在前2条或者1条指令刚刚执行完毕,就被其它的线程

打断并且其它的线程修改了dwCounter2的值,那么就会产生计数丢失。程序当中用了10个线程进行计

数,如果2个计数器都不存在同步问题,那么结果应该是一样的,可是事实是计数器二存在同步问题,那

么我们就运行看一看结果的差距有多大:

程序运行10秒之后,计数器一的值是125788870, 计数器二的值是26052074,
125788870 - 26052074 = 99736796
从结果中可以看出短短10秒钟,计数器二因为同步问题就丢掉了99736796次计数,丢失率高达%

79.3,可见,同步问题产生错误的几率是多么的大。可见这绝对不是偶尔发生一次两次的事情,大家可

以想像一下,如果有人往一个银行账户中汇款,三笔汇款中丢了两笔,人们会有何感想呢?

了解了同步问题产生的根源,再提出解决方案是很简单的,这在其他的应用程序中早有体现,如各种多

用户版的数据库在操作记录之前都要对记录进行锁定,保证一条记录在同一时刻只能被一个对象操作;

Windows中的写文件函数在遇到其他程序正在写入中的时候会返回共享错误,而不是不管青红皂白直接

写入了事。类似的例子还可以找到很多,归纳起来不外乎一点:就是保证整个存取过程的独占性,在一

个线程对某个对象进程操作的过程中,需要有某种机制阻止其他线程的操作。

将这个思路用于多线程之间的同步,可以设计出一些方案来:

(1)设置一个“允许操作”标志变量,当线程需要进行独占操作的时候将标志位复位,操作完成后将标志

位置位,任何线程如果需要对对象操作,操作之前必须判断标志位是否置位,如果没有则等待。

(2)如果觉得上面的方法存在CPU占用率的问题,可以使用事件对象来代替自己定义的标志变量。

(3)使用临界区对象(Critical Section Objects)。

考察这些方案,其实方案1和2不一定就能正常工作,因为设置标志和测试标志这个过程是由多条指令完

成的,这些指令本身就存在同步问题,比如某个线程测试到标志变量变为“允许”状态,然后它将标志变

量的状态复位并开始操作数据,但如果线程在测试标志变量和将标志变量复位之间被打断的话,其他线

程可能在这期间也在做同样的事情。将上面的例子按照方案1和2修改后运行,就可以发现计数值还是不

同步的。

其实Windows提供了专门的解决方案——使用临界区对象。

临界区也是Windows中的一种对象,从理解的角度看,同样可以把它看做是一种标志,只不过多个线程

同时操作这个“标志”的时候,由Windows负责标志测试中的同步问题罢了。

临界区对象是定义在数据段中的一个CRITICAL_SECTION结构,结构的具体字段不必关心,也不应该

关心,因为它的维护和测试工作都是由Windows来完成的,只需把它想像成一个标志就可以了,结构应

当定义成全局变量,因为在各线程中都要测试它。

定义了CRITICAL_SECTION结构后,必须首先对它进行初始化:

VOID InitializeCriticalSection(
  LPCRITICAL_SECTION lpCriticalSection  // critical section
);

lpCriticalSection参数指向数据段中定义的CRITICAL_SECTION结构。

假如将需要独占的工作看成是使用一个单人更衣室,那么标志就相当于更衣室门上的牌子,当一个人进

入更衣室的时候,将牌子翻到“里面有人”这一面,出来的时候将牌子翻回到“里面无人”这一面,上面的

方法1和2就相当于谁先看到这个牌子,谁就可以进入,当几个人同时看到牌子的时候就产生矛盾了。如

果使用临界区,就相当于门口站了一个工作人员(这里就是Windows),只有向他申请后获得允许的人

才可以进入,其他的人即使同时提交了申请,也将暂时被拦在外面。

所以,定义并初始化临界区以后,当需要对只能独占的数据进行操作的时候,可以先向Windows递交“

进入更衣室”的申请,只有里面没有人,Windows才会答复,这个工作由EnterCriticalSection函数来

完成:

VOID EnterCriticalSection(
  LPCRITICAL_SECTION lpCriticalSection  // critical section
);

如果当前由其他线程拥有临界区,函数不会返回,如果函数返回就表示现在可以独占数据了。调用

EnterCriticalSection函数可以看成是让Windows检测标志,如果是“不允许”则等待;是“允许”则将

标志修改为“不允许”状态并返回。

当完成操作的时候,还要将临界区交还Windows,以便其他线程可以申请使用,这个工作由

LeaveCriticalSection函数完成:

VOID LeaveCriticalSection(
  LPCRITICAL_SECTION lpCriticalSection   // critical section
);

LeaveCriticalSection函数的功能可以看成是将标志从“不允许”改回“允许”状态。

当程序不再使用临界区的时候,必须使用DeleteCriticalSection将它删除:

VOID DeleteCriticalSection(
  LPCRITICAL_SECTION lpCriticalSection   // critical section
);

现在用临界区来修改一下前面的程序:

// ThreadSyn.c

#include
#include "resource.h"

HWND        hWinMain = NULL;
HWND        hWinCount = NULL;
DWORD        dwThreads = 0;

DWORD        dwOption = 0;
#define        F_STOP    0x0001

DWORD        dwCounter1 = 0;
DWORD        dwCounter2 = 0;

char        szStop[] = TEXT("停止计数");
char        szStart[] = TEXT("计数");

CRITICAL_SECTION stCS;

DWORD WINAPI Counter(LPVOID lpParameter);

LRESULT CALLBACK DialogProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM

lParam);

int WINAPI WinMain(IN HINSTANCE hInstance, IN HINSTANCE hPrevInstance, IN

LPSTR lpCmdLine, IN int nShowCmd )
{
    DialogBoxParam(hInstance, MAKEINTRESOURCE(IDD_MAIN), NULL,

DialogProc, 0);
    return 0;
}

LRESULT CALLBACK DialogProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM

lParam)
{
    int i = 0;
    HANDLE hThread = NULL;
    switch ( uMsg )
    {
    case WM_TIMER:
        EnterCriticalSection(&stCS);
        SetDlgItemInt(hWinMain, IDC_COUNTER1, dwCounter1, FALSE);
        SetDlgItemInt(hWinMain, IDC_COUNTER2, dwCounter2, FALSE);
        LeaveCriticalSection(&stCS);
        break;
    case WM_COMMAND:
        if ( LOWORD(wParam) == IDOK )
        {
            if ( dwThreads )
            {
                dwOption |= F_STOP;
                KillTimer(hWnd, 1);
            }
            else
            {
                dwCounter1 = 0;
                dwCounter2 = 0;
                while ( i < 10 )    
                {
                    hThread = CreateThread(NULL, 0,

Counter, NULL, 0, NULL);
                    CloseHandle(hThread);
                    i++;
                }
                SetTimer(hWnd, 1, 500, NULL);
            }
        }
        break;
    case WM_CLOSE:
        if ( !dwThreads )
        {
            DeleteCriticalSection(&stCS);
            EndDialog(hWnd, 0);
        }
        break;
    case WM_INITDIALOG:
        hWinMain = hWnd;
        hWinCount = GetDlgItem(hWinMain, IDOK);    
        InitializeCriticalSection(&stCS);
        break;
    default:
        return FALSE;
    }
    return TRUE;
}

DWORD WINAPI Counter(LPVOID lpParameter)
{
    DWORD dwTmp;
    dwThreads++;
    SetWindowText(hWinCount, szStop);
    dwOption &= ~F_STOP;

    while ( !(dwOption & F_STOP) )
    {
        EnterCriticalSection(&stCS);
        ++dwCounter1;
        dwTmp = dwCounter2;
        ++dwTmp;
        dwCounter2 = dwTmp;
        LeaveCriticalSection(&stCS);
    }
    dwThreads--;
    SetWindowText(hWinCount, szStart);

    return 0;
}

上面修改的代码,增加了临界区对象用于同步:
CRITICAL_SECTION stCS;

在计数的时候:

EnterCriticalSection(&stCS);
++dwCounter1;
dwTmp = dwCounter2;
++dwTmp;
dwCounter2 = dwTmp;
LeaveCriticalSection(&stCS);

通过EnterCriticalSection和LeaveCriticalSection保证操作的原子性,同时在显示计数值的时候:

EnterCriticalSection(&stCS);
SetDlgItemInt(hWinMain, IDC_COUNTER1, dwCounter1, FALSE);
SetDlgItemInt(hWinMain, IDC_COUNTER2, dwCounter2, FALSE);
LeaveCriticalSection(&stCS);

这样也保证了显示的时候的原子性,于是你看到的结果是,2个文本框的值始终保持一致,程序在运行

10秒钟之后,计数器的值是2026824,26052074 - 2026824 = 24025250,可见在不同步的时候

错误计数的值要远远大于正确的值,正确计数的值只有不同步的时候计数值的%7.8,效率远远不如不

同步的时候,这说明花在等待上的时间实在是太多了,但这是多线程为了保持数据同步所必须付出的代

价。

本文的理论部分来源于罗云彬32位汇编语言,源程序也是由32位汇编源程序改写而来,想知道汇编的写

法,请看他的那本书。

转载请注明出处:
author: cnhnyu
e-mail: cnhnyu@gmail.com
qq: 94483026

阅读(4472) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~