C語言/原子/編譯,你真的明白了嗎?

来源:http://www.cnblogs.com/Colin-Cai/archive/2017/10/14/7668982.html
-Advertisement-
Play Games

說到原子,類似於以下的代碼可能人人都可以看出貓膩。 我想大多數人都知道其結果未必會得到1000000000。 測試一下吧。 可是真的知道貓膩了嗎?如果我編譯的時候優化一下呢? 運行速度一下子變的飛快,而且似乎都得到了10億。 這裡,mythread里cnt自加5億次被優化成了 cnt += 5000 ...


  版權申明:本文為博主窗戶(Colin Cai)原創,歡迎轉帖。如要轉貼,必須註明原文網址

  http://www.cnblogs.com/Colin-Cai/p/7668982.html 

  作者:窗戶

  QQ:6679072

  E-mail:[email protected]

  說到原子,類似於以下的代碼可能人人都可以看出貓膩。

/* http://www.cnblogs.com/Colin-Cai */
#include <stdio.h>
#include <pthread.h>

int cnt = 0;
void* mythread(void* arg)
{
        int i;
        for(i=0;i<500000000;i++)
                cnt++;
        return NULL;
}

int main()
{
        pthread_t id, id2;

        pthread_create(&id, NULL, mythread, NULL);
        pthread_create(&id2, NULL, mythread, NULL);
        pthread_join(id, NULL);
        pthread_join(id2, NULL);
        printf("cnt = %d\n", cnt);

        return 0;
}

 

  我想大多數人都知道其結果未必會得到1000000000。

  測試一下吧。

linux-p94b:/tmp/testhere # gcc test1.c -lpthread
linux-p94b:/tmp/testhere # for((i=0;i<10;i++));do ./a.out ; done
cnt = 1000000000
cnt = 1000000000
cnt = 1000000000
cnt = 1000000000
cnt = 1000000000
cnt = 1000000000
cnt = 958925625
cnt = 1000000000
cnt = 1000000000
cnt = 1000000000

  可是真的知道貓膩了嗎?如果我編譯的時候優化一下呢?

linux-p94b:/tmp/testhere # gcc -O2 test1.c -lpthread
linux-p94b:/tmp/testhere # for((i=0;i<10;i++));do ./a.out ; done
cnt = 1000000000
cnt = 1000000000
cnt = 1000000000
cnt = 1000000000
cnt = 1000000000
cnt = 1000000000
cnt = 1000000000
cnt = 1000000000
cnt = 1000000000
cnt = 1000000000

  運行速度一下子變的飛快,而且似乎都得到了10億。

  這裡,mythread里cnt自加5億次被優化成了 cnt += 500000000

  那麼當然快啊,可是似乎這與我們當初想測試原子有那麼一些差異,一樣的代碼,不一樣的編譯,卻帶來了不同的結果。

  其實原因在於,我們這裡代碼寫的不好,才沒有表達好我們當初的意思,我們是希望cnt真的自加5億次。那麼怎麼辦呢?其實很好辦,在cnt的定義前面加個volatile,那麼這裡對於cnt的自加則不會優化。很多時候,為什麼我們優化前和優化後的結果不一樣,常常是因為寫代碼的人不明白程式的優化規則。在上個公司的時候,我很想臨走的時候再給大家做一個培訓,說說C語言的優化,同時說說我們平時寫的無意依賴於編譯的所謂垃圾代碼,但是直到離開,我還是沒有做此培訓。

  我們加了volatile試一下,

linux-p94b:/tmp/testhere # gcc -O2 test1.c -lpthread
linux-p94b:/tmp/testhere # for((i=0;i<10;i++));do ./a.out ; done
cnt = 635981117
cnt = 675792826
cnt = 522700646
cnt = 593410055
cnt = 544306380
cnt = 630888304
cnt = 580539893
cnt = 629360072
cnt = 555570127

  我們在cnt定義前加個volatile,效果果然就更明顯了,因為真的是自加5億次,導致問題的機會變多了。那麼之前沒加volatile並優化編譯,會不會也有不得到10億的可能呢?

  我們首先要明白的是,這裡的cnt++不是原子操作,中間有隨時調度的可能。

  5億次太多,我們就拿只自加1次為例即可說明,兩個線程都只自加1次,本來期待結果為2.

  cnt++在一般的處理器中至少有三條指令,我們用偽彙編來寫。  

  cnt -> reg  //把cnt從記憶體載入到寄存器reg

  reg+1 -> reg //寄存器reg自加1

  reg -> cnt     //把reg的內容寫入記憶體

  

  那麼,

 

       (線程1)cnt -> reg

  (線程1)reg+1 -> reg

  (線程1)reg -> cnt

       (線程2)cnt -> reg

  (線程2)reg+1 -> reg

  (線程2)reg -> cnt

 

  理想中,我們認為處理器的執行是以上這樣,結果cnt里的值是2。

  但假設過程中發生了調度,指令執行的順序並非像以上這樣,假如變成了以下這樣

 

       (線程1)cnt -> reg

  (線程1)reg+1 -> reg  

       (線程2)cnt -> reg

  (線程2)reg+1 -> reg

  (線程2)reg -> cnt

  (線程1)reg -> cnt 

  我們再來算算,

  cnt = 0,  reg任意

       (線程1)cnt -> reg

  cnt = 0, reg =  0

  (線程1)reg+1 -> reg

  cnt = 0, reg = 1

  此處調度,reg = 1會被保存,併在重新調度回來之後有效,而cnt不會管

 

  調度之後

  cnt = 0, reg任意 

       (線程2)cnt -> reg

  cnt = 0, reg = 0

  (線程2)reg+1 -> reg

  cnt = 0, reg = 1

  (線程2)reg -> cnt

  cnt = 1, reg = 1

  此處又發生調度,reg會恢復之前保存的1,而cnt不會有任何變化

  所以在執行下一條指令前,

  cnt = 1, reg = 1

  (線程1)reg -> cnt 

  cnt = 1, reg = 1

  

  我們可以看到,結果成了1,而不是2,這就是非原子操作導致的結果,其實之前優化成cnt += 500000000本身也依然有此問題,只是難以觀察的到。

  雖然x++不是原子,但是我們可以使用鎖的方式,來人為的製造“原子”,比如這裡用互斥。

  

