【Linux】線程基本知識概述

本文內容概述:
1.線程的基本概念(包括線程的定義,線程之間的共享資源和私有資源);
2.基本函數(包括線程的創建,終止,等待,可分離和可切換,當然其中還會涉及互斥鎖方面的內容等等)。

1.線程的基本概念:
   在前邊的學習中,我們知道,進程是在各自獨立的地址空間中運行,如果需要共享資源,則要進行進程間的通信,比如管道,消息隊列,信號量,共享內存這些 ,所以完成通信是比較困難的。而線程是進程內的一個執行分支,顧名思義,線程是在進程內部執行,佔用着進程的地址空間,所以,線程之間的通信是比較容易的---定義一個公共的buf,然後就能直接進行通信。
   在Linux系統下,是沒有線程 這個概念的,都是用進程去模擬線程。
   我們看到的進程有可能是線程,所以,我們將線程稱爲輕量級進程。
由於線程是進程內部的執行流,所以線程就會共享一些進程的信息,當然也會有自己私有的信息:
線程共享進程的以下資源:
   1)文件描述符信息;
   2)每種信號的處理方式(由於線程是進程內部的執行流,所以每種信號的處理方式也是共享的);
   3)當前工作目錄;
   4)各種id信息,比如用戶id和組id信息。
線程也會有自己私有的信息:
   1)線程id;
   2)線程的上下文信息(每個線程都必須有自己的上下文信息,存儲在自己的PCB中,當然有些操作系統中會     有一種存儲線程信息的TCB結構體)
   3)棧空間
   4)調度優先級
   5)errno變量
      在Linux下的C api函數發生異常時,一般會將errno變量(在errno.h文件中)賦予一個整形值,不同的值代表不同的含義,可以通過輸出的值來查看出錯的信息。
比如:printf("%d",errno);就可以輸出異常時的錯誤碼
    6)信號屏蔽字信息,這裏的 信號屏蔽字就有點類似於文件的權限信息中的umask。
2.基本函數:
(1)線程的創建

(2)線程的終止:
    線程的終止方法是有3種:
     a.直接在線程函數中return,但是在main函數中return,是表示整個進程的終止;
     b.採用pthread_cancel函數讓同一進程中的其他的線程去終止;

程序舉例: 
#include<stdio.h>
#include<pthread.h>
#include<unistd.h>
void* pthreadRun(void* arg)
{
    int count = 3;
    while(count--)
    {
        printf("new pthread,tid:%lu,pid:%d\n",pthread_self(),getpid());
    }
}
int main()
{
    pthread_t tid = 0;
    int ret = pthread_create(&tid,NULL,pthreadRun,NULL);
    if(ret != 0)
    {
        perror("pthread_create");
        return -1;
    }
    if(ret != 0)
    {
        perror("pthread_detach");
        return -1;
    }
    sleep(1);
   if(pthread_cancel(tid)!= 0)
    {
        printf("cancel failed\n");
        return 1;
    }

    int exitCode;
    pthread_join(tid,(void**)&exitCode);
    printf("new pthread exit code:%d\n",exitCode);
    return 0;
}

這段程序的運行結果是:


解釋: 由於在主線程中已經sleep了1秒之後,才終止新線程的,由於在1秒之內,新線程早已運行完成,所以,就會終止失敗。

如果將終止新線程之前的sleep去掉,程序的運行結果究竟是什麼呢?

解釋:新線程沒有執行完成,就被主線程終止了,此時是終止成功的,所以退出碼就是-1.

c.使用pthread_exit()自己終止自己。


(3)線程的等待:


(4)線程的可分離性:
    一個線程是可結合的或者是可分離的。一個可結合的線程可以被與之在同一個進程中的其他線程所終止 ;而一個可分離的線程不需要主線程的等待,它的存儲器等資源會在它運行結束時被操作系統回收。我們都知道,線程是進程內部的一個執行分支,爲什麼這麼說?因爲進程內的線程共享一個地址空間,所以在被分離的線程沒有執行完成時,主線程不要退出,否則新的線程無法繼續執行。主線程不可以等待被分離的線程。

程序舉例:
#include<stdio.h>
#include<pthread.h>
#include<unistd.h>
void* pthreadRun(void* arg)
{
    int count = 3;
    while(count--)
    {
        printf("new pthread,tid:%lu,pid:%d\n",pthread_self(),getpid());
        sleep(1);
    }
}
int main()
{
    pthread_t tid = 0;
    int ret = pthread_create(&tid,NULL,pthreadRun,NULL);
    if(ret != 0)
    {
        perror("pthread_create");
        return -1;
    }
    usleep(1000);
    ret = pthread_detach(tid);
    if(ret != 0)
    {
        perror("pthread_detach");
        return -1;
    }
     if(pthread_cancel(tid)!= 0)
    {
        printf("cancel failed\n");
        return 1;
    }

    int exitCode;
    if(0 == pthread_join(tid,(void*)&exitCode))
    {
        printf("wait success.\n");
        printf("new pthread exit code:%d\n",exitCode);
    }
    else
    {
        printf("wait failed\n");
    }
    return 0;
}
程序運行結果:


