線程總結
- 1 線程的實現
- 線程創建
- 線程退出
- 線程等待
- 線程清理
- 2 線程的屬性
- 線程的分離
- 線程的棧地址
- 線程棧大小
- 線程的調度策略
- 線程優先級
- 3 線程的同步
- 互斥鎖
- 讀寫鎖
- 條件變量
- 信號量
線程是系統獨立調度和分配的基本單位。同一進程中的多個線程將共享該進程中的全部系統資源,例如文件描述符和信號處理等。一個進程可以有很多線程,每個線程并發執行不同的任務。
1 線程的實現
線程創建
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg);
- thread:指向線程標識符的指針,當線程創建成功后,用來返回創建的線程ID
- att:指定線程的屬性,NULL表示默認
- start_routine:函數指針,指向線程創建后要調用的函數,直接賦值函數名即可
- arg是傳給函數的參數
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>void * my_pthread(void *arg)
{printf("pthread id =%ld\n",pthread_self());return NULL;
}int main(void)
{pthread_t pid;if(pthread_create(&pid,NULL,my_pthread,NULL)){printf("pthread_create error\n");exit(1);}sleep(2);return 0;
}
線程退出
void pthread_exit(void *retval);
- retval是線程結束時的返回值,可由其他函數如pthread_join來獲取
注意:如果進程中任何一個線程調用exit()或_exit()函數,那么整個進程就會終止。線程的正常退出方法有線程從線程函數中返回(return)、線程可以被另一個線程終止以及線程自己調用pthread_exit()
線程等待
在調用pthread_create函數后,就會運行相關的線程函數。pthread_join()是一個線程阻塞函數,調用后,調用者一直等待指定的線程結束才返回,被等待線程的資源就會被回收。
int pthread_join(pthread_t thread, void **retval);
- pthread是等待結束的線程id
- retval是用戶定義的指針,用來存儲被等待線程結束時的返回值。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>void * my_pthread(void *arg)
{int ret =5;printf("pthread id =%ld\n",pthread_self());pthread_exit((void *)&ret);
}int main(void)
{pthread_t pid;void *ret ;if(pthread_create(&pid,NULL,my_pthread,NULL)){printf("pthread_create error\n");exit(1);}pthread_join(pid,&ret);printf("ret = %d\n",*(int * )ret);return 0;
}
線程函數運行結束是可以有返回值的,這個函數的返回值怎么返回呢?可以通過return語句進行返回,也可以通過pthread_exit()函數進行返回。函數的這個返回值怎么來接收呢?就通過pthread_join()函數來接受。
當然也可以選擇不接受該線程的返回值,只阻塞該線程:
pthread_join(tid, NULL);
線程清理
線程終止有兩種情況:正常終止和非正常終止。線程主動調用pthread_exit()或者從線程中return都使進程正常退出。非正常終止時線程在其他線程的干預下停止運行或者由于自身運行錯誤而退出。
線程可以安排它退出時需要調用的函數,這與進程在退出時可以用atexit函數安排退出函數是類似的。這樣的函數稱為線程清理處理程序。一個線程可以建立多個清理處理程序。
void pthread_cleanup_push(void (* rtn)(void *), void * arg);
函數說明:將清除函數壓入清除棧。rtn是清除函數,arg是清除函數的參數。
void pthread_cleanup_pop(int execute);
函數說明:將清除函數彈出清除棧。執行到pthread_cleanup_pop()時,參數execute決定是否在彈出清除函數的同時執行該函數,execute非0時,執行;execute為0時,不執行。
從pthread_cleanup_push的調用點到pthread_cleanip_pop之間的程序段中的終止動作(包括調用pthread_exit()和異常終止,不包括return)都將執行pthread_cleanup_push()所指定的清理函數。
int pthread_cancel(pthread_t thread);
函數說明:取消線程,該函數在其他線程中調用,用來強行殺死指定的線程。
2 線程的屬性
參考文章:
- https://blog.csdn.net/zsf8701/article/details/7842392
- https://blog.csdn.net/qq_22847457/article/details/89461222
- https://blog.csdn.net/yychuyu/article/details/84503261?spm=1001.2101.3001.6650.1&utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-1.pc_relevant_antiscanv2&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-1.pc_relevant_antiscanv2&utm_relevant_index=2
線程屬性標識符:pthread_attr_t 包含在 pthread.h 頭文件中。
typedef struct
{int detachstate; //線程的分離狀態int schedpolicy; //線程調度策略structsched_param schedparam; //線程的調度優先級int inheritsched; //線程的繼承性int scope; //線程的作用域size_t guardsize; //線程棧末尾的警戒緩沖區大小int stackaddr_set; //線程的棧設置void* stackaddr; //線程棧的位置size_t stacksize; //線程棧的大小
}pthread_attr_t;
屬性值不能直接設置,須使用相關函數進行操作,初始化的函數為pthread_attr_init,這個函數必須在pthread_create函數之前調用。之后須用pthread_attr_destroy函數來釋放資源。線程屬性主要包括如下屬性:作用域(scope)、棧尺寸(stack size)、棧地址(stack address)、優先級(priority)、分離的狀態(detached state)、調度策略和參數(scheduling policy and parameters)。默認的屬性為非綁定、非分離、缺省1M的堆棧、與父進程同樣級別的優先級。
線程的分離
分離狀態屬性確定使用線程屬性對象 attr 創建的線程是在可連接狀態還是可分離狀態下創建。
如果在創建線程時就知道不需要了解線程的終止狀態,就可以修改pthread_attr_t結構中的detachstate線程屬性,讓線程一開始就處于分離狀態。可以使用pthread_attr_setdetachstate函數把線程屬性detachstate設置成以下兩個合法值之一:
PTHREAD_CREATE_DETACHED
PTHREAD_CREATE_JOINABLE
/* 設置線程的分離狀態 */int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);/* 獲取線程的分離狀態 */ int pthread_attr_getdetachstate(const pthread_attr_t *attr, int *detachstate);
注意事項:如果設置一個線程為分離線程,而這個線程運行又非常快,它很可能在pthread_create函數返回之前就終止了,
它終止以后就可能將線程號和系統資源移交給其他的線程使用,這樣調用pthread_create的線程就得到了錯誤的線程號。
解決方法:要避免這種情況可以采取一定的同步措施,最簡單的方法之一是可以在被創建的線程里調用pthread_cond_timedwait函數,讓這個線程等待一會兒,留出足夠的時間讓函數pthread_create返回。設置一段等待時間,是在多線程編程里常用的方法。
線程的棧地址
POSIX.1定義了兩個常量_POSIX_THREAD_ATTR_STACKADDR 和_POSIX_THREAD_ATTR_STACKSIZE檢測系統是否支持棧屬性。也可以給sysconf函數傳遞_SC_THREAD_ATTR_STACKADDR或 _SC_THREAD_ATTR_STACKSIZE來進行檢測。
當進程棧地址空間不夠用時,指定新建線程使用由malloc分配的空間作為自己的棧空間。通過pthread_attr_setstack和pthread_attr_getstack兩個函數分別設置和獲取線程的棧地址。
int pthread_attr_setstack(pthread_attr_t *attr,void *stackaddr, size_t stacksize);int pthread_attr_getstack(const pthread_attr_t *attr,void **stackaddr, size_t *stacksize);
線程棧大小
當系統中有很多線程時,可能需要減小每個線程棧的默認大小,防止進程的地址空間不夠用。當線程調用的函數會分配很大的局部變量或者函數調用層次很深時,可能需要增大線程棧的默認大小。
函數pthread_attr_getstacksize和 pthread_attr_setstacksize可以設置或者獲取線程的棧大小。
int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize);int pthread_attr_getstacksize(const pthread_attr_t *attr, size_t *stacksize);
線程的調度策略
POSIX標準指定了三種調度策略:先入先出策略 (SCHED_FIFO)、循環策略 (SCHED_RR) 和自定義策略 (SCHED_OTHER)。SCHED_FIFO 是基于隊列的調度程序,對于每個優先級都會使用不同的隊列。SCHED_RR 與 FIFO 相似,不同的是前者的每個線程都有一個執行時間配額。SCHED_FIFO 和 SCHED_RR 是對 POSIX Realtime 的擴展。SCHED_OTHER 是缺省的調度策略。
- 新線程默認使用 SCHED_OTHER 調度策略。線程一旦開始運行,直到被搶占或者直到線程阻塞或停止為止。
- SCHED_FIFO
如果調用進程具有有效的用戶 ID ,則爭用范圍為系統 (PTHREAD_SCOPE_SYSTEM) 的先入先出線程屬于實時 (RT) 調度類。如果這些線程未被優先級更高的線程搶占,則會繼續處理該線程,直到該線程放棄或阻塞為止。對于具有進程爭用范圍 (PTHREAD_SCOPE_PROCESS)) 的線程或其調用進程沒有有效用戶 ID 的線程,請使用 SCHED_FIFO,SCHED_FIFO 基于 TS 調度類。 - SCHED_RR
如果調用進程具有有效的用戶 ID ,則爭用范圍為系統 (PTHREAD_SCOPE_SYSTEM)) 的循環線程屬于實時 (RT) 調度類。如果這些線程未被優先級更高的線程搶占,并且這些線程沒有放棄或阻塞,則在系統確定的時間段內將一直執行這些線程。對于具有進程爭用范圍 (PTHREAD_SCOPE_PROCESS) 的線程,請使用 SCHED_RR(基于 TS 調度類)。此外,這些線程的調用進程沒有有效的用戶 ID 。
int pthread_attr_setschedpolicy(pthread_attr_t *attr, int policy);int pthread_attr_getschedpolicy(const pthread_attr_t *attr, int *policy);
參數policy可以為SCHED_FIFO, SCHED_RR, and SCHED_OTHER
線程優先級
線程優先級存放在結構sched_param中,
struct sched_param {int sched_priority; /* Scheduling priority */};
可以通過pthread_attr_setschedparam()和pthread_attr_getschedparam設置和獲取線程的調度優先級 ,它的完整定義是:
int pthread_attr_setschedparam(pthread_attr_t *attr,const struct sched_param *param);int pthread_attr_getschedparam(const pthread_attr_t *attr,struct sched_param *param);
3 線程的同步
參考文章
https://blog.csdn.net/qq_43412060/article/details/106989170
https://www.cnblogs.com/wsw-seu/p/8036218.html
互斥鎖
互斥鎖用pthread_mutex_t數據類型表示,互斥鎖可以用來控制線程對共享資源的互斥訪問,確保同一時間只有一個線程訪問數據 。
/* 初始化互斥鎖 */
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr);
/* 銷毀互斥鎖 */
int pthread_mutex_destroy(pthread_mutex_t *mutex);
/* 對互斥鎖加鎖 */
int pthread_mutex_lock(pthread_mutex_t *mutex);
/* 對互斥鎖嘗試加鎖 */
int pthread_mutex_trylock(pthread_mutex_t *mutex);
/* 對互斥鎖解鎖 */
int pthread_mutex_unlock(pthread_mutex_t *mutex);
接下來我們來實現主線程負責接收用戶輸入,函數線程負責將用戶輸入打印到終端界面
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<string.h>
#include<unistd.h>#include<pthread.h>
#include<time.h>
#include<fcntl.h>pthread_mutex_t mutex;char buff[128] = {0};void *fun(void *arg)
{while(1){pthread_mutex_lock(&mutex);if(strncmp(buff,"end",3) == 0){break;}printf("fun :%s\n",buff);memset(buff,0,128);int n = rand() % 3 +1;sleep(n);pthread_mutex_unlock(&mutex);n = rand() % 3 + 1;sleep(n);}
}int main()
{srand((unsigned int)(time(NULL) * time(NULL)));pthread_mutex_init(&mutex,NULL);//初始化的鎖是解鎖狀態的//創建一個線程pthread_t id;int res = pthread_create(&id,NULL,fun,NULL);assert(res == 0);while(1){pthread_mutex_lock(&mutex);printf("input:");fgets(buff,127,stdin);pthread_mutex_unlock(&mutex);if(strncmp(buff,"end",3) == 0){break;}int n = rand()%3 + 1;sleep(n);}//等待函數線程的結束pthread_join(id,NULL);pthread_mutex_destroy(&mutex);exit(0);
}
讀寫鎖
讀寫鎖是更高級的互斥鎖,有更高的并行性。互斥鎖只允許一個線程對臨界區訪問,而讀寫鎖可以讓多個讀者并發訪問。
讀寫鎖可以多個讀者讀,但只允許一個寫者寫。
- 如果一個線程用讀鎖鎖定了臨界區,那么其他線程也可以用讀鎖來進入臨界區,這樣就可以多個線程并行操作。但這個時候,如果再進行寫鎖加鎖就會發生阻塞,寫鎖請求阻塞后,后面如果繼續有讀鎖來請求,這些后來的讀鎖都會被阻塞!這樣避免了讀鎖長期占用資源,防止寫鎖饑餓!
- 如果一個線程用寫鎖鎖住了臨界區,那么其他線程不管是讀鎖還是寫鎖都會發生阻塞!
/* 讀寫鎖的銷毀 */
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
/* 讀寫鎖的初始化 */
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,const pthread_rwlockattr_t *restrict attr);
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;/* 讀寫鎖加鎖解鎖 */
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);int pthread_rwlock_timedrdlock(pthread_rwlock_t *restrict rwlock, const struct timespec *restrict abs_timeout);
int pthread_rwlock_timedwrlock(pthread_rwlock_t *restrict rwlock, const struct timespec *restrict abs_timeout);int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
我們使用讀寫鎖來實現兩個線程讀寫一段數據
主線程寫入時加寫鎖,兩個函數線程讀取時加讀鎖,這樣兩個函數線程可以同時讀取,
# include<unistd.h>
#include <stdlib.h>
# include<stdio.h>
# include<string.h>
# include<time.h>
# include<assert.h>
# include<pthread.h>pthread_rwlock_t rwlock;//創建讀寫鎖
char buff[128]={0};void* fun(void* arg)//讀數據
{while(1){pthread_rwlock_rdlock(&rwlock);if(strncmp(buff,"end",3)==0){break;}printf("fun:%s\n",buff);//memset(buff,0,128);int n=rand()%3+1;sleep(n);pthread_rwlock_unlock(&rwlock);n=rand()%3+1;sleep(1);}
}
void* fun1(void* arg)//讀數據
{while(1){pthread_rwlock_rdlock(&rwlock);if(strncmp(buff,"end",3)==0){break;}printf("fun1:%s\n",buff);//memset(buff,0,128);int n=rand()%3+1;sleep(n);pthread_rwlock_unlock(&rwlock);n=rand()%3+1;sleep(1);}
}
int main()
{srand((unsigned int)(time(NULL)*time(NULL)));pthread_rwlock_init(&rwlock,NULL);//初始化pthread_t id[2];int res=pthread_create(&id[0],NULL,fun,NULL);//創建線程int r=pthread_create(&id[1],NULL,fun1,NULL);assert(res==0);while(1)//寫數據{pthread_rwlock_wrlock(&rwlock);printf("input:");fgets(buff,127,stdin);pthread_rwlock_unlock(&rwlock);if(strncmp(buff,"end",3)==0){break;}int n=rand()%3+1;sleep(1);}pthread_join(id[0],NULL);pthread_join(id[1],NULL);pthread_rwlock_destroy(&rwlock);
}
條件變量
條件變量是利用線程間共享全局變量進行同步的一種機制。一個線程修改條件,另一個線程等待條件,一旦等到自己需要的條件,就去運行。條件變量用pthread_cond_t類型的實例表示。
信號量
這個信號量和進程間用的信號量作用類似,當線程訪問一些有限的公共資源時,就必須做到線程間同步訪問。其實就類似于一個計數器,有一個初始值用于記錄臨界資源的個數。信號量由sem_t的實例表示。