zoukankan      html  css  js  c++  java
  • 同步机制(一)

      什么是同步机制?

        同步机制 :在并发程序设计中,各进程对公共变量的访问必须加以制约,这种制约称为同步。

      为什么需要同步机制?

        当计算机只运行一个线程的时候,自然不需要同步。所有的资源都是这个线程独享。那么就不会有任何竞争。

        但是当计算机出现了多个线程的时候,那么就出现了各种麻烦,为了处理这些麻烦我们就需要使用一些办法来解决这些麻烦。

        多线程引出的麻烦(对资源的竞争导致的出错) : 

          设想存在A,B两个线程。对同一数据C进行修改。首先A读取数据C,得知C=13。这时候发生了线程切换。切换为B,然后B读取C,得知C=13。然后修改C,将C减少1,最后保存C。这时候C为12。执行结束后,又切换回A,A将C增加1,但是A得知的C是13,于是C=13+1=14。保存C。这时候C=14。

          将上述例子代入生活中来说 :首先A本来是从存钱罐里塞进去了一块钱,B拿出了一块钱。但是最终结果确实C从13元变成14元了。莫名其妙多了一块钱了!

      如何实现同步机制?

        原子操作 : 原子操作的意思就是不可切割,不可打断的操作。

          CPU层面的原子操作 : 对于CPU而言,一条机器指令(机器指令和汇编指令是一一对应的关系,所以后面的例子我会采取汇编指令代替机器指令)就是一个原子操作。因为中断随时有可能发生,但只会发生在两句机器指令之间,而不会在一句机器指令执行到一半就发生,这是不被允许的(被动的进程切换就是通过时钟中断处理程序来实现的)。

          CPU是如何实现原子操作的?

            多种情况分析:

            例1) 多核CPU对自己的寄存器进行修改不存在公共资源的竞争,因为寄存器是CPU私有的(每个CPU都有自己的寄存器),其它的CPU无法直接访问。

            例2) 单核CPU对自己的寄存器进行修改 : 同上

            例3) 单核CPU对内存(公共变量)进行修改 : 因为只有一个CPU,一个内存,也就可以理解为内存是CPU私有的。

            例4) 多核CPU对同一地址的物理内存(公共变量)进行修改 :

                这时候会存在公共资源的竞争,我们可以简单地把CPU修改内存的内容分为三步 : 

                  1) 读取对应内存上的内容

                  2) 用内容进行计算

                  3) 将结果写入对应内存

            cpu0的任务是将内存1111的值+1:inc word ptr ds:[1111]

            cpu1的任务是将内存1111的值-1 : dec word ptr ds:[1111]

            那么就回到了我们之前提到的例子 : 多线程引出的麻烦(对资源的竞争导致的出错)

            什么是总线锁 : 就是使用处理器提供的一个LOCK信号,当一个处理器在总线上输此信号时,其他处理器的请求将被阻塞住,那么该处理器可以独占共享内存。      

            解决策略 : 在进行操作 1)之前,先使用总线锁锁定这段内存(例如 : 1111 ~ 1113)。使得其他CPU无法对该内存操作,当操作 3) 结束,就释放这段内存的总线锁。

          那么CPU层面的原子操作的支持也就形成了 : 一条机器指令就是一个原子操作

        软件层面的原子操作 : 

          原子整数 : 既然机器指令允许直接对内存进行算术运算,那么直接设置一个宏或函数,使用汇编操作该变量即可达成原子整数了。

           原子整数的加法代码实现(任意平台都能运行,这个本来就不需要头文件):

    int add (int * pval, int num){
        int old;
        __asm__  volatile( // volatile : 修饰内嵌汇编时表示不要优化指令
            "lock; xaddl %2,%1;" // %1 += %2
            : "=a" (old) // =表示是输出参数,a表示rax寄存器
            : "m" (*pval) , "a"(num) // m 表示内存变量, a表示rax寄存器
            : "cc", "memory"
        );
        return old;
    }

        虽然看上去这并不是一句汇编,但实际上只有 : lock xaddl %2,%1,这句汇编是临界区,也就是说,这句话锁上了内存,然后将pval的虚拟地址对应的物理地址上的数据增加了num。后面的 return old,只不过把最后结果返回,但是实际上返回值为Null也完全不会影响,因为早已经通过指针修改了那块地址的内容了。

        原子操作的使用(任意平台都能运行,这个本来就不需要头文件):

    int add (int * pval, int num){
        int old;
        __asm__  volatile( // volatile : 修饰内嵌汇编时表示不要优化指令
            "lock; xaddl %2,%1;" // %1 += %2
            : "=a" (old) // =表示是输出参数,a表示rax寄存器
            : "m" (*pval) , "a"(num) // m 表示内存变量, a表示rax寄存器
            : "cc", "memory"
        );
        return old;
    }
    
    int main (){
        int c = 1;
        add(&c, 100);
        // printf("c : %d
    ",c);
        return 0;
    }

        在这里仅仅提供了单线程的使用,实际上多线程也是这么使用的。

        在多线程模式下的运行(在Linux平台下可运行):

    # include<stdio.h>
    # include<unistd.h>
    # include<pthread.h>
    
    #define THREAD_COUNT 10
    /*
    关于
    volatile的本意是“易变的” 因为访问寄存器要比访问内存单元快的多,所以编译器一般都会作减少存取内存的优化,但有可能会读脏数据。
    当要求使用volatile声明变量值的时候,系统总是重新从它所在的内存读取数据,即使它前面的指令刚刚从该处读取过数据。
    精确地说就是,遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问;
    如果不使用valatile,则编译器将对所声明的语句进行优化。
    
    与 register 相反。
    */
    
    /*
    内嵌汇编语法如下 :
           __asm__(汇编语句模板: 输出部分: 输入部分: 破坏描述部分)
    共四个部分:汇编语句模板,输出部分,输入部分,破坏描述部分,各部分使用":"格开,汇编语句模板必不可少,其他三部分可选,'
    如果使用了后面的部分,而前面部分为空,也需要用":"格开,相应部分内容为空。
    例如:
                 __asm__ __volatile__("cli": : :"memory")
    */
    int add (int * pval, int num){
        int old;
        __asm__  volatile( // volatile : 修饰内嵌汇编时表示不要优化指令
            "lock; xaddl %2,%1;" // %1 += %2
            : "=a" (old) // =表示是输出参数,a表示rax寄存器
            : "m" (*pval) , "a"(num) // m 表示内存变量, a表示rax寄存器
            : "cc", "memory"
        );
        return old;
    }
    
    void* thread_callback(void *arg){
        int * pcount = (int *)arg;  
        int i = 0;
    
        while(i++ < 100000){
            // (*pcount)++;
            add(pcount,1);
            usleep(1);
        }
    }
    
    int main (){
        pthread_t threadid[THREAD_COUNT] = {0}; // 初始化
        int i = 0;
        int count = 0;
    
        // 创建线程
        for (i = 0; i < THREAD_COUNT; i++){
            pthread_create(&threadid[i], NULL, thread_callback, &count);// thread_callback, &count : 函数与其参数
        }
    
        for (i = 0; i < 10; i++){
            printf("count : %d
    ", count);
            sleep(1);
        }
        return 0;
    }

     

     

  • 相关阅读:
    python IDE比较与推荐
    一个平庸程序员的想法
    [转载]Malcolm的新书:Outliers
    程序员的编辑器——VIM
    Blender网络资源
    普通人的编辑利器——Vim
    易学易用的Windows PowerShell
    分区表的修复(转)
    云南电信DNS服务器地址
    滇南本草(上)
  • 原文地址:https://www.cnblogs.com/vizdl/p/12247200.html
Copyright © 2011-2022 走看看