進程線程通信同步以及對應原型函數

進程通信和同步(進程的同步是在進程通信基礎上使用的)

進程通信(參考APUE)

主要方式: 管道、信號、信號量、消息隊列、共享內存、套接字

管道

又分爲有名管道和無名管道,管道都是半雙工的

  • 有名管道:任意進程之間的通信,有名管道就是FIFO,採用先進先出隊列,只允許數據單向流動,linux |就是管道。
  • 無名管道:父子進程通信

匿名管道 用於解決父子關係的,用fork來創建子進程。

#include<unistd.h>
int pipe(int pipefd[2]);

數組中存着前者是管道的讀端,後者是寫端,一個進程可以關閉某一個端口來實現自己到底是讀還是寫。
如fork出一個子進程,想要父寫子讀的話,父進程就關閉讀端 close(pipefd[0]) ,子進程就關閉寫端close(pipefd[1]); 創建好管道之後就可以通過read 和 write來進行讀寫操作。
write(pipefd[1],buf,strlen(buf)); read(pipefd[0],buf,strlen(buf));

樣例代碼參照底部

有名管道

fifo創建一個命名管道,可以解決非血緣關係的進程之間的通信。
管道的是實現方式也是通過文件來實現的。
首先命令行 mkfifo filename就可以創建一個管道文件。
然後在不同進程裏傳入文件名即可進行讀寫,open,write,close open,read,close

信號

信號就是由用戶、系統和進程發送給目標進程的信息,通知目標進程中某個狀態的改變或者異常。

信號的產生分爲硬中斷和軟中斷

  1. 終端中駛入特殊字符來產生信號,如 ctrl+某個信號
  2. 系統異常,訪問非法內存,浮點數異常
  3. 系統狀態變化。設置了alarm定時器,到時間會引起信號
  4. kill指令或者函數。

函數原型: sighandler_t signal(int signum,sighandler_t handler),handler一般是一個函數,這個函數傳入信號的數值,根據信號數值表設計判斷就可以進行不同的處理了。

SIGINT Term 鍵盤輸入以終端進程(ctrl + C)

共享內存

mmap(系統調用)

將磁盤文件的一部分直接映射到進程的內存中,mmap設置了兩種機制:共享和私有
共享映射:內存中對文件進行修改,那麼磁盤中對應的文件也會被修改。
私有映射:內存中的文件和磁盤中的文件是獨立關係的,兩者進行修改都不會對對方造成影響

函數原型

#include<sys/mman.h>
void *mmap(void* addr,size_t length, int prot , int flags ,int fd, off_t offset);
int munmap(void* addr,size_t length);

mmap存在一個問題
當flags爲MAP_SHARED的時候,即使修改了內存,並不能保證映射文件能夠馬上更新,映射文件的更新是由內核虛擬內存調度算法進行的。因此如果兩個進程同時寫會導致映射文件內容的不可預知性。

shmget(System V)

需要進行同步

int shmget(key_t key, size_t size, int shmflg);//創建共享內存,key命名
void *shmat(int shm_id, const void *shm_addr, int shmflg);//啓動共享內存,把共享內存連接到當前進程地址空間,因爲是一個void*指針,所以可以用任意對象指針來改變。如 shared = (string *)shm; 然後就可以對shared進行修改就是修改共享內存。
int shmdt(const void *shmaddr);//分離共享內存
int shmctl(int shm_id, int command, struct shmid_ds *buf);//控制共享內存

消息隊列

消息隊列是在內存中獨立於進程的一段內存區域,創建了消息隊列,任何進程只要有訪問權限就可以訪問消息隊列。

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgget(key_t key, int msgflg);//創建一個消息隊列 返回msqid
int msgctl(int msqid,int cmd,struct msqid_ds *buf);// 獲取和設置消息隊列的屬性
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);//發送到消息隊列, msgp就是發送的數據,數據結構中第一個字段必須爲long類型
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg);//接受消息。

信號量

int semget(key_t key,int nsems,int flags);//第二個參數是信號量集的個數
int semctl(int semid,int semnum,int cmd,su);//根據cmd來判斷是SETVAL還是GETVAL
semop(int id,struct sembuf sb[],int len)// sembuf是一個結構體
struct sembuf{      short sem_num,  //信號量的下標      
                    short sem_op,     // 1表示V操作,-1表示P操作     
                    short  sem_flg   //一般填0,表示如果信號量爲0就阻塞;

socket(不在此處介紹)

線程同步

因爲線程是共享進程的內存空間的,因此線程基本上是不需要進行數據交換,主要要做的是線程之間的同步。

