網站首頁 編程語言 正文
前言
在本篇文章當中講主要給大家介紹?pthread
?并發編程當中關于線程的基礎概念,并且深入剖析進程的相關屬性和設置,以及線程在內存當中的布局形式,幫助大家深刻理解線程。
深入理解 pthread_create
基礎例子介紹
在深入解析?pthread_create
?之前,我們先用一個簡單的例子簡單的認識一下 pthread,我們使用 pthread 創建一個線程并且打印 Hello world 字符串。
#include?<stdio.h> #include?<pthread.h> void*?func(void*?arg)?{ ??printf("Hello?World?from?tid?=?%ld\n",?pthread_self());?//?pthread_self?返回當前調用這個函數的線程的線程?id ??return?NULL; } int?main()?{ ??pthread_t?t;?//?定義一個線程 ??pthread_create(&t,?NULL,?func,?NULL);?//?創建線程并且執行函數?func? ??//?wait?unit?thread?t?finished ??pthread_join(t,?NULL);?//?主線程等待線程?t?執行完成然后主線程才繼續往下執行 ??printf("thread?t?has?finished\n"); ??return?0; }
編譯上述程序:
clang?helloworld.c?-o?helloworld.out?-lpthread 或者 gcc?helloworld.c?-o?helloworld.out?-lpthread
在上面的代碼當中主線程(可以認為是執行主函數的線程)首先定義一個線程,然后創建線程并且執行函數 func ,當創建完成之后,主線程使用 pthread_join 阻塞自己,直到等待線程 t 執行完成之后主線程才會繼續往下執行。
我們現在仔細分析一下?pthread_create
?的函數簽名,并且對他的參數進行詳細分析:
int?pthread_create(pthread_t?*thread,?const?pthread_attr_t?*attr, ??????????????????????????void?*(*start_routine)?(void?*),?void?*arg);
- 參數 thread 是一個類型為 pthread_t 的指針對象,將這個對象會在 pthread_create 內部會被賦值為存放線程 id 的地址,在后文當中我們將使用一個例子仔細的介紹這個參數的含義。
- 參數 attr 是一個類型為 pthread_attr_t 的指針對象,我們可以在這個對象當中設置線程的各種屬性,比如說線程取消的狀態和類別,線程使用的棧的大小以及棧的初始位置等等,在后文當中我們將詳細介紹這個屬性的使用方法,當這個屬性為 NULL 的時候,使用默認的屬性值。
- 參數 start_routine 是一個返回類型為 void* 參數類型為 void* 的函數指針,指向線程需要執行的函數,線程執行完成這個函數之后線程就會退出。
- 參數 arg ,傳遞給函數 start_routine 的一個參數,在上一條當中我們提到了 start_routine 有一個參數,是一個 void 類型的指針,這個參數也是一個 void 類型的指針,在后文當中我們使用一個例子說明這個參數的使用方法。
深入理解參數 thread
在下面的例子當中我們將使用 pthread_self 得到線程的 id ,并且通過保存線程 id 的地址的變量 t 得到線程的 id ,對兩個得到的結果進行比較。
#include?<stdio.h> #include?<pthread.h> void*?func(void*?arg)?{ ??printf("線程自己打印線程\tid?=?%ld\n",?pthread_self()); ??return?NULL; } int?main()?{ ??pthread_t?t; ??pthread_create(&t,?NULL,?func,?NULL); ??printf("主線程打印線程?t?的線程?id?=?%ld\n",?*(long*)(&t)); ??pthread_join(t,?NULL); ??return?0; }
上面程序的執行結果如下圖所示:
根據上面程序打印的結果我們可以知道,變量?pthread_t t
?保存的就是線程 id 的地址, 參數 t 和線程 id 之間的關系如下所示:
在上面的代碼當中我們首先對 t 取地址,然后將其轉化為一個 long 類型的指針,然后解引用就可以得到對應地址的值了,也就是線程的ID。
深入理解參數 arg
在下面的程序當中我們定義了一個結構體用于保存一些字符出的信息,然后創建一個這個結構體的對象,將這個對象的指針作為參數傳遞給線程要執行的函數,并且在線程內部打印字符串當中的內容。
#include?<stdio.h> #include?<pthread.h> #include?<malloc.h> #include?<stdlib.h> #include?<string.h> typedef?struct?info?{ ??char?s[1024];?//?保存字符信息 ??int??size;????//?保存字符串的長度 }info_t; static void*?func(void*?arg)?{ ??info_t*?in?=?(info_t*)arg; ??in->s[in->size]?=?'\0'; ??printf("string?in?arg?=?%s\n",?in->s); ??return?NULL; } int?main()?{ ??info_t*?in?=?malloc(sizeof(info_t));?//?申請內存空間 ??//?保存?HelloWorld?這個字符串?并且設置字符串的長度 ??in->s[0]?=?'H'; ??in->s[1]?=?'e'; ??in->s[2]?=?'l'; ??in->s[3]?=?'l'; ??in->s[4]?=?'o'; ??in->s[5]?=?'W'; ??in->s[6]?=?'o'; ??in->s[7]?=?'r'; ??in->s[8]?=?'l'; ??in->s[9]?=?'d'; ??in->size?=?10; ??pthread_t?t;?????????//?將?in?作為參數傳遞給函數?func ??pthread_create(&t,?NULL,?func,?(void*)in);? ??pthread_join(t,?NULL); ??free(in);?//?釋放內存空間 ??return?0; }
上面程序的執行結果如下所示:
可以看到函數參數已經做到了正確傳遞。
深入理解參數 attr
在深入介紹參數 attr 前,我們首先需要了解一下程序的內存布局,在64位操作系統當中程序的虛擬內存布局大致如下所示,從下往上依次為:只讀數據/代碼區、可讀可寫數據段、堆區、共享庫的映射區、程序棧區以及內核內存區域。我們程序執行的區域就是在棧區。
根據上面的虛擬內存布局示意圖,我們將其簡化一下得到單個線程的執行流和大致的內存布局如下所示(程序執行的時候有他的棧幀以及寄存器現場,圖中將寄存器也做出了標識):
程序執行的時候當我們進行函數調用的時候函數的棧幀就會從上往下生長,我們現在進行一下測試,看看程序的棧幀最大能夠達到多少。
#include?<stdio.h> #include?<stdlib.h> #include?<sys/types.h> #include?<unistd.h> int?times?=?1; void*?func(void*?arg)?{ ??char?s[1?<<?20];?//?申請?1MB?內存空間(分配在棧空間上) ??printf("times?=?%d\n",?times); ??times++; ??func(NULL); ??return?NULL; } int?main()?{ ??func(NULL); ??return?0; }
上述程序的執行結果如下圖所示:
從上面的程序我們可以看到在第 8 次申請棧內存的時候遇到了段錯誤,因此可以判斷棧空間大小在 8MB 左右,事實上我們可以查看 linux 操作系統上,棧內存的指定大小:
事實上在 linux 操作系統當中程序的棧空間的大小默認最大為 8 MB。
現在我們來測試一下,當我們創建一個線程的時候,線程的棧的大小大概是多少:
#include?<stdio.h> #include?<pthread.h> #include?<stdlib.h> #include?<sys/types.h> #include?<unistd.h> int?times?=?1; void*?func(void*?arg)?{ ??printf("times?=?%d\n",?times); ??times++; ??char?s[1?<<?20];?//?申請?1MB?內存空間(分配在棧空間上) ??func(NULL); ??return?NULL; } int?main()?{ ??pthread_t?t; ??pthread_create(&t,?NULL,?func,?NULL); ??pthread_join(t,?NULL); ??return?0; }
上面的程序執行結果如下圖所示,可以看到當我們創建一個線程的時候棧的最大的大小也是 8MB。
設置線程棧空間的大小
現在如果我們有一個需求,需要的棧空間大于 8MB,我們應該怎么辦呢?這就是我們所需要談到的 attr,這個變量是一個?pthread_attr_t?對象,這個對象的主要作用就是用于設置線程的各種屬性的,其中就包括線程的棧的大小,在下面的程序當中我們將線程的棧空間的大小設置成 24MB,并且使用程序進行測試。
#include?<stdio.h> #include?<pthread.h> #include?<stdlib.h> #define?MiB?*?1?<<?20 int?times?=?0; void*?stack_overflow(void*?args)?{ ??printf("times?=?%d\n",?++times); ??char?s[1?<<?20];?//?1?MiB ??stack_overflow(NULL); ??return?NULL; } int?main()?{ ??pthread_attr_t?attr; ??pthread_attr_init(&attr);?//?對變量?attr?進行初始化操作 ??pthread_attr_setstacksize(&attr,?24?MiB);?//?設置棧幀大小為?24?MiB?這里使用了一個小的?trick?大家可以看一下?MiB?的宏定義 ??pthread_t?t; ??pthread_create(&t,?&attr,?stack_overflow,?NULL); ??pthread_join(t,?NULL); ??pthread_attr_destroy(&attr);?//?釋放線程屬性的相關資源 ??return?0; }
上面的程序執行結果如下圖所示:
從上面程序的執行結果來看我們設置的 24 MB 的棧空間大小起到了效果,我們可以通過線程的遞歸次數可以看出來我們確實申請了那么大的空間。在上面的程序當中我們對屬性的操作如下,這也是對屬性操作的一般流程:
- 使用?
pthread_attr_init
?對屬性變量進行初始化操作。 - 使用各種各樣的函數對屬性 attr 進行操作,比如?
pthread_attr_setstacksize
,這個函數的作用就是用于設置線程的棧空間的大小。 - 使用?
pthread_attr_destroy
?釋放線程屬性相關的系統資源。
自己為線程的棧申請空間
在上一小節當中我們通過函數?pthread_attr_setstacksize
?給棧空間設置了新的大小,并且使用程序檢查驗證了新設置的棧空間大小,在這一小節當中我們將介紹使用我們自己申請的內存空間也可以當作線程的棧使用。我們將使用兩種方法取驗證這一點:
- 使用?
malloc
?函數申請內存空間,這部分空間主要在堆當中。 - 使用?
mmap
?系統調用在共享庫的映射區申請內存空間。
使用 malloc 函數申請內存空間
#include?<stdio.h> #include?<pthread.h> #include?<stdlib.h> #define?MiB?*?1?<<?20 int?times?=?0; static void*?stack_overflow(void*?args)?{ ??printf("times?=?%d\n",?++times); ??char?s[1?<<?20];?//?1?MiB ??stack_overflow(NULL); ??return?NULL; } int?main()?{ ??pthread_attr_t?attr; ??pthread_attr_init(&attr); ??void*?stack?=?malloc(2?MiB);?//?使用?malloc?函數申請內存空間?申請的空間大小為?2?MiB? ??pthread_t?t; ??pthread_attr_setstack(&attr,?stack,?2?MiB);?//?使用屬性設置函數設置棧的位置?棧的最低地址為?stack?棧的大小等于?2?MiB? ??pthread_create(&t,?&attr,?stack_overflow,?NULL); ??pthread_join(t,?NULL); ??pthread_attr_destroy(&attr);?//?釋放系統資源 ??free(stack);?//?釋放堆空間 ??return?0; }
上述程序的執行結果如下圖所示:
從上面的執行結果可以看出來我們設置的棧空間的大小為 2MB 成功了。在上面的程序當中我們主要使用?pthread_attr_setstack
?函數設置棧的低地址和棧空間的大小。我們申請的內存空間內存布局大致如下圖所示:
使用 mmap 申請內存作為棧空間
#define?_GNU_SOURCE #include?<stdio.h> #include?<pthread.h> #include?<stdlib.h> #include?<sys/mman.h> #define?MiB?*?1?<<?20 #define?STACK_SIZE?2?MiB int?times?=?0; static void*?stack_overflow(void*?args)?{ ??printf("times?=?%d\n",?++times); ??char?s[1?<<?20];?//?1?MiB ??stack_overflow(NULL); ??return?NULL; } int?main()?{ ??pthread_attr_t?attr; ??pthread_attr_init(&attr); ??void*?stack?=?mmap(NULL,?STACK_SIZE,?PROT_READ?|?PROT_WRITE, ????????????????MAP_PRIVATE?|?MAP_ANONYMOUS?|?MAP_STACK,?-1,?0); ??if?(stack?==?MAP_FAILED) ??????perror("mapped?error:"); ??pthread_t?t; ??pthread_attr_setstack(&attr,?stack,?STACK_SIZE); ??pthread_create(&t,?&attr,?stack_overflow,?NULL); ??pthread_join(t,?NULL); ??pthread_attr_destroy(&attr); ??free(stack); ??return?0; }
在上面的程序當中我們使用 mmap 系統調用在共享庫空間申請了一段內存空間,并且將其做為棧空間,我們在這里就不將程序執行的結果放出來了,上面整個程序和前面的程序相差不大,只是在申請內存方面發生了變化,總體的方向是不變的。
根據前面知識的學習,我們可以知道多個線程可以共享同一個進程虛擬地址空間,我們只需要給每個線程申請一個棧空間讓線程執行起來就行,基于此我們可以知道多個線程的執行流和大致的內存布局如下圖所示:
在上圖當中不同的線程擁有不同的棧空間和每個線程自己的寄存器現場,正如上圖所示,棧空間可以是在堆區也可以是在共享庫的映射區域,只需要給線程提供棧空間即可。
深入理解線程的狀態
在?pthread
?當中給我們提供了一個函數?pthread_cancel
?可以取消一個正在執行的線程,取消正在執行的線程之后會將線程的退出狀態(返回值)設置成宏定義?PTHREAD_CANCELED
?。我們使用下面的例子去理解一下線程取消的過程:
#include?<stdio.h> #include?<pthread.h> #include?<assert.h> void*?task(void*?arg)?{ ?while(1)?{ ????pthread_testcancel();?//?測試是否被取消執行了 ??} ??return?NULL; } int?main()?{ ??void*?res; ??pthread_t?t; ??pthread_create(&t,?NULL,?task,?NULL); ??int?s?=?pthread_cancel(t);?//?取消函數的執行 ??if(s?!=?0) ????fprintf(stderr,?"cancel?failed\n"); ??pthread_join(t,?&res); ??assert(res?==?PTHREAD_CANCELED); ??return?0; }
在上面的程序當中我們在主線程當中使用函數?pthread_cancel
?函數取消線程的執行,編譯執行上面的程序是可以通過的,也就是說程序正確執行了,而且 assert 也通過了。我們先不仔細去分析上面的代碼的執行流和函數的意義。我們先需要了解一個線程的基本特性。
與線程取消執行相關的一共有兩個屬性,分別是:
1.取消執行的狀態,線程的取消執行的狀態一共有兩個:
- PTHREAD_CANCEL_ENABLE:這個狀態表示這個線程是可以取消的,也是線程創建時候的默認狀態。
- PTHREAD_CANCEL_DISABLE:這個狀態表示線程是不能夠取消的,如果有一個線程發送了一個取消請求,那么這個發送取消消息的線程將會被阻塞直到線程的取消狀態變成 PTHREAD_CANCEL_ENABLE 。
2.取消執行的類型,取消線程執行的類型也有兩種:
- PTHREAD_CANCEL_DEFERRED:當一個線程的取消狀態是這個的時候,線程的取消就會被延遲執行,知道線程調用一個是取消點的(cancellation point)函數,比如 sleep 和 pthread_testcancel ,所有的線程的默認取消執行的類型就是這個類型。
- PTHREAD_CANCEL_ASYNCHRONOUS:如果線程使用的是這個取消類型那么線程可以在任何時候被取消執行,當他接收到了一個取消信號的時候,馬上就會被取消執行,事實上這個信號的實現是使用 tgkill 這個系統調用實現的。
事實上我們很少回去使用 PTHREAD_CANCEL_ASYNCHRONOUS ,因為這樣殺死一個線程會導致線程還有很多資源沒有釋放,會給系統帶來很大的災難,比如線程使用 malloc 申請的內存空間沒有釋放,申請的鎖和信號量沒有釋放,尤其是鎖和信號量沒有釋放,很容易造成死鎖的現象。
有了以上的知識基礎我們現在可以來談一談前面的兩個函數了:
- pthread_cancel(t) :是給線程 t 發送一個取消請求。
- pthread_testcancel():這個函數是一個取消點,當執行這個函數的時候,程序就會取消執行。
現在我們使用默認的線程狀態和類型創建一個線程執行死循環,看看線程是否能夠被取消掉:
#include?<stdio.h> #include?<pthread.h> #include?<assert.h> #include?<unistd.h> void*?task(void*?arg)?{ ??while(1)?{ ???? ??} ??return?NULL; } int?main()?{ ??void*?res; ??pthread_t?t1; ??pthread_create(&t1,?NULL,?task,?NULL); ??int?s?=?pthread_cancel(t1); ??if(s?!=?0)?//?s?==?0?mean?call?successfully ????fprintf(stderr,?"cancel?failed\n"); ??pthread_join(t1,?&res); ??assert(res?==?PTHREAD_CANCELED); ??return?0; }
在上面的代碼當中我們啟動了一個線程不斷的去進行進行死循環的操作,程序的執行結果為程序不會終止,因為主線程在等待線程的結束,但是線程在進行死循環,而且線程執行死循環的時候沒有調用一個是取消點的函數,因此程序不會終止取消。
下面我們更改程序,將線程的取消類型設置為?PTHREAD_CANCEL_ASYNCHRONOUS?,在看看程序的執行結果:
#include?<stdio.h> #include?<pthread.h> #include?<assert.h> #include?<unistd.h> void*?task(void*?arg)?{ ??pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS,?NULL); ??while(1)?{ ???? ??} ??return?NULL; } int?main()?{ ??void*?res; ??pthread_t?t1; ??pthread_create(&t1,?NULL,?task,?NULL); ??int?s?=?pthread_cancel(t1); ??if(s?!=?0)?//?s?==?0?mean?call?successfully ????fprintf(stderr,?"cancel?failed\n"); ??pthread_join(t1,?&res); ??assert(res?==?PTHREAD_CANCELED); ??return?0; }
在上面的程序當中我們在線程執行的函數當中使用?pthread_setcanceltype
?將線程的取消類型設置成 PTHREAD_CANCEL_ASYNCHRONOUS 這樣的話就能夠在其他線程使用 pthread_cancel 的時候就能夠立即取消線程的執行。
int?pthread_setcanceltype(int?type,?int?*oldtype)
上方是 pthread_setcanceltype 的函數簽名,在前面的使用當中我們只使用了第一個參數,第二個參數我們是設置成 NULL,第二個參數我們可以傳入一個 int 類型的指針,然后會在將線程的取消類型設置成 type 之前將前一個 type 拷貝到 oldtype 所指向的內存當中。
type: 有兩個參數:PTHREAD_CANCEL_ASYNCHRONOUS 和 PTHREAD_CANCEL_DEFERRED 。
int?pthread_setcancelstate(int?state,?int?*oldstate);
設置取消狀態的函數簽名和上一個函數簽名差不多,參數的含義也是差不多,type 表示需要設置的取消狀態,有兩個參數:PTHREAD_CANCEL_ENABLE 和 PTHREAD_CANCEL_DISABLE ,參數 oldstate 是指原來的線程的取消狀態,如果你傳入一個 int 類型的指針的話就會將原來的狀態保存到指針指向的位置。
其實關于線程的一些細節還有比較多的內容限于篇幅,在本篇文章當中主要給大家介紹這些細節。
關于棧大小程序的一個小疑惑
在上文當中我們使用了一個小程序去測試線程的棧空間的大小,并且打印函數?func
?的調用次數,每一次調用的時候我們都會申請 1MB 大小的棧空間變量。現在我們看下面兩個程序,在下面兩個程序只有?func
?函數有區別,而在?func
?函數當中主要的區別就是:
- 在第一個程序當中是先申請內存空間,然后再打印變量?
times
?的值。 - 在第二個程序當中是先打印變量?
times
?的值,然后再申請內存空間。
#include?<stdio.h> #include?<pthread.h> #include?<stdlib.h> #include?<sys/types.h> #include?<unistd.h> int?times?=?1; //?先申請內存空間再打印 void*?func(void*?arg)?{ ??char?s[1?<<?20];?//?申請?1MB?內存空間(分配在棧空間上) ??printf("times?=?%d\n",?times); ??times++; ??func(NULL); ??return?NULL; } int?main()?{ ??pthread_t?t; ??pthread_create(&t,?NULL,?func,?NULL); ??pthread_join(t,?NULL); ??return?0; }
#include?<stdio.h> #include?<pthread.h> #include?<stdlib.h> #include?<sys/types.h> #include?<unistd.h> int?times?=?1; //?先打印再申請內存空間 void*?func(void*?arg)?{ ??printf("times?=?%d\n",?times); ??times++; ??char?s[1?<<?20];?//?申請?1MB?內存空間(分配在棧空間上) ??func(NULL); ??return?NULL; } int?main()?{ ??pthread_t?t; ??pthread_create(&t,?NULL,?func,?NULL); ??pthread_join(t,?NULL); ??return?0; }
由于上面兩個程序的輸出結果是一樣的,所以我就只放出一個程序的輸出結果了:
但是不對呀!如果是后申請內存空間的話,程序的輸出應該能夠打印?times = 8
?啊,因為之前只申請了 7MB 的空間,我們打印?times = 8
?的時候還沒有執行到語句?char s[1 << 20];
?,那為什么也只打印到 7 呢?
出現上面問題的主要原因就需要看編譯器給我們編譯后的程序是如何申請內存空間的。我們將上面的函數?func
?的匯編代碼展示出來:
00000000004005e0 <func>: 4005e0: 55 push %rbp 4005e1: 48 89 e5 mov %rsp,%rbp 4005e4: 48 81 ec 20 00 10 00 sub $0x100020,%rsp 4005eb: 48 8d 04 25 3c 07 40 lea 0x40073c,%rax 4005f2: 00 4005f3: 48 89 7d f8 mov %rdi,-0x8(%rbp) 4005f7: 8b 34 25 40 10 60 00 mov 0x601040,%esi 4005fe: 48 89 c7 mov %rax,%rdi 400601: b0 00 mov $0x0,%al 400603: e8 c8 fe ff ff callq 4004d0 <printf@plt> 400608: 48 bf 00 00 00 00 00 movabs $0x0,%rdi 40060f: 00 00 00 400612: 8b 34 25 40 10 60 00 mov 0x601040,%esi 400619: 81 c6 01 00 00 00 add $0x1,%esi 40061f: 89 34 25 40 10 60 00 mov %esi,0x601040 400626: 89 85 ec ff ef ff mov %eax,-0x100014(%rbp) 40062c: e8 af ff ff ff callq 4005e0 <func> 400631: 48 bf 00 00 00 00 00 movabs $0x0,%rdi 400638: 00 00 00 40063b: 48 89 85 e0 ff ef ff mov %rax,-0x100020(%rbp) 400642: 48 89 f8 mov %rdi,%rax 400645: 48 81 c4 20 00 10 00 add $0x100020,%rsp 40064c: 5d pop %rbp 40064d: c3 retq
上面的匯編代碼是上面的程序在 x86_64 平臺下得到的,我們需要注意一行匯編指令?sub $0x100020,%rsp
?,這條指令的主要作用就是將棧頂往下擴展(棧是從上往下生長的)1 MB 字節(實際上稍微比 1MB 大一點,因為還有其他操作需要一些棧空間),事實上就是給變量 s 申請 1MB 的棧空間。
好了,看到這里就破案了,原來編譯器申請棧空間的方式是將棧頂寄存器 rsp ,往虛擬地址空間往下移動,而編譯器在函數執行剛開始的時候就申請了這么大的空間,因此不管是先申請空間再打印,還是先打印再申請空間,在程序被編譯成匯編指令之后,函數?func
?在函數剛開始就申請了對應的空間,因此才出現了都只打印到?times = 7
?。
總結
在本篇文章當中主要給大家介紹了線程的基本元素和一些狀態,還重點介紹了各種與線程相關屬性的函數,主要使用的各種函數如下:
- pthread_create,用與創建線程
- pthread_attr_init,初始話線程的基本屬性。
- pthread_attr_destroy,釋放屬性相關資源。
- pthread_join,用于等待線程執行完成。
- pthread_attr_setstacksize,用于設置線程執行棧的大小。
- pthread_attr_setstack,設置線程執行棧的棧頂和棧的大小。
- pthread_testcancel,用于檢測線程是否被取消了,是一個取消點。
- pthread_cancel,取消一個線程的執行。
原文鏈接:https://www.cnblogs.com/Chang-LeHung/p/16853089.html
相關推薦
- 2023-11-17 深度學習中分布式訓練的現狀及未來
- 2023-01-15 nx.adjacency_matrix計算鄰接矩陣與真實結果不一致的解決_python
- 2023-01-02 Kotlin?RadioGroup與ViewPager實現底層分頁按鈕方法_Android
- 2022-06-10 阿里云日志服務日志過濾器配置_服務器其它
- 2022-07-23 SQL?Server中T-SQL標識符介紹與無排序生成序號的方法_MsSql
- 2022-09-25 Django 遷移錯誤 Cannot add foreign key constraint,字段類型
- 2023-03-22 一文帶你學會Python?Flask框架設置響應頭_python
- 2023-10-17 My-form組件,基于element傳參展示用于后臺管理篩選的表單框
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支