為什麼在同一進程中創建不同線程,但線程各自的變數無法在線程間互相訪問?

初學操作系統,看到課本里說:同一進程內的多個線程都是共享該進程的內存的,有以下的疑問:

  1. 在實際的多線程編程中,在一個進程內創建多個線程,為什麼各個線程各自的變數又不能相互訪問,需要通過共享隊列這樣的結構才能通信呢?
  2. 如果是多線程編程中每個線程都有各自獨立的工作內存,那這樣效果不就幾乎和多進程是一樣的了嗎(指無法共享內存)?

望解答。


在回答之前,先討論你拋出的問題1「在實際的多線程編程中,在一個進程內創建多個線程,為什麼各個線程各自的變數又不能相互訪問,需要通過共享隊列這樣的結構才能通信呢?」

各線程各自的變數又不能相互訪問,你這裡說的變數具體是什麼變數? 如果是全局變數,所有線程都是可以訪問的;如果是棧變數,那無論哪個線程只要執行該函數,它就可以訪問它的棧變數;如果是線程變數(linux下gcc使用__thread標識符創建的變數)的確是每線程一份的,如果不採用特殊技巧,每個線程只能訪問自己的那份。 所以,不清楚你這裡說的變數是指什麼變數。

好了,現在可以回答你的問題了。

世間萬物,相生相剋,實際上他們都有自己的語義空間,當你把它換到另一個語義空間去討論時,你會發現原來的概念不對了。是的,軟體上的每層都有自己的抽象和邏輯,如何你將多軟體上不同層次的抽象或者邏輯拉到一起討論,但又無法分辨他們的原本應該有語義時,那就把人都搞糊塗了。

先說操作系統次層的語義或邏輯:

進程與線程是操作系統的概念,進程間的內存空間是隔離,是相互看不見,摸不著,訪問不了的。而一個進程內的多個線程,共用同一個地址空間,也即多線程之間內存是完全共享的。注意,在操作系統層面上,只有內存空間的隔離與共享,沒有變數這個概念。

如果把線程作為一個執行體,而沒有多線程場景下的進程也是一個執行體,那麼多線程下的執行體間的內存是共享的。而多進程執行體之間的內存是不共享的,但有時候多個進程之間需要通過大家可以訪問的內存來傳遞信息,OS就劃一塊物理內存出來,讓兩個進程將物理內存映射到兩個進程各自的虛擬內容內,這種方案稱為共享內存。實際進程內的多個線程間所有內存空間都是共享的,但由於這個天生的,所以我們一般不將之稱為「共享內存」。

再說編語言層次的語義或邏輯:

操作系統上每個進程的內存相當於一塊地,但這塊地如何管理,就需要編語言提供更高層次的抽象了。 C/C++語言開發出來的程序(和動態庫,Linux so/windows dll)都是運行在獨立的地址空間上的,如果兩個進程運行代碼相同,只是代碼完全相關,但運行空間是隔離的。所以從這個角度來說,C/C++是沒有多進程概念的。也許你會說,如果沒有進程概念,那為什麼C/C++可以訪問共享內存。那是因為你程序的邏輯裡面有多進程,所以才需要訪問共享內存。並且訪問共享內存介面是操作系作提供的,並不是C語言提供的。當調用操作系統的庫功能映射好共享內存之後,C語言還是當它是進程內的內存一樣使用,所以它是不感知多進程的。

那麼,在多線程裡面,所有線程都能看見整個進程的地址空間,是不是任何線程都可以訪問其它線程的任何空間?

答案是肯定的,線程可以訪問其它線程的任何空間,這個是操作系統給予它的權利。

那麼它是否可以訪問其它線程的棧變數和線程變數呢?答案也是肯定的,一定是可以,只要操作系統給予的權利,所有空間都能訪問,管它是棧空間,還是線程變數空間呢。前提是你要知道這些空間的地址,才能訪問。比如,我們要做壞事,比如殺張三,從可能性上來說,是可以的,只要你有工具,任何人都可以殺,如果殺張三,你得知道張三在哪裡。

在C語言層面上,任何訪問都是通過變數來實施的。而在C語言里,變數種類和可見範圍:

  1. 全局變數:任何線程都可以訪問
  2. 棧變數(局部變數):任何線程執行訪函數均可訪問,函數外不可訪問
  3. 線程變數(gcc擴展使用__thread定義的變數):每個線程只能訪問自己的那個拷貝,其它線程的拷貝不可見

為什麼C語言提供這麼多種類的變數,難度是吃飯了撐著沒有事幹嗎?當然不是,提供多種變數抽象,就是為了在不同場景使用不同的方式,如果還是用彙編的方式來理解內存,那不是要回到原始社區了么。

不管提供多好么好的抽象和「隔離」效果,只要C語言還有無所不能的指針,只要你不小心將某個線程原本不該訪問的地址保存下來,或者產生了一個野指針指向了某線程原本不該訪問的變數,C語言所有努力都會白費。

原到題主的問題:從OS角度,所有線程都有訪問進程的地址空間,從C語言角度,線程可以訪問全局變數,只能在進入函數時訪問全局變數,每個線程只能訪問自己的線程變數拷貝;但是,如果指針作惡,語言上的約束就失效了。


