目錄
- 1、線程是什么?通過一個圖來理解……
- 2、Linux進程和線程?
- 2.1、之間的關系和區別
- 2.2、線程的優缺點?
- 3、線程的創建
- 3.1、POSIX線程庫
- 3.2、創建線程
- 3.3、PS查看運行的線程
- 4、線程的終止
- 5、線程的等待
- 6、線程分離
- 7、線程封裝
1、線程是什么?通過一個圖來理解……
首先,我們知道進程等于PCB+自己的數據和代碼,在創建進程的時候,操作系統要對進程進行描述,則創建一個結構體,里面包含進程的所有屬性信息(標志符、狀態信息、優先級等……),該結構體在Linux中稱為“PCB”,即進程控制塊,在Litnux中結構體名叫task_strcut
。該信息會存儲在程序地址空間中,也就是所說的虛擬地址空間。里面就存儲了該進程的所有信息和自己的代碼和數據信息。它們通過頁表進行映射到物理內存空間,也就是加載內存,進行CPU的調度執行。
上面描述的整個流程,從進程的創建(PCB的創建)、程序的代碼和數據通過頁表進行的映射關系的建立、到CPU的調度執行,該流程就可以看做是一個執行路線,只不過只有一個執行流,我么把該執行線路就稱之為線程
。
即線程就是一個進程中的一天控制序列。并且一個進程中至少有一個執行線程。
之前描述的PCB
只有一個,此處就稱為該進程有一個執行線程,但在一個進程中可以存在多個執行流,所有執行流會公用一個虛擬內存空間,在虛擬內存中,操作系統會將資源合理分配給多個執行流。
2、Linux進程和線程?
2.1、之間的關系和區別
- 進程:是系統資源分配的基本單位。
- 線程:是CPU調度執行的基本單位。
- 線程共享進程數據的同時,線程也有自己的一部分數據信息,用來描述不同線程。(線程ID、調度優先級、信號屏蔽字等……)
- 進程中的多線程共享:即在一個進程中,有一個程序地址空間,該空間被所有線程共享,因此Text Segment、Data Segment都是共享的,因此只要定義一個函數后,所有線程都是可以調用的,同時定義了一個全局變量后也是共享的,此外多線程之間還共享了文件描述符表、當前工作目錄、用戶id和組id等……
- 線程是進程的執行分支,只要一個線程出現異常情況,也就影響到整個進程,從而導致整個進程的崩潰,進而終止退出。
2.2、線程的優缺點?
- 線程的優點:
從創建的角度看:創建一個新線程比創建一個新進程的代價小的多,因為從上面的關系和區別可以看出,創建一個線程不需要從新分配虛擬地空間,沒有數據的大量拷貝,而創建進程需要創建新的程序地址空間,同時還要拷貝原始數據,增加的系統的消耗。
從切換的角度看:與進程間切換相比,線程間切換需要操作的工作量更小,主要區別就是線程之間的切換,虛擬地址空間是相同的。
從執行效率上看:在多線程的情況下可以實現并發執行,提高執行的效率。
- 線程的缺點:
主要是性能上,一個處理器上有密集型線程的數量進行執行的時候,會有較大的性能損失,會增加額外的同步和調度開銷。但是,合理的使?多線程,能提?CPU密集型程序的執?效率。
健壯性上:在多線程的情況下,由于虛擬地址空間是共享的,會造成一些空間的數據在同一時刻被多個線程訪問,即缺少資源的保護,因此在后面會提到互斥與信號量,用來應對多線程的情況下共享資源的多次訪問。
3、線程的創建
3.1、POSIX線程庫
- 在Linux中,創建線程就會用到里面的庫,需要包含頭文件<pthread.h>。
- 同時在鏈接這些線程函數庫的時候,編譯時需要加
-lpthread
選項。
3.2、創建線程
- 函數接口是:
pthread_create()
- 在代碼中的使用:
#include<iostream>
#include<pthread.h>
#include<string.h>
#include<unistd.h>void *work(void *arg)
{while(1){std::cout<<"我是線程-1: "<<pthread_self()<<"……"<<std::endl;sleep(1);}
}int main()
{pthread_t tid;int ret=pthread_create(&tid,nullptr,work,nullptr);if(ret!=0){std::cout<<"create failed!,code_num:"<<strerror(ret)<<std::endl;exit(-1);}while(1){std::cout<<"我是主線程: "<<pthread_self()<<"……"<<std::endl;sleep(1);}return 0;
}
- 上面的代碼就是在主線程中創建一個新的線程,總共兩個線程,因此運行后有兩個執行流,整個程序稱為一個進程,每個線程有自己的唯一表示符號,上面使?是是
pthread_self()
,得到的這個數實際上是?個地址,在虛擬地址空間上的?個地址,通過這個地址, 可以找到關于這個線程的基本信息,包括線程ID,線程棧,寄存器等屬性。即線程id。我們通過打印看到確實有兩個同時輸出在顯示器上,下面通過ps
查看是不是真有兩個線程在運行。
3.3、PS查看運行的線程
while :; do ps -aL | head -1 && ps -aL | grep 可執行程序名 | grep -v grep ; echo "************" ; sleep 1 ; done
循環監控查看線程情況。
通過布局監控,運行程序可以看到,確實有兩個線程,它們的PID都是一樣的,說明這兩個線程擁有同一個父進程,看到LWP
,其中一個和PID
相同,說明該線程就是主線程
,另一個就是創建的新線程。LWP
是什么呢?LWP
得到的是真正的線程ID
。
注意:
主線程的棧在虛擬地址空間的棧上,?其他線程的棧在是在共享區(堆棧之間),因為pthread
系列函數都是pthread
庫提供給我們的。?pthread
庫是在共享區的。所以除了主線程之外的其他線程的棧都在共享區。
4、線程的終止
只終止其中的某個線程,而不是終止整個進程,有一下三種方法:
- 在創建的線程函數中調用
return
,注意不要在主線程中調用return
,在主線程中調用retrun
,就相當于調用exit
。
void *work(void *arg)
{int num=5;while(num--){std::cout<<"我是線程-1: "<<pthread_self()<<"……"<<std::endl;sleep(1);}return nullptr;
}
- 直接調用退出接口
pthread_exit()
終止線程。
value_ptr
:value_ptr不要指向?個局部變量
- 調用
pthread_cancel()
取消一個在執行的線程
參數就是傳入線程id。成功返回0。
5、線程的等待
-
為什么要等待?
因為退出的線程,其空間沒有被釋放,依舊在進程地址空間中的。若創建新的線程并不會使用剛退出的線程的地址空間,因此就造成了浪費。 -
等待的函數接口:
pthread_join()
thread
:線程ID
value_ptr
:它指向?個指針,后者指向線程的返回值,返回值會根據調用不同的終止接口返回不同的值。
- 通過代碼演示:創建三個線程,調用不同的終止接口,看等待的返回接收參數值是什么……
#include <iostream>
#include <pthread.h>
#include <string.h>
#include <unistd.h>void *thread1(void *arg)
{std::cout << "thread1 running……" << std::endl;int *p = (int *)malloc(sizeof(int));*p = 1;return (void *)p;
}void *thread2(void *arg)
{std::cout << "thread2 running……" << std::endl;int *p = (int *)malloc(sizeof(int));*p = 2;pthread_exit((void *)p);
}void *thread3(void *arg)
{while (1){std::cout << "thread3 running……" << std::endl;sleep(1);}return nullptr;
}int main()
{pthread_t tid;void *ret;if (pthread_create(&tid, nullptr, thread1, nullptr) != 0){std::cout << "創建失敗……" << std::endl;exit(-1);}pthread_join(tid, &ret);printf("thread1 退出,thread1 id=%X,return code:%d\n", tid, *(int *)ret);free(ret);if (pthread_create(&tid, nullptr, thread2, nullptr) != 0){std::cout << "創建失敗……" << std::endl;exit(-1);}pthread_join(tid, &ret);printf("thread2 退出,thread2 id=%X,return code:%d\n", tid, *(int *)ret);free(ret);if (pthread_create(&tid, nullptr, thread3, nullptr) != 0){std::cout << "創建失敗……" << std::endl;exit(-1);}sleep(3);pthread_cancel(tid);pthread_join(tid, &ret);if (ret == PTHREAD_CANCELED)printf("thread return, thread id=%X, return code:PTHREAD_CANCELED\n",tid);elseprintf("thread return, thread id=%X, return code:NULL\n", tid);while(1){std::cout<<"我是主線程……"<<std::endl;sleep(1);}return 0;
}
- 如果thread線程通過return返回,value_ ptr所指向的單元?存放的是thread線程函數的返回值。
- 如果thread線程被別的線程調?pthread_ cancel異常終掉,value_ ptr所指向的單元?存放的是常數PTHREAD_ CANCELED。
- 如果thread線程是??調?pthread_exit終?的,value_ptr所指向的單元存放的是傳給pthread_exit的參數。
- 如果對thread線程的終?狀態不感興趣,可以傳NULL給value_ ptr參數。
6、線程分離
上面看到線程的等待,線程退出后需要對線程進行等待,保證資源的釋放,避免系統資源的泄露。可以看到通過等待線程進行資源的釋放不是太方便,因此線程創建在默認情況下是joinable的,也就是可以線程分離的,指將一個線程從其創建者(通常是主線程)中分離出來,使其成為一個獨立的執行實體。分離后的線程在終止時會自動釋放其資源,而不需要其他線程顯式地等待或回收它。
主要特點
1、資源自動回收:分離的線程在結束時系統會自動回收其資源
2、無需join:其他線程不需要調用join()或類似函數來等待分離線程結束
3、獨立性:分離后的線程運行獨立于創建它的線程
線程分離的接口函數:pthread_detach(線程ID)
下面代碼描述:即通過線程分離技術,讓線程自己執行5次后自動分離,自動釋放資源,分離后依然顯示調用join等到,依舊會等待的,只不過不會執行后面的代碼,因為是阻塞式等待的。
#include <iostream>
#include <pthread.h>
#include <string.h>
#include <unistd.h>void* thread1(void* arg)
{int num=5;while(num--){std::cout<<(char*)arg<<std::endl;sleep(1);}pthread_detach(pthread_self());return nullptr;
}int main()
{pthread_t tid;if(pthread_create(&tid,nullptr,thread1,(void*)"thread1線程running……")!=0){std::cout<<"創建線程失敗……"<<std::endl;}sleep(1);if(pthread_join(tid,nullptr)==0){std::cout<<"等待成功……"<<std::endl;return 0;//不return 就會繼續執行后面的主線程}else{std::cout<<"等待失敗……"<<std::endl;return -1;}while(1){std::cout<<"主線程運行中……"<<std::endl;sleep(1);}return 0;
}
使用場景
1、后臺任務(如日志記錄、監控)
2、不需要與主線程同步的一次性任務
3、長時間運行的服務線程
注意事項
1、分離后無法再join該線程
2、分離線程不能返回結果給創建者線程
3、主線程退出可能導致分離線程被強制終止(取決于平臺和設置)
4、需要謹慎處理共享資源,因為缺乏同步機制
7、線程封裝
下面對創建線程步驟做一次封裝:
#pragma once#include <iostream>
#include <cstring>
#include <functional>
#include <pthread.h>namespace ThreadMoodule
{using work_t = std::function<void(std::string)>;static int NameId = 1;// 枚舉線程狀態enum STATUS{NEW,RUNNING,STOP};// 封裝線程class Thread{private:// 此處需要注意,寫為成員函數的時候,函數的第一個參數默認是this*,若下面線程函數就需要用static修改,或者寫在類外,由于封裝,因此加staticstatic void *Routine(void *args){Thread *td = static_cast<Thread *>(args);td->_task(td->_name);return 0;}void EnableDetach(){_joinable=false;}public:Thread(work_t task): _task(task), _status(STATUS::NEW), _joinable(true){_name = "thread_" + std::to_string(NameId++);}bool StartThread(){// 啟動線程if (_status != STATUS::RUNNING){// int n = pthread_create(&_tid, nullptr, Routine, nullptr);//第四個參數由于線程函數寫在類中,// 被static修飾無法使用this*指針,因此無法訪問成員變量,因此該參數傳入this,傳遞給函數。int n = pthread_create(&_tid, nullptr, Routine, this);if (n != 0){return false; // 創建失敗}_status = STATUS::RUNNING;return true;}return false;}bool StopThread() // 即取消{if (_status == STATUS::RUNNING){int n = pthread_cancel(_tid);if (n != 0){return false;}_status = STATUS::STOP;return true;}return false;}bool JoinThread(){if (_joinable){int n = pthread_join(_tid, nullptr);if (n != 0){return false;}_status = STATUS::STOP;return true;}return false;}void DetachThread(){//前提是沒有分離的EnableDetach();pthread_detach(_tid);}std::string Name(){return _name;}bool IsJoinAble(){return _joinable;}~Thread(){}private:std::string _name;pthread_t _tid;pid_t _pid; // 多線程,所有pid都是一樣的bool _joinable; // 默認是不可分離的work_t _task;STATUS _status;};
}
#include "Thread.hpp"
#include <unistd.h>
#include <vector>#define THREAD_NUM 5int main()
{std::vector<ThreadMoodule::Thread> threads;for (int i = 0; i < THREAD_NUM; i++){ThreadMoodule::Thread t([](std::string name){while(true){std::cout<<name<<"執行任務……"<<std::endl;sleep(1);} });threads.emplace_back(t);}for(auto& n:threads){n.StartThread();}sleep(1);for(auto& e:threads){e.StopThread();}sleep(1);for(auto& e:threads){e.JoinThread();}return 0;
}