  1. 鎖機制:包括互斥鎖/互斥量、讀寫鎖、自旋鎖、條件變量
    • 互斥鎖 互斥量:用排他的方式來限制數據結構被併發修改。
    • 讀寫鎖:多個線程可以同時讀,但是寫互斥
    • 自旋鎖:互斥鎖會引起調用者睡眠,但是自旋鎖不會,自旋鎖會循環檢測資源是否可以訪問. 因爲自旋鎖的使用者一般保持鎖很短的時間,因此選擇自旋等待而不是睡眠可以提高處理效率.
  2. 信號量機制:無名線程信號量 有名線程信號量
  3. 信號機制:類似於進程的信號
  4. 屏障:屏障允許每個線程等待,直到所有的合作線程都達到某一個點,然後從該點繼續執行。
  5. 條件變量

互斥鎖

  • 常見問題,兩個線程給一個引用變量加1一共10000次,那麼最後的結果會是多少呢?比如 sums=sums+i
    最後的結果是不可預知的,因爲兩個線程使用的一個共享資源,有可能某個線程加的過程中還未賦值的時候切換到另一個線程,另一個線程上加了上去同時賦值了sums,然後切換回原來的線程,但是原來的線程已經完成了sums+i的運算,於是就把sums的值重新覆蓋了。

出現這個的原因是 sums++/sums+=1/sums=sums+1 這些操作都不是原子操作,都是通過操作符函數來實現的,操作符函數是對地址的值或者臨時變量上加1 然後返回數值;這是分兩步走的,所以多線程下會出現不可預知性。

要解決上面這種情況就要多線程之間的互斥鎖

  • 第一種鎖
    std::mutex mylock; mylock.lock() sums+=1; mylock.unlock();
    線程傳入參數 std::thread t1(work,std::ref(val),std::ref(mylock)); 如果線程需要引用參數就必須要用std::ref
  • 第二種鎖
{
    std::mutex mylock;
    std::lock_guard<std::mutex> mylock_guard(mylock);//當對象被創建的時候上鎖,被銷燬的時候解鎖; 這個對象只有構造和析構函數兩個函數。
}
  • 第三種
{
    std::mutex mylock;
    std::unique_lock<mutex> ulock(mylock);//當對象被創建的時候上鎖,被銷燬的時候解鎖; 這個對象只有構造和析構函數兩個函數。
}

unique_lock相比於lock_guard來的複雜一些,lock_gurad只有兩個函數,而unique_lock可用的函數更多,因此從操作上來說更加的靈活。

條件變量

condition_variable是一個類,通常搭配互斥量mutex來用。 條件變量一般是在消費者生產者中使用,因爲可以用條件變量來喚醒。
需要知道的兩個函數是wait和notify_*函數。

notify_one每次只會喚醒一個線程。如果用notify_all就會喚起所有線程,但是每次只有一個線程能夠繼續後面的工作,剩下的線程又只能繼續等待,這樣多個線程等待一個喚醒的情況就是驚羣效應。

驚羣效應消耗了什麼資源?
Linux會對每一個線程(進程)進行調度、上下文切換。 上下文切換過高使得CPU就像一個搬運工,在寄存器和運行隊列中奔波。
直接的消耗包括 CPU 寄存器要保存和加載(例如程序計數器)、系統調度器的代碼需要執行。間接的消耗在於多核 cache 之間的共享數據。
爲了確保只有一個進程(線程)得到資源,需要對資源操作進行加鎖保護,加大了系統的開銷。

參考代碼見樣例代碼。

讀寫鎖

