一 線程的操作
1. 創建線程:pthread_create
int pthread_create(pthread_t *thread, // 線程 idconst pthread_attr_t *attr, // 線程屬性設置void *(*start_routine) (void *), // 回調函數void *arg // 傳遞給回調函數的參數);
// 返回值0成功,否則返回錯誤碼
2. 等待線程:pthread_join
int pthread_join(pthread_t thread, // pthread_create 的返回值 void **retval // pthread_create 回調函數的返回值);
// 返回值成功0,否則返回錯誤碼
和進程一樣,線程執行完,也需要等待回收獲取執行結果,否則類似僵尸進程。
示例:
#include <iostream>
#include <pthread.h>void* fun(void *arg)
{const char *s = static_cast<const char *>(arg);std::cout << s << std::endl;return (void *)"正常退出";
}
int main()
{pthread_t pid;pthread_create(&pid, nullptr, fun, (void *)"hello world");void *result;pthread_join(pid, &result);std::cout << static_cast<const char *>(result) << std::endl;return 0;
}
創建多線程,進程內部就有多個執行流,誰先執行不一定。
void* 可以接收任意類型參數,內置類型,自定義類型都可以。
二級指針存放回調函數的返回值(一級指針的地址)
3. 線程終止
#include <pthread.h>void pthread_exit(void *retval // 終止后的信息);
// 哪個線程調用終止哪個線程
如果不想正常return返回,可以調用 pthread_exit() ,提前終止,攜帶退出信息。
4. 線程分離
#include <pthread.h>int pthread_detach(pthread_t thread // 線程的PID);
// 讓這個線程分離,此后不需要join
一般情況線程結束需要被等待,但可以自己分離出進程,但資源仍然共享,只是由系統來回收。
5. 取消線程
int pthread_cancel(pthread_t thread // 線程ID)
// 取消線程
可以主動取消一個線程,一般是由主線程來取消。
6. 封裝原生API(簡易)
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <functional>// 處理任務
using handler = std::function<void(void)>;class mythread
{
public:// 線程回調函數static void *fun(void *arg){mythread *mythis = static_cast<mythread *>(arg);// 處理任務mythis->_handler();return nullptr;}mythread(const std::string &name, handler ha) : _name(name), _handler(ha), _isdetach(false) {}~mythread() {}// 創建線程初始化tidbool start(){if (pthread_create(&_tid, nullptr, fun, this) != 0)return false;return true;}// 等待線程結束bool join(){if (_isdetach == true)return false;return pthread_join(_tid, nullptr) == 0;}// 分離線程bool setthread_detach(){_isdetach = true;return pthread_detach(_tid) == 0;}// 取消線程bool setthread_cancel(){return pthread_cancel(_tid) == 0;}pthread_t gettid() { return _tid; }private:std::string _name; // 線程名pthread_t _tid; // 線程tidhandler _handler; // 執行任務bool _isdetach; // 分離線程
};
二 用戶級線程
前章說過,Linux中的線程是通過pthread庫對輕量級進程的封裝,使用前必須攜帶 -lpthread 鏈接這個庫。那么用戶級線程是如何封裝的?用戶級線程包含哪些屬性?下面來看看
既然是庫,和標準庫,第三方庫一樣,也要映射到共享區,pthread庫也不例外。
當調用pthread_create(),pthread庫會在內部維護一個用戶級線程結構,并和其他相同的結構組織起來。
當獲取線程的TID的時候,也就是pthread_create第一個參數,就是用戶級線程維護的線程的起始地址,不是內核輕量級進程的LWP字段,所以在線程操作的時候,實際是在對pthread庫操作,pthread庫封裝的輕量級進程,對庫做操作,庫幫你對輕量級進程做操作。
pthread維護的結構,包含很多字段:PID/LWP,回調函數/函數參數,void*退出信息,獨立的棧,TLS線程局部存儲.....等。
線程局部存儲:線程也可以給自己創建獨立的對象,維護在pthread結構中,但僅只支持內置類型,不支持自定義類型: __thread 類型。
獨立的棧:進程里的棧由主線程使用,即沒有創建線程的那個線程使用,其他的線程也是在pthread結構里獨立開辟空間并維護自己的棧,也就不會起沖突了。
三 同步與互斥
如果在多執行流對同一個變量進行操作會有什么問題?
假設對一個變量執行 -- 操作,當減到0結束,那么if()判斷和變量--都是操作,假設變量當前值為1,此時有多個執行流同時執行if(),if里的條件在內存中,先從內存拿到CPU,在由CPU執行,已經是2步操作了,當某個執行流對變量進行--操作,此時因某種原因被切換,比如:時間片到了,后面有優先級更高的....等,其他判斷if()條件的執行流判斷完成,此時if()內的語句已經有多條執行流了,但變量當前值為1,切走的線程又回來了進行--,后面的線程已經進來了,也會--,所以會有數據不一致問題,由并發訪問導致數據不一致問題,稱為線程安全問題。
上述根本原因是if()是由多種操作和切換等方面導致多執行流執行中有中間狀態,稱為非原子操作。
1. 概念:
原子性:執行流執行一段代碼沒有中間過程稱為原子的,這段代碼可能形成一行匯編語句,也可能是多行,只要沒有中間狀態,也就是原子的。
共享資源/臨界資源:多執行流共享一個對象,對象身為共享資源,對共享資源保護的資源,稱為臨界資源。
臨界區:臨界資源的代碼,稱為臨界區,其他稱為非臨界區。
2. 互斥量/互斥鎖:
為了保證在多線程情況下,因為并發允許導致對共享資源修改造成的數據不一致問題,提供了很多互斥機制。
互斥:訪問臨界區的代碼的時候,只有一個執行流能訪問,其他等待,所以相對于其他線程,進入臨界區的線程是原子的,由原來的并發,在臨界區中變成了串行訪問。
API:
#include <pthread.h>int pthread_mutex_init(pthread_mutex_t *mutex, // 鎖 const pthread_mutexattr_t *mutexattr // 鎖的屬性);int pthread_mutex_trylock(pthread_mutex_t *mutex // 申請鎖失敗返回);int pthread_mutex_destroy(pthread_mutex_t *mutex // 釋放鎖);int pthread_mutex_lock(pthread_mutex_t *mutex // 對鎖進行加鎖);int pthread_mutex_unlock(pthread_mutex_t *mutex // 對鎖進行解鎖);
如果鎖是局部變量,則需要進行初始化和手動銷毀。
全局對象則直接初始化,不用銷毀:pthread_mutex_t mut = PTHREAD_MUTEX_INITIALIZER。
互斥鎖特征:如果申請成功就往下執行,否則阻塞,等待后續鎖被解鎖被喚醒重新申請鎖。
所以互斥鎖加鎖期間就是原子的。
互斥鎖實現:
當對鎖進行加鎖的時候,CPU存在一個匯編指令:swap,exchange,作用是交換內存中的值和寄存器中的值,整個下來只有一行匯編語句,也就是原子的,而之前的++/--操作而是3條匯編指令。
首先,CPU會初始化寄存器里的值為0,對應上圖第一行代碼,執行完這時如果被切走。
第一:內存中的值沒變,其他線程可以繼續申請,如果申請到,內存中的值變了,切走的線程再回來,恢復自己的寄存器的里的值:0,交換內存中的值,此時假設內存中原來的值為1,因切走被其他線程交換,變為0,此時0和0交換,在進行if()判斷,走else 阻塞等待。
第二:執行第二行代碼被切走,此時內存中的值由初始1被交換到寄存器,變為0,線程切換保存寄存器的值,后續線程的寄存器初始化為0,和內存中被交換后的值:0,0和0交換,if()不成立,else阻塞。
第三:解鎖重新交換內存中的值和寄存器的值,如果被切走,其他線程已經阻塞,新的線程可以申請,沒被切走喚醒阻塞的線程繼續申請鎖。
示例:
#include "thread.hpp"
#include <vector>// pthread_mutex_t mymtu = PTHREAD_MUTEX_INITIALIZER;
int val = 10000;
void xx(std::string s)
{while (1){// pthread_mutex_lock(&mymtu);if (val > 0){std::cout << s << " :" << val-- << std::endl;// pthread_mutex_unlock(&mymtu);}else{// pthread_mutex_unlock(&mymtu);break;}}
}
int main()
{std::vector<mythread> v;for (int i = 0; i < 5; i++)v.emplace_back(std::to_string(i), xx);for (auto &e : v)e.start();for (auto &e : v)e.join();return 0;
}
因顯示器本就是共享資源所以打印信息混亂正常,可以看到3號線程打印的val是負數,不加保護必定存在線程安全問題,所以要加鎖進行保護。
3 同步/條件變量:
上面說的互斥只是讓臨界區只有一個線程可以訪問。
同步:多個線程訪問臨界區有一定的順序。
明顯互斥也有順序,但解鎖的線程和阻塞的線程狀態不一樣,解鎖的線程解鎖完可以立即去申請鎖,而阻塞的線程要先喚醒再去申請鎖,這樣就導致了一個線程解鎖之后又能申請到鎖,而后面的鎖一直申請不到,導致的問題就是后面的線程干等,線程饑餓問題,也就是同步,但資源競爭不合理。
所以為了讓資源競爭合理,又引入了一個鎖,條件變量。
API:
// 全局不需要釋放
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;// 初始化條件變量
int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr);// 喚醒一個線程
int pthread_cond_signal(pthread_cond_t *cond);// 喚醒全部線程
int pthread_cond_broadcast(pthread_cond_t *cond);// 線程掛到條件變量中
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);// 釋放條件變量
int pthread_cond_destroy(pthread_cond_t *cond);
使用和互斥鎖差不多,條件變量作用就是讓線程競爭資源具有有合理性,如果資源不就緒就掛到條件變量里(隊列里),喚醒依次從隊列頭部取一個,也就保證了每個線程能合理競爭到資源。
當調用pthread_cond_wait()的時候,會釋放互斥鎖,當喚醒的時候,會彈出一個線程,并重新申請鎖。
示例:
#include <iostream>
#include <pthread.h>
#include <unistd.h>pthread_mutex_t mymtu=PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t mycond=PTHREAD_COND_INITIALIZER;int val=0;
void* fun(void* arg)
{while(1){pthread_mutex_lock(&mymtu);pthread_cond_wait(&mycond,&mymtu);std::cout<<static_cast<const char*>(arg)<<std::endl;pthread_mutex_unlock(&mymtu);}return nullptr;
}
int main()
{pthread_t t1,t2,t3;pthread_create(&t1,nullptr,fun,(void*)"thread-1");pthread_create(&t2,nullptr,fun,(void*)"thread-2");pthread_create(&t3,nullptr,fun,(void*)"thread-3");while(1){std::cout<<"wake up"<<std::endl;pthread_cond_signal(&mycond);// pthread_cond_broadcast(&mycond);sleep(1);}pthread_join(t1,nullptr);pthread_join(t2,nullptr);pthread_join(t3,nullptr);return 0;
}
當加入條件變量控制線程資源的競爭,明顯具有一定的順序性,也就是同步。