解釋:從運行結果我們可以看出,我們不可以等待已經被分離的線程,但是可以終止已經分離的線程。

(5)線程的切換:
線程切換的條件:
   a.一個時間片的完成;
   b.操作系統模式的切換(內核態向用戶態的切換)。
時間片的結束,會進行線程的切換,這個是比較容易理解的。
爲什麼操作系統模式的切換就會進行線程的切換?因爲線程(在linux系統下,線程也被認爲是進程,也有自己的PCB)的PCB是在內核中,只有內核態(也就是系統)纔可以進行查看線程的信息,即就是訪問PCB。我們知道,系統調用都是在內核態進行完成的,printf函數的底層調用的就是系統調用,所以我們採用printf函數進行顯示數據的時候,就可能完成線程的切換。
所以,寫一個程序,製造線程的切換。
#include<stdio.h>
#include<pthread.h>
#include<unistd.h>
static int count = 0;
void* pthreadRun(void* arg)
{
    int val = 0;
    int i = 0;
    while(i < 5000)
    {
        val = count;
        printf("new pthread,tid:%lu,pid:%d,count:%d\n",pthread_self(),getpid(),count);
        count = val + 1;
        i++;
    }
    return NULL;
}
int main()
{
    pthread_t tid1 = 0;
    pthread_t tid2 = 0;
    int ret = pthread_create(&tid1,NULL,pthreadRun,NULL);
    if(ret != 0)
    {
        perror("pthread_create");
    return -1;
    }
    ret = pthread_create(&tid2,NULL,pthreadRun,NULL);
    if(ret != 0)
    {
        perror("pthread_create");
        return -1;
    }
    pthread_join(tid1,NULL);
    pthread_join(tid2,NULL);
    printf("count:%d\n",count);
    return 0;
}

運行結果介於5000到10000之間不等。
如果我們給臨界資源加鎖,運行完代碼之後解鎖。代碼如下:
#include<stdio.h>
#include<pthread.h>
#include<unistd.h>
static int count = 0;
pthread_mutex_t myLock = PTHREAD_MUTEX_INITIALIZER;
void* pthreadRun(void* arg)
{
    int val = 0;
    int i = 0;
    while(i < 3000)
    {
       pthread_mutex_lock(&myLock);
        val = count;
        printf("new pthread,tid:%lu,pid:%d,count:%d\n",pthread_self(),getpid(),count);
        count = val + 1;
        i++;
        pthread_mutex_unlock(&myLock);
    }
    return NULL;
}
int main()
{
    pthread_t tid1 = 0;
    pthread_t tid2 = 0;
    int ret = pthread_create(&tid1,NULL,pthreadRun,NULL);
    if(ret != 0)
    {
        perror("pthread_create");
    return -1;
    }
    ret = pthread_create(&tid2,NULL,pthreadRun,NULL);
    if(ret != 0)
    {
        perror("pthread_create");
        return -1;
    }
    pthread_join(tid1,NULL);
    pthread_join(tid2,NULL);
    pthread_mutex_destroy(&myLock);
    printf("count:%d\n",count);
    return 0;
}

這樣對臨界資源進行加鎖之後,就可以保證全局變量最終可以加至6000.
關於互斥鎖的函數的聲明:
1)初始化和銷燬函數

2)申請和釋放鎖資源函數


爲什麼加鎖就可以防止線程之間相互干擾?
   那是因爲加鎖就可以保證鎖之間的代碼是一個原子操作(要麼執行就能執行完,要麼不執行,也就是非0即1的狀態)。
那麼lock()和unlock()函數是如何實現的呢?
方法一:

解釋:我們知道mutex = 0;這一步並不是原子操作,如果兩個線程同時調用lock函數,都判斷出mutex > 0,然後其中一個線程將mutex置爲0,另一個線程並不知道此事,也將mutex置爲0,於是兩個線程都以爲自己獲得了鎖資源,這樣就發生了衝突。所以這種辦法是不可行的。
執行賦值操作需要幾步呢?
步驟一:將mutex的數據從內存讀到寄存器;
步驟二:通過CPU內的運算器將mutex進行賦值;
步驟三:將mutex的值重新寫回內存。
方法二:


解釋:由於方法一中的賦值操作不是原子操作,導致上述方法不可取。爲了解決這個問題,大多體系結構提供了swap或者exchange命令,將寄存器和內存裏的值進行交換,就保證了原子操作。
lock函數 語句解釋:
將al寄存器中的值放置爲0;
將al寄存器和內存中的mutex的值進行交換;
......
如果執行了xchgb指令之後,又有一個線程申請鎖資源,此時它看到的mutex就是0,表示不可以申請到資源;
如果執行了movb指令之後,又有一個線程申請鎖資源,此時哪個線程獲得鎖資源,就看自己的優先級或者權限等等問題了。
所以,這個方法就是lock函數和unlock函數裏執行的操作了。
關於線程的基本知識就先整理到這裏~~


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章