UNIX高級環境編程 第12章 [12.5-12.9]

第12章 線程控制

12.5 重入

可重入函數數組

  • 中斷一個可重入函數的執行,轉而執行另一個函數(通常爲信號處理程序,注意此時依然爲同一個線程),
    返回可重入函數執行不會出現錯誤。安全

  • 可重入與異步信號安全等價(APUE 3 edition, 10.6 )多線程

  • 可重入函數除了使用本身棧上的變量之外不依賴於任何環境(包括 static),這樣的函數就是可重入的,
    能夠容許有該函數的多個副本同時運行異步

線程安全:函數

  • 若是一個函數在相同的時間點能夠被多個線程安全地調用,就稱該函數是線程安全的。spa

  • 1) 若一個函數如同可重入函數同樣,也沒有使用靜態數據(全局變量或static局部變量),那麼該函數也是線程安全的。
    2) 或使用了靜態數據,但採用了同步機制,保證線程的安全調用,那麼也是線程安全的。操作系統

可重入與線程安全的關係:線程

clipboard.png

-------------------------------[Figure 1]--------------------------------指針

舉例說明:func是線程安全的,但不是可重入的:
1)線程安全是顯然的,第b步在任意時刻只有一個線程訪問。
2)假設進程執行完第a步,接收到一個信號,轉而執行信號處理程序,恰巧該信號處理程序中也調用了函數func,那麼就陷入了死鎖。
3) 固然能夠利用遞歸鎖,那麼func也是可重入的,可是遞歸鎖的使用可能會帶來其餘嚴重問題rest

clipboard.png

-------------------------------[Figure 2]--------------------------------

支持線程安全函數的操做系統實現會在<unistd.h>中定義符號_POSIX_THREAD_SAFE_FUNCTIONS,對POSIX.1中的一些非線程安全函數,
它會提供可替代的線程安全版本。下面列出這些函數的線程安全版本。
clipboard.png

POSIX.1還提供了以線程安全的方式管理FILE對象的方法。可使用flockfile和ftrylockfile獲取給定FILE對象關聯的鎖。
這個鎖是遞歸的:當你佔有這把鎖的時候,仍是能夠再次獲取該鎖,並且不會致使死鎖。

int ftrylockfile(FILE *fp);
void flockfile(FILE *fp);
void funlockfile(FILE *fp);

12.6 線程特定數據(TSD)

使用線程特定數據理由:

  • 須要基於線程來維護一些數據(好比,線程id)

  • 但願可以提供一種機制能夠在多線程的環境下使用基於進程的接口。(好比,errno)

int pthread_key_create(pthread_key_t *keyp, void (*destructor)(void *));
void* pthread_getspecific(pthread_key_t key); 
int pthread_setspecific(pthread_key_t key, const void *value);
int pthread_key_delete(pthread_key_t *key);

注意:取消鍵與當前線程TSD的關聯關係(鍵與其餘線程TSD的關聯關係不變),但並不會激活與鍵關聯的析構函數,
   須要應用程序本身利用free函數

使用TSD過程:
1. 啓動一個進程並建立了若干線程(A,B),其中一個線程(好比線程A),要申請線程私有數據,系統調用
pthread_key_creat在下圖所示的key結構數組中找到第一個未用的元素,並把它的鍵(0)返回給調用者
2. 線程a經過pthrea_getspecific調用得到線程a的pkey[0]值,返回的是一個空指針NULL,
3. 用malloc在堆裏分配一段空間,使用pthread_setspecific調用將pkey[0]指向剛纔分配的內存區域。
4. 若線程b若使用相同的鍵,線程B只須要重複第三步。最終結果以下Figure 3所示。
clipboard.png

------------------------------------------[Figure 3]----------------------------------------

須要確保分配的鍵並非因爲在初始化階段的競爭而發生變化:

int pthread_once(pthread_once_t *initflag, void (*initfn)(void));
struct tsd { pthread_t tid; };

pthread_key_t key;
pthread_once_t create_done = PTHREAD_ONCE_INIT;

void destructor(void* arg){
    struct tsd* tsdptr = (struct tsd*)arg;
    free(tsdptr);
}

void key_create(void){
    pthread_key_create(&key, destructor);
}

void key_init(){
    struct tsd *tsdptr = (struct tsd*)malloc(sizeof(struct tsd));
    tsdptr->tid = pthread_self();
    pthread_setspecific(key, tsdptr);
}

void* p_fun(void* arg){
    pthread_once(&create_done, key_create);
    key_init();
    return (void*)0;
}

12.8 線程和信號

1. 每一個線程都有本身的信號屏蔽字,可是信號的處理是進程中全部線程共享的.
2. 進程中的信號是傳遞到單個線程的,線程中必須使用pthread_sigmask設置信號屏蔽字.
3. 若是一個信號與硬件故障相關,該信號通常被髮送到引發該事件的線程中,不然信號被髮送到任意一個線程

