0xAA55 发表于 2014-11-7 14:12:26

【多线程】什么是原子操作

原子操作,听起来相当高大上,我还质子操作、中子操作呢!
其实这个原子操作和原子毛关系都没有。它是多线程编程的术语。

在介绍什么是原子操作之前,大家先来看一下这种情况:

1、一个程序在一个拥有多核CPU或多CPU的电脑上启动了。(PS.现在大家用的电脑基本都是多核的吧?正在使用触屏安卓的触生——我!还在苦逼地摸着我的单核手机在这打字呢)

2、这个程序躲过了各种杀毒软件的追杀,活了下来。然后玩电脑的人要它帮忙处理一个棘手的问题。于是这个程序使出了它的绝招!那就是利用多核CPU的优势,让两个CPU核心一起干活!于是两个CPU核心各自运行这个程序的两个部分,一个负责foo,一个负责bar(不要问我foo和bar分别是什么意思,这两个单词没意思!)。开始欢快地干活了!

3、这个时候问题来了——两个CPU核心,都要操作同一个挖掘机,其中一个要把挖掘机的勺子抬起来,另一个则要把挖掘机的勺子放下来,那么到底如何是好?

如果一起操作挖掘机的话,天知道最后勺子是抬起来了还是放下去了还是压根就没动!

咋办?

这个时候,如果这两个CPU其中一个先上,然后另外一个先让开,等对方操作完了再上,我们好歹就知道勺子的状态是最后一个家伙决定的。这个能让另外一个CPU让开的能力,就是我们要说的原子操作了。

这里我们来说一下真实的情况。假设我们要启动五十个线程,我们需要统计哪些线程结束了,哪些还在运行,这个时候常用的做法是创建一个全局变量,线程运行的时候给它累加1,结束前再把它减去1,这样就能通过这个变量来判断正在运行的线程数了。#include<stdio.h>
#include<process.h>
#include<Windows.h>

unsigned int g_uNbRunningThreads=0;//正在运行的线程数

//=============================================================================
//ThreadProc:
//线程处理函数
//-----------------------------------------------------------------------------
void ThreadProc(void*pParam)
{
        g_uNbRunningThreads++;

        //然后这个线程开始做一些事。。
        printf("Fuck!");
        Sleep(233);//睡233毫秒
        fputs("Shit!",stderr);

        g_uNbRunningThreads--;
}

void main()
{
        unsigned i=50;
        while(i--)//创建50个线程
                _beginthread(ThreadProc,0,NULL);
        while(g_uNbRunningThreads)
                Sleep(233);//等待所有线程运行完
}
程序看起来好像就应该是这样,对吧?恐怕不是。其中g_uNbRunningThreads++这句可能被编译为以下的指令:mov eax, ;从内存中读取g_uNbRunningThreads的值
inc eax ;加上1
mov ,eax ;再写回内存是不是看起来很愚蠢?虽然英特尔的处理器支持inc dword这样的直接给内存值加1的指令,但是编译器一般没那么聪明,或者有别的考虑什么的。

这样就会导致一个问题:假设g_uNbRunningThreads的值是零,两个线程同时执行了mov eax,,然后同时执行了inc eax,最后同时执行了mov ,eax,那么最终的值是1,而不是2.当然也有可能有个线程先运行,然后给g_uNbRunningThreads的值加上了1,那么另一个线程运行后,g_uNbRunningThreads的值就是2了。但至少,我们不能确定g_uNbRunningThreads的值到底会变成多少。嗯。大家想起刚才所说的挖掘机问题吧?这个时候挖掘机的勺子到底是抬起来了还是放下来了还是压根就没动,天知道。

我们这样运行一下就比较直观了:#include<stdio.h>
#include<process.h>
#include<Windows.h>

unsigned long g_uNbRunningThreads=0;//正在运行的线程数

//=============================================================================
//ThreadProc:
//线程处理函数
//-----------------------------------------------------------------------------
void ThreadProc(void*pParam)
{
        //只做一件事:把这个值+1
        g_uNbRunningThreads++;
}

void main()
{
        unsigned j;//我们多做几次这种实验
        for(j=0;j<50;j++)//做50次
        {
                unsigned i;
                g_uNbRunningThreads=0;
                for(i=0;i<50;i++)//创建50个线程
                        _beginthread(ThreadProc,0,NULL);
                Sleep(233);//等待所有线程运行完
                printf("g_uNbRunningThreads=%u\n",g_uNbRunningThreads);
        }
}大家就会看到我画红色框的那个地方,g_uNbRunningThreads的值居然是49,而不是50。这是因为有些线程打断了另外一些线程对g_uNbRunningThreads进行读写的操作。

解决这个问题的方法就是用原子操作。Windows的API提供了这么几个函数:
InterlockedIncrement:给一个变量进行++操作
InterlockedDecrement:给一个变量进行--操作
InterlockedExchange:给变量赋值
InterlockedExchangeAdd:给变量进行+=操作
...
其实有很多Interlocked开头的函数,在MSDN就能看到。我就不一一列举了。
那么上面的代码可以用InterlockedIncrement和InterlockedDecrement来给g_uNbRunningThreads的值进行修改。使用这些函数的操作就是原子操作。

我们使用原子操作,重新写一下那段程序,那应该就是这样的:#include<stdio.h>
#include<process.h>
#include<Windows.h>

unsigned long g_uNbRunningThreads=0;//正在运行的线程数

//=============================================================================
//ThreadProc:
//线程处理函数
//-----------------------------------------------------------------------------
void ThreadProc(void*pParam)
{
        //只做一件事:把这个值+1
        InterlockedIncrement(&g_uNbRunningThreads);//这次是原子操作
}

void main()
{
        unsigned j;//我们多做几次这种实验
        for(j=0;j<50;j++)//做50次
        {
                unsigned i;
                g_uNbRunningThreads=0;
                for(i=0;i<50;i++)//创建50个线程
                        _beginthread(ThreadProc,0,NULL);
                Sleep(233);//等待所有线程运行完
                printf("g_uNbRunningThreads=%u\n",g_uNbRunningThreads);
        }
}这回这个问题就解决了,没有出现线程操作被打断的情况,这就是传说中的原子操作。下一章我将会讲到多线程之间对同一段内存的读写是如何处理的。
SRC+BIN:
页: [1]
查看完整版本: 【多线程】什么是原子操作