  1. 如果一個線程用讀鎖鎖住了臨界區,那麼其他的讀線程還是可以用讀鎖來訪問,這樣就可以由多個線程並行操作。 這時候來一個寫鎖的話就會被阻塞, 寫鎖阻塞後,後來的讀鎖也都會被阻塞,這樣做就可以避免讀鎖長期佔用臨界區資源,防止寫鎖飢餓。
  2. 如果一個線程的寫鎖鎖住了臨界區,那麼之後無論什麼鎖都會發生阻塞。

實現方法上有兩種,一種是C14的新特性,一種是POSIX下的pthread中實現的讀寫鎖的機制。
C14
C14中提供了一個新的鎖方式 std::shared_lock 能夠以共享模式進行鎖定,其實就是讀鎖。 但是mutex是不可以多次加鎖的因此C14中還提供了std::shared_timed_mutex

std::lock_guardstd::shared_timed_mutex writerLock(shared_mutex);//以排他性方式上鎖unique_lock可以這麼做。
std::shared_lockstd::shared_timed_mutex readerLock(shared_mutex)//;以共享所有權方式上鎖
上面兩者都是退出作用域就可以自動解鎖。

pthread
初始化讀寫鎖 pthread_rwlock_init 語法
讀取讀寫鎖中的鎖 pthread_rwlock_rdlock 語法
讀取非阻塞讀寫鎖中的鎖pthread_rwlock_tryrdlock 語法
寫入讀寫鎖中的鎖 pthread_rwlock_wrlock 語法
寫入非阻塞讀寫鎖中的鎖pthread_rwlock_trywrlock 語法
解除鎖定讀寫鎖 pthread_rwlock_unlock 語法
銷燬讀寫鎖 pthread_rwlock_destroy 語法

自旋鎖

自旋鎖是一種用於保護多線程共享資源的鎖,與一般的互斥鎖(mutex)不同之處在於當自旋鎖嘗試獲取鎖的所有權時會以忙等待(busy waiting)的形式不斷的循環檢查鎖是否可用。在多處理器環境中對持有鎖時間較短的程序來說使用自旋鎖代替一般的互斥鎖往往能提高程序的性能。

CAS操作

CAS(Compare and Swap),即比較並替換,實現併發算法時常用到的一種技術,這種操作提供了硬件級別的原子操作(通過鎖總線的方式)

bool CAS(V,A,B)
{
    if(V==A){
        swap(V,B);
        return true;
     }
     return false;
}

自旋鎖的用途,本質上是希望使得一個線程在不滿足情況下,一直處於輪詢狀態;x,那麼僞代碼邏輯

b=true
while(CAS(flag,false,b)==false);//如果flag==true那麼就一直循環,如果flag==false就把flag=b,同時退出循環
//do something
flag=false;

樣例代碼

  1. 無名管道

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <string.h>

int main(void){
        char buf[1024] = "Hello Child\n";
        char str[1024];
        int fd[2];
        if(pipe(fd) == -1){
                perror("pipe");
                exit(1);
        }
        pid_t pid = fork();
        // 父寫子讀 0寫端 1讀端
        if(pid > 0){
                printf("parent pid\n");
                close(fd[0]);                    // 關閉讀端
                sleep(5);
                write(fd[1], buf, strlen(buf));  // 在寫端寫入buf中的數據
                wait(NULL);
                close(fd[1]);
        }
        else if(pid == 0){
                close(fd[1]);                   // 關閉寫端
                int len = read(fd[0], str, sizeof(str));   // 在讀端將數據讀到str
                write(STDOUT_FILENO, str, len);
                close(fd[0]);
        }
        else {
                perror("fork");
                exit(1);
        }
        return 0;}

2.信號量

#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
using namespace std;

void sig_handler(int signum)
{
    if(0 > signum)
    {
        fprintf(stderr,"sig_handler param err. [%d]\n",signum);
        return;
    }
    if(SIGINT == signum)
    {
        printf("Received signal [%s]\n",SIGINT==signum?"SIGINT":"Other");
    }
    if(SIGQUIT == signum)
    {
        printf("Received signal [%s]\n",SIGQUIT==signum?"SIGQUIT":"Other");
    }

    return;
}

int main(int argc,char **argv)
{
    printf("Wait for the signal to arrive.\n ");

    /*登記信息*/
    signal(SIGINT,sig_handler);
    signal(SIGQUIT,sig_handler);

    pause();
    pause();

    signal(SIGINT,SIG_IGN);
    return 0;
}
  1. 條件變量+mutex

#include <iostream>
#include <thread>
#include <mutex>
#include <queue>
#include <windows.h>
#include <condition_variable>

std::mutex mtx;        // 全局互斥鎖
std::queue<int> que;   // 全局消息隊列
std::condition_variable cr;   // 全局條件變量
int cnt = 1;           // 數據

void producer() {
        while(true) {
                {
                        std::unique_lock<std::mutex> lck(mtx);//在這個作用域內lck可以使用,創建即上鎖,銷燬即解鎖。
                        // 在這裏也可以加上wait 防止隊列堆積  while(que.size() >= MaxSize) que.wait();
                        que.push(cnt);
                        std::cout << "向隊列中添加數據:" << cnt ++ << std::endl;
                        // 這裏用大括號括起來了 爲了避免出現虛假喚醒的情況 所以先unlock 再去喚醒
                }
                cr.notify_all();       // 喚醒所有wait
        }}

void consumer() {
        while (true) {
                std::unique_lock<std::mutex> lck(mtx);
                while (que.size() == 0) {           // 這裏防止出現虛假喚醒  所以在喚醒後再判斷一次
                        cr.wait(lck);
                }
                int tmp = que.front();
                std::cout << "從隊列中取出數據:" << tmp << std::endl;
                que.pop();
        }}

int main(){
        std::thread thd1[2], thd2[2];
        for (int i = 0; i < 2; i++) {
                thd1[i] = std::thread(producer);
                thd2[i] = std::thread(consumer);
                thd1[i].join();
                thd2[i].join();
        }
        return 0;}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章