謝邀。變數的可訪問性其實與多線程沒有關係,只是取決於你是否把它定義成了局部變數。分配在棧上的局部變數肯定沒法共享;反之,全局變數或者堆上的變數,只要你把其引用或者指針帶進線程,總是可以多線程共享訪問。但這種用法一般需要考慮互斥加鎖。舉個簡單的栗子:

//...
int global_var = 0;

void thread_func(void* param)
{
//...
lock();
//可以改成對變數的任意讀寫操作
display(global_var);
global_var = (int) time(0);
unlock();

//...
}

//...
int result = pthread_create(
thread_id,
NULL,
(void*)thread_func,
NULL);


我來搗個亂……

除了其他進程的東西,其他的都是可以直接訪問的……

棧上的局部變數?threadlocal?都可以的。說到底都在同一片虛擬內存里,扔個指針隨便怎麼訪問。

隨手寫段代碼(原諒這隨性的、目無章法的風格):

std::atomic& ptr = nullptr;
std::atomic& ptr2 = nullptr;
auto GetThreadId() { return *(uint32_t*)std::this_thread::get_id(); }
int main()
{
char x;
thread_local int i;
printf("main thread:%ld
", GetThreadId());
printf("input char and int:");
std::cin &>&> x &>&> i;
ptr.store(x);
ptr2.store(i);
while (getchar())
{
std::thread([]
{
printf("in thread %ld: ptr:%p val:%c ptr2:%p val:%d
", GetThreadId(), ptr.load(), *(ptr.load()), ptr2.load(), *(ptr2.load()));
}).detach();
}
}

真正讓人操心的是怎麼樣正確地訪問,畢竟棧上空間隨時可能沒掉,threadlocal設計就是為了每個線程獨享一份數據……

多線程下,一個線程的操作未必能馬上反映到另一個線程。隨意訪問很可能出現一次訪問到舊數據,另一次訪問到新數據的情況,這和你原有的程序邏輯很可能是相違背的……

原子變數只能保證少量內容的原子性,大的數據還是得靠加鎖和信號量……

於是就產生了線程直接數據互相訪問很麻煩的表象……


共享內存是IPC的一種,你非要用在線程間也沒人攔著你。多線程之間到底共享什麼,又獨享什麼,我覺得你應該系統的看一下資料,不是簡單的一句話就能概括的。比如堆上的各種數據是否共享,棧上的數據,還有很多系統資源(如打開的文件),等等。多線程編程最重要也是最難的就是共享數據的訪問,臨界區怎麼保護,怎麼同步互斥,這些都是多線程的基礎也是必須要掌握的。既然你想要使用多線程,就應該深究這些,並多多實踐。我不想在這裡給你籠統的總結,因為那會造成很多誤導,何況網上已經有很多總結性文章。我建議你看看權威的資料,並自己實踐一下。


確切來說沒有共享變數這個說法,只有共享內存。 所謂共享變數應該指同一作用域。 不同線程雖然有可能是同一個代碼段,但不會是同一作用域,所以不會「共享」。 而共享內存,並沒有作用域之分,同一進程內,不管什麼線程都可以通過同一虛擬內存地址來訪問。 不同進程也可以通過ipc等方式共享內存數據。


樓上大佬們說的很好了,我弱弱的補個圖

但是,在多線程的過程中,每個線程都是獨立運行的,當然可以調用各種常式來完成正在執行的任何工作。 而不是地址空間中的單個堆棧,每個線程將有一個堆棧。 假設我們有一個多線程的進程,它有兩個線程; 得到的地址空間看起來不同(圖26.1,右)。

在這個圖中,您可以看到兩個堆棧分布在整個進程的地址空間中。 因此,我們放在堆棧上的任何堆棧分配的變數,參數,返回值和其他東西都將被放置在有時稱為線程本地存儲的地方,即相關線程的堆棧中。

——摘自《Operating System -- Three Easy Steps》


線程中的變數是在線程棧上開闢的(在C/C++中線程入口地址就是函數地址),其它線程肯定不能訪問,這些實際上是局部變數。但是共享隊列是在進程角度上看待的,一個進程中共享的某些內存單元就是在各個線程中分享的。就像在進程中動態開闢內存地址,你可以將這個地址傳遞到各個線程中一個意思。


謝邀, 比較同意 @苳杋 的答案,簡單直接。個人想法和他雷同,不再啰嗦。


1. 共享隊列的目的是為了解決線程之間的資源讀寫競爭衝突的

2. 確實類似,為了滿足不同層級下的相似需求


位於棧空間的局部變數不可以。。

因為棧是用來函數調用傳參和返回值的,線程就是一系列函數調用,棧空間不互相獨立,函數就調用全部亂套了。

堆空間不涉及函數傳參和返回值,自然在進程內是統一。


多線程是在共享內存的基礎上實現的並發,都共享內存了,同一地址空間,肯定是可以訪問的。只是訪問的方法有所不同而已。


推薦閱讀:

下面代碼是線程不安全的代碼,請問為什麼很難跑出不安全的樣例?
Linux的epoll使用LT+非阻塞IO和ET+非阻塞IO有效率上的區別嗎?
自己寫的 Tiny web server "Bad file descriptor"出錯?
多線程下epoll如何保證event.data.ptr不成為野指針?
Android的界面組件不能被子線程訪問是什麼意思呢?

TAG:操作系統 | 計算機科學 | 多線程 | 並發 |