int pthread_sigmask(int how, const sigset_t *set, sigset_t *oldset);
int pthread_kill(pthread_t thread, int sig);

[pthread_xxx的函數通常返回錯誤碼,而不是設置errno]

注意,用sigprocmask修改的是進程(主線程)的信號屏障字,而不是本線程的信號屏蔽字

int sigwait(const sigset_t * restrict set, int * restrict signop);
signop爲當前接受到的信號[APUE 3 edition 中文版的解釋錯誤]

1. set爲等待的信號或信號集
2. 若信號集中的某些信號在sigwait調用時處於掛起狀態,那麼sigwait將無阻塞地返回,且移除這些掛起信號,若爲排隊信號,一次移除一個
3. 線程在調用sigwait以前,必須阻塞等待的信號(避免出現窗口期)。sigwait函數會原子地取消信號集的阻塞狀態,直到信號到來
4. 任意信號均可以喚醒調用sigwait的線程(和進程的sigsuspend很不一樣)
5. 使用sigwait的好處在於它能夠簡化信號處理,利用一個或多個線程專門處理信號,從而將異步產生的信號以同步方式處理
---5.1 假設專用線程A處理SIGINT,SIGQUIT信號, 在建立線程A以前,主線程首先屏蔽這兩個信號
---5.2 線程A建立以後,繼承主線程的信號屏蔽字,線程A調用sigwait函數
---5.3 sigwait取消對SIGINT與SIGQUIT的屏蔽,接着線程A阻塞,等待任意信號的到來
---5.4 當一個信號到來時,喚醒線程A,在線程A的正常的上下文中調用信號的處理程序
6. 若多個線程在sigwait的調用中由於同一個信號而阻塞,那麼在信號傳遞的時候,就只有一個線程從中返回
7. 若一個信號被捕獲(使用sigaction創建了一個信號處理程序),而一個線程正在sigwait調用中等待同一個信號,那麼不一樣操做系統的處理方式不一樣,因此儘可能避免

void* p_fun(void* arg){
    sigset_t sigs;
    sigemptyset(&sigs);
    sigaddset(&sigs, (int)arg);
    int sig;
    for(;;){
        if (sigwait(&sigs, &sig) != 0){
            fprintf(stderr, "sigwait err");
            return (void*)-1;
        }
        if (sig == SIGINT){
            printf("tid : %ld receive SIGINT.\n", pthread_self());
        }
        if (sig == SIGTSTP){
            printf("tid : %ld receive SIGTSTP.\n", pthread_self());
        }
    }
    return (void*)0;
}
int main(){
    sigset_t sigs;
    sigemptyset(&sigs);
    sigaddset(&sigs, SIGINT);
    sigaddset(&sigs, SIGTSTP);
    sigprocmask(SIG_SETMASK, &sigs, NULL);
    pthread_t tid[2];
    if (pthread_create(&tid[0], NULL, p_fun, (void*)SIGINT) != 0){
        fprintf(stderr, "pthread_create err");
        exit(-1);
    }
    if (pthread_create(&tid[1], NULL, p_fun, (void*)SIGTSTP) != 0){
        fprintf(stderr, "pthread_create err");
        exit(-1);
    }
    for(;;){
        /*main proccess*/
    }
    exit(0);
}
int pthread_kill(pthread_t tid, int signo);

能夠傳一個0值來檢查線程tid是否存在,相似kill檢查進程的存在性
鬧鐘定時器是進程資源,全部線程共享相同的鬧鐘

12.9 線程和fork

在子進程內部,只存在一個線程,以下圖所示,子進程中只有線程T3,由於在父進程中由線程T3建立了子進程。
因此互斥量M1,M2在子進程中就沒法解鎖。
``

clipboard.png

不一致解決方法:

  • 1. 若是子進程從fork返回之後立刻調用其中一個exec函數,能夠避免這種不一致,在fork返回和子進程調用其中一個exec函數之間,
    子進程只能調用異步信號安全的函數

  • 2. 調用pthread_atfork函數創建fork處理程序

int pthread_atfork()(void (*prepare)(void),void (*parent)(void),void (*child)(void));

------2.1 prepare由父進程在fork建立子進程前調用,prepare的任務是獲取父進程定義的全部鎖
------2.2 parent是在fork建立了子進程之後,在fork返回以前在父進程環境中調用的,parent的任務是對prepare得到的鎖進行解鎖
------2.3 child在fork返回以前在子進程環境中調用,與parent同樣,child必須釋放prepare處理程序得到的全部鎖
------2.4 能夠屢次調用pthread_atfork從而設置多套fork處理程序。parent和child是以他們註冊時的順序進行調用,
----------prepare的調用順序與他們註冊時的順序相反,好比若模塊A調用模塊B,每一個模塊都有本身的一套鎖。
----------那麼模塊B必須在模塊A以前設置它的pthread_atfork函數

相關文章
相關標籤/搜索