Ⅰ linux下各種鎖的理解和使用及總結解決epoll驚群問題(面試常考)-
鎖出現的原因
臨界資源是什麼: 多線程執行流所共享的資源
鎖的作用是什麼, 可以做原子操作, 在多線程中針對臨界資源的互斥訪問... 保證一個時刻只有一個線程可以持有鎖對於臨界資源做修改操作...
任何一個線程如果需要修改,向臨界資源做寫入操作都必須持有鎖,沒有持有鎖就不能對於臨界資源做寫入操作.
鎖 : 保證同一時刻只能有一個線程對於臨界資源做寫入操作 (鎖地功能)
再一個直觀地代碼引出問題,再從指令集的角度去看問題
上述一個及其奇怪的結果,這個結果每一次運行都可能是不一樣的,Why ? 按照我們本來的想法是每一個線程 + 20000000 結果肯定應該是60000000呀,可以就是達不到這個值
為何? (深入匯編指令來看) 一定將過程放置到匯編指令上去看就可以理解這個過程了.
a++; 或者 a += 1; 這些操作的匯編操作是幾個步驟?
其實是三個步驟:
正常情況下,數據少,操作的線程少,問題倒是不大,想一想要是這樣的情況下,操作次數大,對齊操作的線程多,有些線程從中間切入進來了,在運算之後還沒寫回內存就另外一個線程切入進來同時對於之前的數據進行++ 再寫回內存, 啥效果,多次++ 操作之後結果確實一次加加操作後的結果。 這樣的操作 (術語叫做函數的重入) 我覺得其實就是重入到了匯編指令中間了,還沒將上一次運算的結果寫回內存就重新對這個內存讀取再運算寫入,結果肯定和正常的邏輯後的結果不一樣呀
來一幅圖片解釋一下
咋辦? 其實問題很清楚,我們只需要處理的是多條匯編指令不能讓它中間被插入其他的線程運算. (要想自己在執行匯編指令的時候別人不插入進來) 將多條匯編指令綁定成為一條指令不就OK了嘛。
也就是原子操作!!!
不會原子操作?操作系統給咱提供了線程的 綁定方式工具呀:mutex 互斥鎖(互斥量), 自旋鎖(spinlock), 讀寫鎖(readers-writer lock) 他們也稱作悲觀鎖. 作用都是一個樣,將多個匯編指令鎖成為一條原子操作 (此處的匯編指令也相當於如下的臨界資源)
悲觀鎖:鎖如其名,每次都悲觀地認為其他線程也會來修改數據,進行寫入操作,所以會在取數據前先加鎖保護,當其他線程想要訪問數據時,被阻塞掛起
樂觀鎖:每次取數據的時候,總是樂觀地認為數據不會被其他線程修改,因此不上鎖。但是在更新數據前, 會判斷其他數據在更新前有沒有對數據進行修改。
互斥鎖
最為常見使用地鎖就是互斥鎖, 也稱互斥量. mutex
特徵,當其他線程持有互斥鎖對臨界資源做寫入操作地時候,當前線程只能掛起等待,讓出CPU,存在線程間切換工作
解釋一下存在線程間切換工作 : 當線程試圖去獲取鎖對臨界資源做寫入操作時候,如果鎖被別的線程正在持有,該線程會保存上下文直接掛起,讓出CPU,等到鎖被釋放出來再進行線程間切換,從新持有CPU執行寫入操作
互斥鎖需要進行線程間切換,相比自旋鎖而言性能會差上許多,因為自旋鎖不會讓出CPU, 也就不需要進行線程間切換的步驟,具體原理下一點詳述
加互斥量(互斥鎖)確實可以達到要求,但是會發現運行時間非常的長,因為線程間不斷地切換也需要時間, 線程間切換的代價比較大.
相關視頻推薦
你繞不開的組件—鎖,4個方面手撕鎖的多種實現
「驚群」原理、鎖的設計方案及繞不開的「死鎖」問題
學習地址:C/C++Linux伺服器開發/後台架構師【零聲教育】-學習視頻教程-騰訊課堂
需要C/C++ Linux伺服器架構師學習資料加qun812855908獲取(資料包括 C/C++,Linux,golang技術,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒體,CDN,P2P,K8S,Docker,TCP/IP,協程,DPDK,ffmpeg 等),免費分享
自旋鎖
spinlock.自旋鎖.
對比互斥量(互斥鎖)而言,獲取自旋鎖不需要進行線程間切換,如果自旋鎖正在被別的線程佔用,該線程也不會放棄CPU進行掛起休眠,而是恰如其名的在哪裡不斷地循環地查看自旋鎖保持者(持有者)是否將自旋鎖資源釋放出來... (自旋地原來就是如此)
口語解釋自旋:持有自旋鎖的線程不釋放自旋鎖,那也沒有關系呀,我就在這里不斷地一遍又一遍地查詢自旋鎖是否釋放出來,一旦釋放出來我立馬就可以直接使用 (因為我並沒有掛起等待,不需要像互斥鎖還需要進行線程間切換,重新獲取CPU,保存恢復上下文等等操作)
哪正是因為上述這些特點,線程嘗試獲取自旋鎖,獲取不到不會採取休眠掛起地方式,而是原地自旋(一遍又一遍查詢自旋鎖是否可以獲取)效率是遠高於互斥鎖了. 那我們是不是所有情況都使用自旋鎖就行了呢,互斥鎖就可以放棄使用了嗎????
解釋自旋鎖地弊端:如果每一個線程都僅僅只是需要短時間獲取這個鎖,那我自旋占據CPU等待是沒啥問題地。要是線程需要長時間地使用占據(鎖)。。。 會造成過多地無端占據CPU資源,俗稱站著茅坑不拉屎... 但是要是僅僅是短時間地自旋,平衡CPU利用率 + 程序運行效率 (自旋鎖確實是在有些時候更加合適)
自旋鎖需要場景:內核可搶占或者SMP(多處理器)情況下才真正需求 (避免死鎖陷入死循環,瘋狂地自旋,比如遞歸獲取自旋鎖. 你獲取了還要獲取,但是又沒法釋放)
自旋鎖的使用函數其實和互斥鎖幾乎是一摸一樣地,僅僅只是需要將所有的mutex換成spin即可
僅僅只是在init存在些許不同
何為驚群,池塘一堆, 我瞄準一條插過去,但是好似所有的都像是覺著自己正在被插一樣的四處逃竄。 這個就是驚群的生活一點的理解
驚群現象其實一點也不少,比如說 accept pthread_cond_broadcast 還有多個線程共享epoll監視一個listenfd 然後此刻 listenfd 說來 SYN了,放在了SYN隊列中,然後完成了三次握手放在了 accept隊列中了, 現在問題是這個connect我應該交付給哪一個線程處理呢.
多個epoll監視准備工作的線程 就是這群 (),然後connet就是魚叉,這一叉下去肯定是所有的 epoll線程都會被驚醒 (多線程共享listenfd引發的epoll驚群)
同樣如果將上述的多個線程換成多個進程共享監視 同一個 listenfd 就是(多進程的epoll驚群現象)
咱再畫一個草圖再來理解一下這個驚群:
如果是多進程道理是一樣滴,僅僅只是將所有的線程換成進程就OK了
終是來到了今天的正題了: epoll驚群問題地解決上面了...
首先 先說說accept的驚群問題,沒想到吧accept 平時大家寫它的多線程地時候,多個線程同時accept同一個listensock地時候也是會存在驚群問題地,但是accept地驚群問題已經被Linux內核處理了: 當有新的連接進入到accept隊列的時候,內核喚醒且僅喚醒一個進程來處理
但是對於epoll的驚群問題,內核卻沒有直接進行處理。哪既然內核沒有直接幫我們處理,我們應該如何針對這種現象做出一定的措施呢?
驚群效應帶來的弊端: 驚群現象會造成epoll的偽喚醒,本來epoll是阻塞掛起等待著地,這個時候因為掛起等待是不會佔用CPU地。。。 但是一旦喚醒就會佔用CPU去處理發生地IO事件, 但是其實是一個偽喚醒,這個就是對於線程或者進程的無效調度。然而進程或者線程地調取是需要花費代價地,需要上下文切換。需要進行進程(線程)間的不斷切換... 本來多核CPU是用來支持高並發地,但是現在卻被用來無效地喚醒,對於多核CPU簡直就是一種浪費 (浪費系統資源) 還會影響系統的性能.
解決方式(一般是兩種)
Nginx的解決方式:
加鎖:驚群問題發生的前提是多個進程(線程)監聽同一個套接字(listensock)上的事件,所以我們只讓一個進程(線程)去處理監聽套接字就可以了。
畫兩張圖來理解一下:
上述還沒有進行一個每一個進程都對應一個listensock 而是多線程共享一個listensock 運行結果如下
所有的線程同時被喚醒了,但是實際上會處理連接的僅僅只是一個線程,
咱僅僅只是將主線程做如上這樣一個簡單的修改,每一個線程對應一個listensock;每一個線程一個獨有的監視窗口,將問題拋給內核去處理,讓內核去負載均衡 : 結果如下
僅僅喚醒一個線程來進行處理連接,解決了驚群問題
本文通過介紹兩種鎖入手,以及為什麼需要鎖,鎖本質就是為了保護,持有鎖你就有權力有能力操作寫入一定的臨界保護資源,沒有鎖你就不行需要等待,本質其實是將多條匯編指令綁定成原子操作
然後介紹了驚群現象,通過一個巧妙地例子,扔一顆石子,只是瞄準一條魚扔過去了,但是整池魚都被驚醒了,
對應我們地實際問題就是, 多個線程或者進程共同監視同一個listensock。。。。然後IO連接事件到來地時候本來僅僅只是需要一個線程醒過來處理即可,但是卻會使得所有地線程(進程)全部醒過來,造成不必要地進程線程間切換,多核CPU被浪費喔,系統資源被浪費
處理方式 一。 Nginx 源碼加互斥鎖處理。。 二。設置SO_REUSEPORT, 使得多個進程線程可以同時連接同一個port , 為每一個進程線程搞一個listensock... 將問題拋給內核去處理,讓他去負載均衡地僅僅將IO連接事件分配給一個進程或線程
Ⅱ linux 線程問題
主要理解在fork函數。
你可以參考
http://hi..com/huifeng00/blog/item/c7e9a4c6c5e6eac739db49b6.html
這會產生分支,
fork會產生一個子進程。
根據fork的返回值的不同運行不同的代碼。
在子進程返回值是0,而父進程就是調用這個函數的進程中返回的則是進程的ID也就是>0。
所以在子進程中執行
pthread_attr_init(&attr);
pthread_create(&tid,&attr,runner,NULL);
pthread_join(tid,NULL);
printf("CHILD:value=%d",value);
這個子進程會啟動一個線程
void*runner(void*param){
value=5;
pthread_exit(0);
}
讓value=5;
所以輸出
CHILD:value=5;
而父進程則執行
wait(NULL);
printf("PARENT:value=%d",value);
由於wait()所以它會暫停父進程,等待子進程結束。
子進程結束後,然後父進程運行輸出
PARENT:value=0
至於為什麼是0,因為子進程和父進程的數據是在內存中各有一份的。
子進程只是改變了它那份。父進程中還是0.
Ⅲ Linux線程喚醒多次
生產者產出數據到消費者獲得數據的延時較大,或者是CPU佔用較高。
生產者喚醒邏輯。pthread_mutex_lock(&mutex);pthread_cond_signal(&cond);pthread_mutex_unlock(&mutex);
條件變數的操作也需要達到線程安全的要求,因此需要互斥對象來進行保證。避免兩個線程同時操作條件變數引發問題。而通過查閱pthread_cond_wait()的相關資料可知,當程序運行到pthread_cond_wait()時,會將互斥對象鎖釋放,以便生產者能夠順利喚醒。而在消費者被成功喚醒,pthread_cond_wait()等待完成後,互斥對象會被重新上鎖直到手動釋放。
Ⅳ Linux下線程同步的幾種方法
Linux 線程同步的三種方法
線程的最大特點是資源的共享性,但資源共享中的同步問題是多線程編程的難點。linux下提供了多種方式來處理線程同步,最常用的是互斥鎖、條件變數和信號量。
一、互斥鎖(mutex)
通過鎖機制實現線程間的同步。
初始化鎖。在Linux下,線程的互斥量數據類型是pthread_mutex_t。在使用前,要對它進行初始化。
靜態分配:pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
動態分配:int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutex_attr_t *mutexattr);
加鎖。對共享資源的訪問,要對互斥量進行加鎖,如果互斥量已經上了鎖,調用線程會阻塞,直到互斥量被解鎖。
int pthread_mutex_lock(pthread_mutex *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
解鎖。在完成了對共享資源的訪問後,要對互斥量進行解鎖。
int pthread_mutex_unlock(pthread_mutex_t *mutex);
銷毀鎖。鎖在是使用完成後,需要進行銷毀以釋放資源。
int pthread_mutex_destroy(pthread_mutex *mutex);
[csharp] view plain
#include <cstdio>
#include <cstdlib>
#include <unistd.h>
#include <pthread.h>
#include "iostream"
using namespace std;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
int tmp;
void* thread(void *arg)
{
cout << "thread id is " << pthread_self() << endl;
pthread_mutex_lock(&mutex);
tmp = 12;
cout << "Now a is " << tmp << endl;
pthread_mutex_unlock(&mutex);
return NULL;
}
int main()
{
pthread_t id;
cout << "main thread id is " << pthread_self() << endl;
tmp = 3;
cout << "In main func tmp = " << tmp << endl;
if (!pthread_create(&id, NULL, thread, NULL))
{
cout << "Create thread success!" << endl;
}
else
{
cout << "Create thread failed!" << endl;
}
pthread_join(id, NULL);
pthread_mutex_destroy(&mutex);
return 0;
}
//編譯:g++ -o thread testthread.cpp -lpthread
二、條件變數(cond)
互斥鎖不同,條件變數是用來等待而不是用來上鎖的。條件變數用來自動阻塞一個線程,直到某特殊情況發生為止。通常條件變數和互斥鎖同時使用。條件變數分為兩部分: 條件和變數。條件本身是由互斥量保護的。線程在改變條件狀態前先要鎖住互斥量。條件變數使我們可以睡眠等待某種條件出現。條件變數是利用線程間共享的全局變數進行同步的一種機制,主要包括兩個動作:一個線程等待"條件變數的條件成立"而掛起;另一個線程使"條件成立"(給出條件成立信號)。條件的檢測是在互斥鎖的保護下進行的。如果一個條件為假,一個線程自動阻塞,並釋放等待狀態改變的互斥鎖。如果另一個線程改變了條件,它發信號給關聯的條件變數,喚醒一個或多個等待它的線程,重新獲得互斥鎖,重新評價條件。如果兩進程共享可讀寫的內存,條件變數可以被用來實現這兩進程間的線程同步。
初始化條件變數。
靜態態初始化,pthread_cond_t cond = PTHREAD_COND_INITIALIER;
動態初始化,int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr);
等待條件成立。釋放鎖,同時阻塞等待條件變數為真才行。timewait()設置等待時間,仍未signal,返回ETIMEOUT(加鎖保證只有一個線程wait)
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
int pthread_cond_timewait(pthread_cond_t *cond,pthread_mutex *mutex,const timespec *abstime);
激活條件變數。pthread_cond_signal,pthread_cond_broadcast(激活所有等待線程)
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond); //解除所有線程的阻塞
清除條件變數。無線程等待,否則返回EBUSY
int pthread_cond_destroy(pthread_cond_t *cond);
[cpp] view plain
#include <stdio.h>
#include <pthread.h>
#include "stdlib.h"
#include "unistd.h"
pthread_mutex_t mutex;
pthread_cond_t cond;
void hander(void *arg)
{
free(arg);
(void)pthread_mutex_unlock(&mutex);
}
void *thread1(void *arg)
{
pthread_cleanup_push(hander, &mutex);
while(1)
{
printf("thread1 is running\n");
pthread_mutex_lock(&mutex);
pthread_cond_wait(&cond, &mutex);
printf("thread1 applied the condition\n");
pthread_mutex_unlock(&mutex);
sleep(4);
}
pthread_cleanup_pop(0);
}
void *thread2(void *arg)
{
while(1)
{
printf("thread2 is running\n");
pthread_mutex_lock(&mutex);
pthread_cond_wait(&cond, &mutex);
printf("thread2 applied the condition\n");
pthread_mutex_unlock(&mutex);
sleep(1);
}
}
int main()
{
pthread_t thid1,thid2;
printf("condition variable study!\n");
pthread_mutex_init(&mutex, NULL);
pthread_cond_init(&cond, NULL);
pthread_create(&thid1, NULL, thread1, NULL);
pthread_create(&thid2, NULL, thread2, NULL);
sleep(1);
do
{
pthread_cond_signal(&cond);
}while(1);
sleep(20);
pthread_exit(0);
return 0;
}
[cpp] view plain
#include <pthread.h>
#include <unistd.h>
#include "stdio.h"
#include "stdlib.h"
static pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
struct node
{
int n_number;
struct node *n_next;
}*head = NULL;
static void cleanup_handler(void *arg)
{
printf("Cleanup handler of second thread./n");
free(arg);
(void)pthread_mutex_unlock(&mtx);
}
static void *thread_func(void *arg)
{
struct node *p = NULL;
pthread_cleanup_push(cleanup_handler, p);
while (1)
{
//這個mutex主要是用來保證pthread_cond_wait的並發性
pthread_mutex_lock(&mtx);
while (head == NULL)
{
//這個while要特別說明一下,單個pthread_cond_wait功能很完善,為何
//這里要有一個while (head == NULL)呢?因為pthread_cond_wait里的線
//程可能會被意外喚醒,如果這個時候head != NULL,則不是我們想要的情況。
//這個時候,應該讓線程繼續進入pthread_cond_wait
// pthread_cond_wait會先解除之前的pthread_mutex_lock鎖定的mtx,
//然後阻塞在等待對列里休眠,直到再次被喚醒(大多數情況下是等待的條件成立
//而被喚醒,喚醒後,該進程會先鎖定先pthread_mutex_lock(&mtx);,再讀取資源
//用這個流程是比較清楚的
pthread_cond_wait(&cond, &mtx);
p = head;
head = head->n_next;
printf("Got %d from front of queue/n", p->n_number);
free(p);
}
pthread_mutex_unlock(&mtx); //臨界區數據操作完畢,釋放互斥鎖
}
pthread_cleanup_pop(0);
return 0;
}
int main(void)
{
pthread_t tid;
int i;
struct node *p;
//子線程會一直等待資源,類似生產者和消費者,但是這里的消費者可以是多個消費者,而
//不僅僅支持普通的單個消費者,這個模型雖然簡單,但是很強大
pthread_create(&tid, NULL, thread_func, NULL);
sleep(1);
for (i = 0; i < 10; i++)
{
p = (struct node*)malloc(sizeof(struct node));
p->n_number = i;
pthread_mutex_lock(&mtx); //需要操作head這個臨界資源,先加鎖,
p->n_next = head;
head = p;
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mtx); //解鎖
sleep(1);
}
printf("thread 1 wanna end the line.So cancel thread 2./n");
//關於pthread_cancel,有一點額外的說明,它是從外部終止子線程,子線程會在最近的取消點,退出
//線程,而在我們的代碼里,最近的取消點肯定就是pthread_cond_wait()了。
pthread_cancel(tid);
pthread_join(tid, NULL);
printf("All done -- exiting/n");
return 0;
}
三、信號量(sem)
如同進程一樣,線程也可以通過信號量來實現通信,雖然是輕量級的。信號量函數的名字都以"sem_"打頭。線程使用的基本信號量函數有四個。
信號量初始化。
int sem_init (sem_t *sem , int pshared, unsigned int value);
這是對由sem指定的信號量進行初始化,設置好它的共享選項(linux 只支持為0,即表示它是當前進程的局部信號量),然後給它一個初始值VALUE。
等待信號量。給信號量減1,然後等待直到信號量的值大於0。
int sem_wait(sem_t *sem);
釋放信號量。信號量值加1。並通知其他等待線程。
int sem_post(sem_t *sem);
銷毀信號量。我們用完信號量後都它進行清理。歸還佔有的一切資源。
int sem_destroy(sem_t *sem);
[cpp] view plain
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <semaphore.h>
#include <errno.h>
#define return_if_fail(p) if((p) == 0){printf ("[%s]:func error!/n", __func__);return;}
typedef struct _PrivInfo
{
sem_t s1;
sem_t s2;
time_t end_time;
}PrivInfo;
static void info_init (PrivInfo* thiz);
static void info_destroy (PrivInfo* thiz);
static void* pthread_func_1 (PrivInfo* thiz);
static void* pthread_func_2 (PrivInfo* thiz);
int main (int argc, char** argv)
{
pthread_t pt_1 = 0;
pthread_t pt_2 = 0;
int ret = 0;
PrivInfo* thiz = NULL;
thiz = (PrivInfo* )malloc (sizeof (PrivInfo));
if (thiz == NULL)
{
printf ("[%s]: Failed to malloc priv./n");
return -1;
}
info_init (thiz);
ret = pthread_create (&pt_1, NULL, (void*)pthread_func_1, thiz);
if (ret != 0)
{
perror ("pthread_1_create:");
}
ret = pthread_create (&pt_2, NULL, (void*)pthread_func_2, thiz);
if (ret != 0)
{
perror ("pthread_2_create:");
}
pthread_join (pt_1, NULL);
pthread_join (pt_2, NULL);
info_destroy (thiz);
return 0;
}
static void info_init (PrivInfo* thiz)
{
return_if_fail (thiz != NULL);
thiz->end_time = time(NULL) + 10;
sem_init (&thiz->s1, 0, 1);
sem_init (&thiz->s2, 0, 0);
return;
}
static void info_destroy (PrivInfo* thiz)
{
return_if_fail (thiz != NULL);
sem_destroy (&thiz->s1);
sem_destroy (&thiz->s2);
free (thiz);
thiz = NULL;
return;
}
static void* pthread_func_1 (PrivInfo* thiz)
{
return_if_fail(thiz != NULL);
while (time(NULL) < thiz->end_time)
{
sem_wait (&thiz->s2);
printf ("pthread1: pthread1 get the lock./n");
sem_post (&thiz->s1);
printf ("pthread1: pthread1 unlock/n");
sleep (1);
}
return;
}
static void* pthread_func_2 (PrivInfo* thiz)
{
return_if_fail (thiz != NULL);
while (time (NULL) < thiz->end_time)
{
sem_wait (&thiz->s1);
printf ("pthread2: pthread2 get the unlock./n");
sem_post (&thiz->s2);
printf ("pthread2: pthread2 unlock./n");
sleep (1);
}
return;
}
Ⅳ Linux C++多線程同步的四種方式
From : https://blog.csdn.net/qq_39382769/article/details/96075346
1.同一個線程內部,指令按照先後順序執行;但不同線程之間的指令很難說清楚是哪一個先執行,在並發情況下,指令執行的先後順序由內核決定。
如果運行的結果依賴於不同線程執行的先後的話,那麼就會形成競爭條件,在這樣的情況下,計算的結果很難預知,所以應該盡量避免競爭條件的形成。
2.最常見的解決競爭條件的方法是:將原先分離的兩個指令構成一個不可分割的原子操作,而其他任務不能插入到原子操作中!
3.對多線程來說,同步指的是在一定時間內只允許某一個線程訪問某個資源,而在此時間內,不允許其他線程訪問該資源!
互斥鎖
條件變數
讀寫鎖
信號量
一種特殊的全局變數,擁有lock和unlock兩種狀態。
unlock的互斥鎖可以由某個線程獲得,一旦獲得,這個互斥鎖會鎖上變成lock狀態,此後只有該線程由權力打開該鎖,其他線程想要獲得互斥鎖,必須得到互斥鎖再次被打開之後。
1.互斥鎖的初始化, 分為靜態初始化和動態初始化.
2.互斥鎖的相關屬性及分類
(1) attr表示互斥鎖的屬性;
(2) pshared表示互斥鎖的共享屬性,由兩種取值:
1)PTHREAD_PROCESS_PRIVATE:鎖只能用於一個進程內部的兩個線程進行互斥(默認情況)
2)PTHREAD_PROCESS_SHARED:鎖可用於兩個不同進程中的線程進行互斥,使用時還需要在進程共享內存中分配互斥鎖,然後為該互斥鎖指定屬性就可以了。
互斥鎖存在缺點:
(1)某個線程正在等待共享數據內某個條件出現。
(2)重復對數據對象加鎖和解鎖(輪詢),但是這樣輪詢非常耗費時間和資源,而且效率非常低,所以互斥鎖不太適合這種情況。
當線程在等待滿足某些條件時,使線程進入睡眠狀態;一旦條件滿足,就換線因等待滿足特定條件而睡眠的線程。
程序的效率無疑會大大提高。
1)創建
靜態方式:pthread_cond_t cond PTHREAD_COND_INITIALIZER
動態方式:int pthread_cond_init(&cond,NULL)
Linux thread 實現的條件變數不支持屬性,所以NULL(cond_attr參數)
2)注銷
int pthread_cond_destory(&cond)
只有沒有線程在該條件變數上,該條件變數才能注銷,否則返回EBUSY
因為Linux實現的條件變數沒有分配什麼資源,所以注銷動作只包括檢查是否有等待線程!(請參考條件變數的底層實現)
3)等待
條件等待:int pthread_cond_wait(&cond,&mutex)
計時等待:int pthread_cond_timewait(&cond,&mutex,time)
1.其中計時等待如果在給定時刻前條件沒有被滿足,則返回ETIMEOUT,結束等待
2.無論那種等待方式,都必須有一個互斥鎖配合,以防止多個線程同時請求pthread_cond_wait形成競爭條件!
3.在調用pthread_cond_wait前必須由本線程加鎖
4)激發
激發一個等待線程:pthread_cond_signal(&cond)
激發所有等待線程:pthread_cond_broadcast(&cond)
重要的是,pthread_cond_signal不會存在驚群效應,也就是是它最多給一個等待線程發信號,不會給所有線程發信號喚醒,然後要求他們自己去爭搶資源!
pthread_cond_broadcast() 喚醒所有正在pthread_cond_wait()的同一個條件變數的線程。注意:如果等待的多個現場不使用同一個鎖,被喚醒的多個線程執行是並發的。
pthread_cond_broadcast & pthread_cond_signal
1.讀寫鎖比互斥鎖更加具有適用性和並行性
2.讀寫鎖最適用於對數據結構的讀操作讀操作次數多餘寫操作次數的場合!
3.鎖處於讀模式時可以線程共享,而鎖處於寫模式時只能獨占,所以讀寫鎖又叫做共享-獨占鎖。
4.讀寫鎖有兩種策略:強讀同步和強寫同步
強讀同步:
總是給讀者更高的優先權,只要寫者沒有進行寫操作,讀者就可以獲得訪問許可權
強寫同步:
總是給寫者更高的優先權,讀者只能等到所有正在等待或者執行的寫者完成後才能進行讀
1)初始化的銷毀讀寫鎖
靜態初始化:pthread_rwlock_t rwlock=PTHREAD_RWLOCK_INITIALIZER
動態初始化:int pthread_rwlock_init(rwlock,NULL),NULL代表讀寫鎖採用默認屬性
銷毀讀寫鎖:int pthread_rwlock_destory(rwlock)
在釋放某個讀寫鎖的資源之前,需要先通過pthread_rwlock_destory函數對讀寫鎖進行清理。釋放由pthread_rwlock_init函數分配的資源
如果你想要讀寫鎖使用非默認屬性,則attr不能為NULL,得給attr賦值
int pthread_rwlockattr_init(attr),給attr初始化
int pthread_rwlockattr_destory(attr),銷毀attr
2)以寫的方式獲取鎖,以讀的方式獲取鎖,釋放讀寫鎖
int pthread_rwlock_rdlock(rwlock),以讀的方式獲取鎖
int pthread_rwlock_wrlock(rwlock),以寫的方式獲取鎖
int pthread_rwlock_unlock(rwlock),釋放鎖
上面兩個獲取鎖的方式都是阻塞的函數,也就是說獲取不到鎖的話,調用線程不是立即返回,而是阻塞執行,在需要進行寫操作的時候,這種阻塞式獲取鎖的方式是非常不好的,你想一下,我需要進行寫操作,不但沒有獲取到鎖,我還一直在這里等待,大大拖累效率
所以我們應該採用非阻塞的方式獲取鎖:
int pthread_rwlock_tryrdlock(rwlock)
int pthread_rwlock_trywrlock(rwlock)
互斥鎖只允許一個線程進入臨界區,而信號量允許多個線程進入臨界區。
1)信號量初始化
int sem_init(&sem,pshared, v)
pshared為0,表示這個信號量是當前進程的局部信號量。
pshared為1,表示這個信號量可以在多個進程之間共享。
v為信號量的初始值。
返回值:
成功:0,失敗:-1
2)信號量值的加減
int sem_wait(&sem):以原子操作的方式將信號量的值減去1
int sem_post(&sem):以原子操作的方式將信號量的值加上1
3)對信號量進行清理
int sem_destory(&sem)
Ⅵ linux的CmakeList.txt怎麼寫解決多線程喚起同一個文件(多次)
CMake是一個跨平台的安裝(編譯)工具,可以用簡單的語句來描述所有平台的安裝(編譯過程)。他能夠輸出各種各樣的makefile或者project文件,能測試編譯器所支持的C++特性,類似UNIX下的automake。只是 CMake 的組態檔取名為 CmakeLists.txt。Cmake 並不直接建構出最終的軟體,而是產生標準的建構檔(如 Unix 的 Makefile 或 Windows Visual C++ 的 projects/workspaces),然後再依一般的建構方式使用。這使得熟悉某個集成開發環境(IDE)的開發者可以用標準的方式建構他的軟體,這種可以使用各平台的原生建構系統的能力是 CMake 和 SCons 等其他類似系統的區別之處。CMake 可以編譯源代碼、製作程式庫、產生適配器(wrapper)、還可以用任意的順序建構執行檔。CMake 支持 in-place 建構(二進檔和源代碼在同一個目錄樹中)和 out-of-place 建構(二進檔在別的目錄里),因此可以很容易從同一個源代碼目錄樹中建構出多個二進檔。CMake 也支持靜態與動態程式庫的建構。
Ⅶ Linux 多線程編程(二)2019-08-10
三種專門用於線程同步的機制:POSIX信號量,互斥量和條件變數.
在Linux上信號量API有兩組,一組是System V IPC信號量,即PV操作,另外就是POSIX信號量,POSIX信號量的名字都是以sem_開頭.
phshared參數指定信號量的類型,若其值為0,就表示這個信號量是當前進程的局部信號量,否則該信號量可以在多個進程之間共享.value值指定信號量的初始值,一般與下面的sem_wait函數相對應.
其中比較重要的函數sem_wait函數會以原子操作的方式將信號量的值減一,如果信號量的值為零,則sem_wait將會阻塞,信號量的值可以在sem_init函數中的value初始化;sem_trywait函數是sem_wait的非阻塞版本;sem_post函數將以原子的操作對信號量加一,當信號量的值大於0時,其他正在調用sem_wait等待信號量的線程將被喚醒.
這些函數成功時返回0,失敗則返回-1並設置errno.
生產者消費者模型:
生產者對應一個信號量:sem_t procer;
消費者對應一個信號量:sem_t customer;
sem_init(&procer,2)----生產者擁有資源,可以工作;
sem_init(&customer,0)----消費者沒有資源,阻塞;
在訪問公共資源前對互斥量設置(加鎖),確保同一時間只有一個線程訪問數據,在訪問完成後再釋放(解鎖)互斥量.
互斥鎖的運行方式:串列訪問共享資源;
信號量的運行方式:並行訪問共享資源;
互斥量用pthread_mutex_t數據類型表示,在使用互斥量之前,必須使用pthread_mutex_init函數對它進行初始化,注意,使用完畢後需調用pthread_mutex_destroy.
pthread_mutex_init用於初始化互斥鎖,mutexattr用於指定互斥鎖的屬性,若為NULL,則表示默認屬性。除了用這個函數初始化互斥所外,還可以用如下方式初始化:pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER。
pthread_mutex_destroy用於銷毀互斥鎖,以釋放佔用的內核資源,銷毀一個已經加鎖的互斥鎖將導致不可預期的後果。
pthread_mutex_lock以原子操作給一個互斥鎖加鎖。如果目標互斥鎖已經被加鎖,則pthread_mutex_lock則被阻塞,直到該互斥鎖佔有者把它給解鎖.
pthread_mutex_trylock和pthread_mutex_lock類似,不過它始終立即返回,而不論被操作的互斥鎖是否加鎖,是pthread_mutex_lock的非阻塞版本.當目標互斥鎖未被加鎖時,pthread_mutex_trylock進行加鎖操作;否則將返回EBUSY錯誤碼。注意:這里討論的pthread_mutex_lock和pthread_mutex_trylock是針對普通鎖而言的,對於其他類型的鎖,這兩個加鎖函數會有不同的行為.
pthread_mutex_unlock以原子操作方式給一個互斥鎖進行解鎖操作。如果此時有其他線程正在等待這個互斥鎖,則這些線程中的一個將獲得它.
三個列印機輪流列印:
輸出結果:
如果說互斥鎖是用於同步線程對共享數據的訪問的話,那麼條件變數就是用於在線程之間同步共享數據的值.條件變數提供了一種線程之間通信的機制:當某個共享數據達到某個值時,喚醒等待這個共享數據的線程.
條件變數會在條件不滿足的情況下阻塞線程.且條件變數和互斥量一起使用,允許線程以無競爭的方式等待特定的條件發生.
其中pthread_cond_broadcast函數以廣播的形式喚醒所有等待目標條件變數的線程,pthread_cond_signal函數用於喚醒一個等待目標條件變數線程.但有時候我們可能需要喚醒一個固定的線程,可以通過間接的方法實現:定義一個能夠唯一標識目標線程的全局變數,在喚醒等待條件變數的線程前先設置該變數為目標線程,然後採用廣播的方式喚醒所有等待的線程,這些線程被喚醒之後都檢查該變數以判斷是否是自己.
採用條件變數+互斥鎖實現生產者消費者模型:
運行結果:
阻塞隊列+生產者消費者
運行結果:
Ⅷ linux進程、線程及調度演算法(二)
執行一個 ,但是只要任何修改,都造成分裂如,修改了chroot,寫memory,mmap,sigaction 等。
p1 是一個 task_struct, p2 也是一個 task_struct. linux內核的調度器只認得task_struck (不管你是進程還是線程), 對其進行調度。
p2 的task_struck 被創建出來後,也有一份自己的資源。但是這些資源會短暫的與p1 相同。
進程是區分資源的單位,你的資源是我的資源,那從概念上將就不叫進程。
其他資源都好分配,唯一比較難的是內存資源的重新分配。
非常簡單的程序,但是可以充分說明 COW。
結果:10 -> 20 -> 10
COW 是嚴重依賴於CPU中的MMU。CPU如果沒有 MMU,fork 是不能工作的。
在沒有mmu的CPU中,不可能執行COW 的,所以只有vfork
vfork與fork相比的不同
P2沒有自己的 task_struct, 也就是說P1 的內存資源 就是 P2的內存資源。
結果 10,20,20
vfork:
vfork 執行上述流程,P2也只是指向了P1的mm,那麼將這個vfork 放大,其餘的也全部clone,共同指向P1,那麼就是線程的屬性了。
phtread_create -> Clone()
P1 P2 在內核中都是 task_struct. 都可以被調度。共享資源可調度,即線程。 這就是線程為什麼也叫做輕量級進程
不需要太糾結線程和進程的區別。
4651 : TGID
4652, 4653 tid 內核中 task_struct 真正的pid
linux 總是白發人 送 黑發人。如果父進程在子進程推出前掛掉了。那麼子進程應該怎麼辦?
p3 -> init, p5 -> subreaper
每一個孤兒都會找最近的火葬場
可以設置進程的屬性,將其變為subreaper,會像1號進程那樣收養孤兒進程。
linux的進程睡眠依靠等待隊列,這樣的機制類似與涉及模式中的訂閱與發布。
睡眠,分兩種
每一個進程都是創建出來的,那麼第一個進程是誰創建的呢?
init 進程是被linux的 0 進程 創建出來的。開機創建。
父進程就是 0 號進程,但在pstree,是看不到0進程的。因為0進程創建子進程後,就退化成了idle進程。
idle進程是 linux內核里,特殊調度類。 所有進程都睡眠停止 ,則調度idle進程,進入到 wait for interrupte 等中斷。此時 cpu及其省電,除非來一個中斷,才能再次被喚醒。
喚醒後的任何進程,從調度的角度上說,都比idle進程地位高。idle是調度級別最最低的進程。
0 進程 一跑,則進入等中斷。一旦其他進程被喚醒,就輪不到 0進程了。
所有進程都睡了,0就上來,則cpu需要進入省電模式