#include <stdio.h>
#include <pthread.h>

volatile int cnt = 0;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
void* mythread(void* arg)
{
        int i;
        for(i=0;i<500000000;i++) {
                pthread_mutex_lock(&mutex);
                cnt++;
                pthread_mutex_unlock(&mutex);
        }
        return NULL;
}

int main()
{
        pthread_t id, id2;

        pthread_create(&id, NULL, mythread, NULL);
        pthread_create(&id2, NULL, mythread, NULL);
        pthread_join(id, NULL);
        pthread_join(id2, NULL);
        printf("cnt = %d\n", cnt);

        return 0;
}

  測試一下

linux-p94b:/tmp/testhere # gcc -O2 test1.c -lpthread
linux-p94b:/tmp/testhere # for((i=0;i<10;i++));do ./a.out ; done
cnt = 1000000000
cnt = 1000000000
cnt = 1000000000
cnt = 1000000000
cnt = 1000000000
cnt = 1000000000
cnt = 1000000000
cnt = 1000000000
cnt = 1000000000
cnt = 1000000000

  


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 專業團隊定製,【企業品牌推廣】口碑整合,網路營銷,,網站SEO優化,公司網站排名優化等服務。 1、直觀效果 直觀的效果就是百度或者360搜索品牌名稱時出現百科、新聞、問答等相關信息,從而提高品牌可信度、知名度,從新品牌到知名品牌,這是適合的第一期推廣方案;相對而言,也是前期經濟實惠、必不可少的營銷包 ...
  • 對於一個初學者來說for迴圈可能會擊潰很多人,因為網上找的python的for迴圈沒有詳細的介紹for迴圈的條件以及for迴圈後面定義的東西是什麼意思。首先我先舉一個常用的例子。 for i in range(1,10) : print i 上面這兩條代碼是用來在屏幕上列印1到9的。這裡就不對ran ...
  • 在上一章中,我們已經搭建好了struts2的一個開發環境,那麼這一章就來做一個簡單的登錄功能,以及介紹和使用struts2裡面一個重要的東西-通配符。 第一步,在WebContent下麵新建一個login.jsp的頁面,裡面使用form表單實現一個簡單的登錄頁面。 第二步:打開上一章中建好的Logi ...
  • 配置環境 1. 導入jar包,Struts2官網:http://struts.apache.org/ 2. 創建Action類 繼承ActionSupport (ActionSupport類是一個工具類,它已經實現了Action介面。除此之外,它還實現了Validateable介面,提供了數據校驗功 ...
  • 幾周前,我開始工作於一個證券投資組合網站。雖然我只能使用 React 完成整個網站,但我決定使用 Go 來創建一個可以處理某些任務(例如發送 email)的 API 伺服器,相信這是一個很好的做法。我其中的一個頁面是一個 contact 頁面,目前看起來像這樣:我想使用專門為此 contact 表單... ...
  • #include <iostream>#include<cstdio>using namespace std;int main(){ freopen("input.txt","r",stdin); freopen("output.txt","w",stdout); int a,b; while(ci ...
  • 剛看spring3實戰書籍第一章 切麵以前沒有關註過 現在看到了 隨手試驗一下 AOP AOP(Aspect Oriented Programming),即面向切麵編程,可以說是OOP(Object Oriented Programming,面向對象編程)的補充和完善。OOP引入封裝、繼承、多態等概 ...
  • 多線程的目的 為什麼要使用多線程?可以簡單的分兩個方面來說: 在多個cpu核心下,多線程的好處是顯而易見的,不然多個cpu核心只跑一個線程其他的核心就都浪費了; 即便不考慮多核心,在單核下,多線程也是有意義的,因為在一些操作,比如IO操作阻塞的時候,是不需要cpu參與的,這時候cpu就可以另開一個線 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...