linux:线程
一、進程與線程的區別
?????????????????典型的UNIX/Linux進程可以看成只有一個控制線程:一個進程在同一時刻只做一件事情。有了多個控制線程后,在程序設計時可以把進程設計成在同一時刻做不止一件事,每個線程各自處理獨立的任務。
??????? 進程是程序執行時的一個實例,是擔當分配系統資源(CPU時間、內存等)的基本單位。在面向線程設計的系統中,進程本身不是基本運行單位,而是線程的容器。程序本身只是指令、數據及其組織形式的描述,進程才是程序(那些指令和數據)的真正運行實例
????????????????"進程——資源分配的最小單位,線程——程序執行的最小單位"
???????? ? ? ?進程有獨立的地址空間,一個進程崩潰后,在保護模式下不會對其它進程產生影響,而線程只是一個進程中的不同執行路徑。線程有自己的堆棧和局部變量,但線程沒有單獨的地址空間,一個線程死掉就等于整個進程死掉,所以多進程的程序要比多線程的程序健壯,但在進程切換時,耗費資源較大,效率要差一些。但對于一些要求同時進行并且又要共享某些變量的并發操作,只能用線程,不能用進程。
?????????????????????從上面我們知道了進程與線程的區別,其實這些區別也就是我們使用線程的理由。總的來說就是:進程有獨立的地址空間,線程沒有單獨的地址空間(同一進程內的線程共享進程的地址空間)。?
???使用多線程的理由之一是和進程相比,它是一種非常"節儉"的多任務操作方式。
??????????????? 在Linux系統下,啟動一個新的進程必須分配給它獨立的地址空間,建立眾多的數據表來維護它的代碼段、堆棧段和數據段,這是一種"昂貴"的多任務工作方式。而運行于一個進程中的多個線程,它們彼此之間使用相同的地址空間,共享大部分數據,啟動一個線程所花費的空間遠遠小于啟動一個進程所花費的空間,而且,線程間彼此切換所需的時間也遠遠小于進程間切換所需要的時間。據統計,總的說來,一個進程的開銷大約是一個線程開銷的30倍左右,當然,在具體的系統上,這個數據可能會有較大的區別
使用多線程的理由之二是線程間方便的通信機制。
????????????????對不同進程來說,它們具有獨立的數據空間,要進行數據的傳遞只能通過通信的方式進行,這種方式不僅費時,而且很不方便。線程則不然,由于同一進程下的線程之間共享數據空間,所以一個線程的數據可以直接為其它線程所用,這不僅快捷,而且方便。當然,數據的共享也帶來其他一些問題,有的變量不能同時被兩個線程所修改,有的子程序中聲明為static的數據更有可能給多線程程序帶來災難性的打擊,這些正是編寫多線程程序時最需要注意的地方。
? ? ? ? 二、linux 上關于線程的API
?????? 多線程開發在 Linux 平臺上已經有成熟的 pthread 庫支持。? (編譯的時候需要 -lpthread)
??????????????????????其涉及的多線程開發的最基本概念主要包含三點:線程,互斥鎖,條件。
????????????????????其中,線程操作又分線程的創建,退出,等待 3 種。????????
????????????????????????????????互斥鎖則包括 4 種操作,分別是創建,銷毀,加鎖和解鎖。
????????????????????????????????條件操作有 5 種操作:創建,銷毀,觸發,廣播和等待。
????????????????????????????????其他的一些線程擴展概念,如信號燈等,都可以通過上面的三個基本元素的基本操作封裝出來。詳細請見下表:
三、線程的創建,退出,等待
1、線程的創建
#include <pthread.h> int pthread_create(pthread_t *restrict tidp, const pthread_attr_t *restrict attr, void *(*start_rtn)(void *),void *restrict arg); // 返回:若成功返回0,否則返回錯誤編號????????
????????pthread_create:函數名
?
????????第一個參數是一個指針? ? ,指向一個pthread_t 的指針,在頭文件中聲明pthread_t是一個無符號的長整型,是一個長整型的指針
第二個是線程的屬性
第三個是線程,這是一個函數指針,調用相關的函數
第四個,是給這個線程傳參的參數,因為他是一個無類型的指針,可以傳基本的變量類型int ,char ,也可以傳數組,也可以傳結構體,只要是個地址就可以啦
????
????????????????當pthread_create成功返回時,由tidp指向的內存單元被設置為新創建線程的線程ID。attr參數用于定制各種不同的線程屬性,暫可以把它設置為NULL,以創建默認屬性的線程。
????????新創建的線程從start_rtn函數的地址開始運行,該函數只有一個無類型指針參數arg。
????????????????如果需要向start_rtn函數傳遞的參數不止一個,那么需要把這些參數放到一個結構中,然后把這個結構的地址作為arg參數傳入。
?
2. 線程退出
單個線程可以通過以下三種方式退出,在不終止整個進程的情況下停止它的控制流:
1)線程只是從啟動例程中返回,返回值是線程的退出碼。
2)線程可以被同一進程中的其他線程取消。
3)線程調用pthread_exit:
#include <pthread.h>int pthread_exit(void *rval_ptr);rval_ptr是一個無類型指針,與傳給啟動例程的單個參數類似。進程中的其他線程可以通過調用pthread_join函數?訪問到這個指針
線程退出的時候返回值可以是整數呀,char 型,可以是數組,可以是字符串
線程的退出調用pthread_exit
3、線程等待
?
#include <pthread.h>
int pthread_join(pthread_t thread,
??????????????????? void **rval_ptr);
// 返回:若成功返回0,否則返回錯誤編號
???????? 調用這個函數的線程將一直阻塞,直到指定的線程調用pthread_exit、從啟動例程中返回或者被取消。
????????????????如果例程只是從它的啟動例程返回i,rval_ptr將包含返回碼。
????????????????如果線程被取消,由rval_ptr指定的內存單元就置為PTHREAD_CANCELED。
????????可以通過調用pthread_join自動把線程置于分離狀態,這樣資源就可以恢復。
????????????????如果線程已經處于分離狀態,pthread_join調用就會失敗,返回EINVAL。
???????? 如果對線程的返回值不感興趣,可以把rval_ptr置為NULL。????????
????????????????在這種情況下,調用pthread_join函數將等待指定的線程終止,但并不獲得線程的終止狀態
線程等待調用pthread_join
join和exit可以配合用,join可以回收exit退出的返回值(void *rval_ptr)
補充:線程ID獲取及比較
#include <pthread.h> pthread_t pthread_self(void); // 返回:調用線程的ID
????????????????對于線程ID比較,為了可移植操作,我們不能簡單地把線程ID當作整數來處理,因為不同系統對線程ID的定義可能不一樣。我們應該要用下邊的函數:
#include <pthread.h> int pthread_equal(pthread_t tid1, pthread_t tid2); // 返回:若相等則返回非0值,否則返回04、相關的代碼的實現
這個線程做什么呢?
打印這個線程的id 號,調用pthread_self()
創建的話有四個參數,
第一個參數是一個指針,指向那個t1
第二個是線程屬性,寫NULL
第三個啟動這個線程需要調動哪些函數,(這里面調用func1這個函數)
第四個參數,我們打印了param
我們可以再把param做出來,
把arg轉化成int 型的指針,再對這個指針取數據(先把void * 型改成int *型,再把int *型里面的內容,用取內容的符號取出來)
?
?記得編譯的時候要加上pthread
?結果如下,但是缺了一句話,那么原因是什么呢?
原因是:
main 這個主線程創建第二個線程的時候呢,ret = pthread_create(&t1,NULL,func1,(void *)¶m);
?
一但線程起來的時候?printf("%ld thread is create\n",(unsigned long)pthread_self());就在跑了,跑到一半的時候發現main 函數退出了
導致他來不及執行第二句話
?
????????
有一種方式
不讓主線程退出不就得了
再來打印一下主線程的id號
為什么會多打一個thread is create?
?改動一下
結果
?我想看的再清晰一點:
這樣就知道誰打印的啦
先進入main函數,main函數創建成功,main函數把他自己的控制線程的id打印出來
接著輪到新線程運行,新線程得到的參數是100
如果把while(1)去掉呢?
有可能主要的線程已經退出了,你的新線程還來不及執行就沒了
程序一但退出,主的線程一但退出,代表整個地址空間就釋放了,那么t1新線程沒有任何的地址空間給他操作,所以我們看不到任何t1線程的操作
必須用while(1)這么土的辦法嗎?
可以換成pthread_join
?thread是一個變量,不是一個指針
第二個是回收線程的退出狀態
?先用NULL不收回退出狀態
它也會讓t1執行完,主線程再退出
join可以回收線程退出的狀態
線程等待的第二個參數是一個二級指針
我退出的是ret 的值
這個exit調用的是一個無類型的void * 的一個一級指針
我們定義一個 int *pret
傳的是二級指針過去,那這個pret指向誰呀?指向static int ret =10;
所以我們調用static, 函數調用以后數據還在,如果不用static的話,函數調用一結束,這個數就會消失,那么指向它的內存就無效
?
上面是返回一個整數,那我們可不可以返會一個字符串呢?
P是個一級指針,轉化成void *型
訪問的時候也一樣,用char *pret = NULL;
再把二級指針傳過去
最終調用字符串,字符串用一級指針訪問
?不管什么類型的都不能忽略static,如果去掉的話
結果的ret 就不是10 了,是隨機給你分配的數據
因為我的二級指針是指向ret =10 的,如果不用static函數一調用,ret 就沒掉了
(補充:? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ??
傳參的時候你可以傳遞一個整型數,也可以傳遞一個結構體,
當你傳遞一個變量的時候,你用一個整形或者char型就可以了
當你傳遞多個數據的時候,create 第四個參數設置成一個結構體
)
四、線程共享內存空間的代碼驗證
驗證:線程共享進程的內存空間
?他們的執行順序不一定都是這種順序
?他們線程之間是有競爭的關系
?
然后我們來看一下共享的使用
?
運行的結果
他們的書是沒有重復的,說明他們訪問的是同一個變量
所以說多個線程共享數據段里面的數據
所以說線程共享進程的內存空間
?而且它得到的數都是隨機的
?對比一下
?
這樣寫的話會導致一個問題:
?有時候的線程1不一定會退出
?五、線程同步之互斥量加鎖解鎖
? ????????????????互斥量(mutex)從本質上來說是一把鎖,在訪問共享資源前對互斥量進行加鎖,在訪問完成后釋放互斥量上的鎖。
????????????????對互斥量進行加鎖后,任何其他試圖再次對互斥量加鎖的線程將會被阻塞直到當前線程釋放該互斥鎖。????????
????????????????如果釋放互斥鎖時有多個線程阻塞,所有在該互斥鎖上的阻塞線程都會變成可運行狀態,第一個變為可運行狀態的線程可以對互斥量加鎖,其他線程將會看到互斥鎖依然被鎖住,只能回去等待它重新變為可用。在這種方式下,每次只有一個線程可以向前運行。
??????????????????????如果釋放互斥鎖時有多個線程阻塞,所有在該互斥鎖上的阻塞線程都會變成可運行狀態,第一個變為可運行狀態的線程可以對互斥量加鎖,其他線程將會看到互斥鎖依然被鎖住,只能回去等待它重新變為可用。在這種方式下,每次只有一個線程可以向前運行。
????????????????在設計時需要規定所有的線程必須遵守相同的數據訪問規則。????????
????????????????只有這樣,互斥機制才能正常工作。操作系統并不會做數據訪問的串行化。
????????????????如果允許其中的某個線程在沒有得到鎖的情況下也可以訪問共享資源,那么即使其它的線程在使用共享資源前都獲取了鎖,也還是會出現數據不一致的問題
?這里有錯誤:銷毀鎖,加鎖,解鎖應該都是指針
?這里有錯誤:銷毀鎖,加鎖,解鎖應該都是指針?
這里有錯誤:銷毀鎖,加鎖,解鎖應該都是指針
我們可以理解下文的g_data為互斥量
?運行的時候不一定誰在前,誰在后
能不能改成先讓t1運行,t2 在運行呢?
先定義一個鎖(被鎖鎖住的代碼,都叫互斥量),
這個鎖是一個全局的變量,也就是說線程1和線程2都能看到這把鎖
創建鎖和銷毀鎖
第一個參數就是一個指針,指針就是地址
第二個參數是鎖的屬性
#include <pthread.h> int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr); int pthread_mutex_destroy(pthread_mutex_t *mutex); // 返回:若成功返回0,否則返回錯誤編號?
運行結果
只能確保t1運行完,t2在運行
或者t2運行完,t1在運行
(main這個線程我們沒有控制它的)
我們可以再修改一下代碼,看一下
運行結果
t1里面不管多少都先得執行完畢
鎖的作用呢?一但我們獲得這個鎖,肯定保證里面的鎖執行完畢,其他的線程才能繼續走
?我們再修改一下,再加上一個線程
?
t1可能被main打斷,但是不會被t2和t3 打斷,t1結束后,t2和t3再來競爭
或者相反,但是t1不可能被t2和t3打斷
?
6、互斥鎖限制共享資源的訪問
上面的demo4.c 想實現讓線程1 先運行,當g_data 為3的時候讓他退出
但是我們發現等于3的時候,不一定在線程1里面,也有可能在線程2,也有可能在main線程里面
(給這把鎖取名mutex)
就算func2先得到這把鎖,但是睡眠的那一秒。func1就能競爭到鎖,
func1競爭到鎖的時候,不斷檢測g_data是否到達了3,到3才解鎖,否則不解鎖
?
運行的結果,
?運行的結果,
?
那么t1能不能讓整個進程退出呢?
修改一下代碼
運行的結果,
因為進程是線程的容器,線程崩了,整個進程也會崩
線程調用exit能把整個進程結束掉
?運行的結果,
做一個測試,g_data=3的這件事,t1一定能競爭到的
寫個腳本
?里面的內容
相關的指令
?運行的結果
運行了三遍
還有一種方法
?運行結果
按ctrl +c發現退不了,那就只能殺死它
ps -aux |grep a.out
查到a.out的進程a.out
?發現停止了
這節課的 思路:線程1 ,while(1)里面呢,不斷對全局變量做出訪問,并做一些打印判斷等等
?
?
?七、什么情況造成死鎖
(有兩個鎖,當線程A獲得一把鎖時,想要獲得另一把鎖,同時線程B獲得線程A想要的那把鎖,想得到線程A得到的那把鎖時,導致線程A和線程B都想拿到對方手里有的那把鎖,誰都不肯解鎖,會造成死鎖)
線程1 拿著鎖1 ,想要獲得鎖2
線程2 拿著鎖2 ,想要獲得鎖1
對于線程1 來說永遠拿不到鎖2 ,因為鎖2 已經被線程2 拿走了
對于線程2 來說 永遠拿不到鎖1,因為鎖1 已經被線程1 拿走了
這樣線程1 就會卡死想拿到鎖2 的地方
線程2 就會卡死在想拿到鎖1 的的地方
大家誰都拿不到另外一把鎖,導致造成死鎖,大家都在休眠等待
?
?
?運行的結果
t1和t2 都沒辦法運行下去
?這樣的情況是因為t3 先運行了
?
八、線程條件控制實現線程的同步
在前幾節
在訪問共享資源,臨界資源的時候,涉及互斥鎖
觸發和廣播的區別是
觸發發送一條,
廣播是發送廣播以后,接收者有很多個
?2.等待
#include <pthread.h> int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex); (一個是條件,一個是鎖)int pthread_cond_timedwait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex,cond struct timespec *restrict timeout); // 返回:若成功返回0,否則返回錯誤編號?
1. 創建及銷毀條件變量
#include <pthread.h> int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr); (一個是條件,一個是狀態) int pthread_cond_destroy(pthread_cond_t cond); // 返回:若成功返回0,否則返回錯誤編號?3. 觸發
#include <pthread.h> int pthread_cond_signal(pthread_cond_t *cond); (都是指針) int pthread_cond_broadcast(pthread_cond_t *cond); // 返回:若成功返回0,否則返回錯誤編號?現在我們想實現;
讓其他線程對全局變量的值做改變,
線程1 做判斷,全局變量加到3的時候,
線程1 才做出變化,否則一直休眠,等待被喚醒
效果是線程二執行 0,1,2,? 出現3的時候給線程一發信號,?線程一執行一次
?
?
?
運行的結果
?
九、測試結果怎么辦呢?
?
?
?
?再寫一個測試文件
?
?
運行結果十次,把運行的結果追加到? ?test.ret.txt里面,
你會發現程序卡在這里
?
?
我們可以加一個取地址符號,就可以再背后運行
?
?
打開test.ret.txt
10、 補充:
pthread_cond_t cond;?
//動態初始化: pthread_cond_init(&cond, NULL);
//靜態初始化: pthread_cond_t = PTHREAD_COND_INITIALIZER;
pthread_mutex_t;
//動態初始化: pthread_mutex_init(&mutex,NULL);
//靜態初始化: pthread_mutex_t = PTHREAD_MUTEX_INITIALIZER;
然后main函數里面的初始化就可以注釋掉
用宏的話是靜態初始化
?
?
?
總結
- 上一篇: linux:进程之间的通信
- 下一篇: Linux:网络编程