• Window下线程与线程同步总结


    目录

    一 线程创建与使用

    线程创建函数 CreateThread与_beginthreadex

    等待函数 WaitForSingleObject 和 WaitForMultipleObjects

    例程代码

    二 线程同步

    互斥对象

    互斥对象使用

    CreateMutex函数原型

    例程代码 

     事件对象

    事件对象的使用

    函数原型

    例程代码

    关键代码段-(临界区)

    关键代码段的使用

    例程代码

     信号量 Semaphore

    信号量详细介绍

    函数原型

    例程代码 

     线程同步的四种方式对比

    线程死锁


    一 线程创建与使用

    线程创建函数 CreateThread与_beginthreadex

    CreateThread是一种微软在Windows API中提供了建立新的线程的函数,该函数在主线程的基础上创建一个新线程。线程终止运行后,线程对象仍然在系统中,必须通过CloseHandle函数来关闭该线程对象。函数原型如下

    HANDLE CreateThread(

    LPSECURITY_ATTRIBUTES lpThreadAttributes,//SD

    SIZE_T dwStackSize,//initialstacksize

    LPTHREAD_START_ROUTINE lpStartAddress,//threadfunction

    LPVOID lpParameter,//threadargument

    DWORD dwCreationFlags,//creationoption

    LPDWORD lpThreadId//threadidentifier

    )

    • 第一个参数 lpThreadAttributes 表示线程内核对象的安全属性,一般传入NULL表示使用默认设置。
    • 第二个参数 dwStackSize 表示线程栈空间大小。传入0表示使用默认大小(1MB)。
    • 第三个参数 lpStartAddress 表示新线程所执行的线程函数地址,多个线程可以使用同一个函数地址。
    • 第四个参数 lpParameter 是传给线程函数的参数。
    • 第五个参数 dwCreationFlags 指定额外的标志来控制线程的创建,为0表示线程创建之后立即就可以进行调度,如果为CREATE_SUSPENDED则表示线程创建后暂停运行,这样它就无法调度,直到调用ResumeThread()。
    • 第六个参数 lpThreadId 将返回线程的ID号,传入NULL表示不需要返回该线程ID号

    返回值 : // 成功返回新线程句柄, 失败返回0

    _beginthreadex是对CreateThread函数的优化,使用方法基本一致。函数原型如下

    unsigned long _beginthreadex(

      

        void *security,    // 安全属性, 为NULL时表示默认安全性

        unsigned stack_size,    // 线程的堆栈大小, 一般默认为0

        unsigned(_stdcall *start_address)(void *),   // 线程函数

        void *argilist, // 线程函数的参数

       unsigned initflag,    // 新线程的初始状态,0表示立即执行,//CREATE_SUSPENDED表示创建之后挂起

        unsigned *threaddr    // 用来接收线程ID

    );

    返回值 : // 成功返回新线程句柄, 失败返回0

     

    等待函数 WaitForSingleObject 和 WaitForMultipleObjects

    函数功能:等待函数 – 使线程进入等待状态,直到指定的内核对象被触发。函数原型如下

    DWORDWINAPIWaitForSingleObject(

      HANDLEhHandle,

      DWORDdwMilliseconds

    );

    函数说明:

    第一个参数为要等待的内核对象。

    第二个参数为最长等待的时间,以毫秒为单位,如传入5000就表示5秒,传入0就立即返回,传入INFINITE表示无限等待。

    因为线程的句柄在线程运行时是未触发的,线程结束运行,句柄处于触发状态。所以可以用WaitForSingleObject()来等待一个线程结束运行。

    函数返回值:

    在指定的时间内对象被触发,函数返回WAIT_OBJECT_0。超过最长等待时间对象仍未被触发返回WAIT_TIMEOUT。传入参数有错误将返回WAIT_FAILED

     

    WaitForMultipleObjects(

        _In_ DWORD nCount,    // 要监测的句柄的组的句柄的个数

        _In_reads_(nCount) CONST HANDLE* lpHandles,   //要监测的句柄的组

        _In_ BOOL bWaitAll,  // TRUE 等待所有的内核对象发出信号, FALSE 任意一个内核对象发出信号

        _In_ DWORD dwMilliseconds //等待时间

        );

     例程代码

    1. #include <stdio.h>
    2. #include <windows.h>
    3. #include <process.h>
    4. unsigned int WINAPI ThreadFun(LPVOID p)
    5. {
    6. int cnt = *((int*)p);
    7. for (int i = 0; i < cnt; i++)
    8. {
    9. Sleep(1000);
    10. puts("running thread");
    11. }
    12. return 0;
    13. }
    14. int main()
    15. {
    16. printf("main begin\n");
    17. int iParam = 5;
    18. unsigned int dwThreadID;
    19. DWORD wr;
    20. HANDLE hThread = (HANDLE)_beginthreadex(NULL, 0, ThreadFun,
    21. (void*)&iParam, 0, &dwThreadID);
    22. if (hThread == NULL)
    23. {
    24. puts("_beginthreadex() error");
    25. return -1;
    26. }
    27. //
    28. printf("WaitForSingleObject begin\n");
    29. if ((wr = WaitForSingleObject(hThread, INFINITE)) == WAIT_FAILED)
    30. {
    31. puts("thread wait error");
    32. return -1;
    33. }
    34. printf("WaitForSingleObject end\n");
    35. printf("main end\n");
    36. system("pause");
    37. return 0;
    38. }

    二 线程同步

    互斥对象

    互斥对象使用

    (mutex)属于内核对象,它能够确保线程拥有对单个资源的互斥访问权。

    互斥对象包含一个使用数量,一个线程ID和一个计数器。其中线程ID用于标识系统中的哪个线程当前拥有互斥对象,计数器用于指明该线程拥有互斥对象的次数。

    创建互斥对象:调用函数CreateMutex。调用成功,该函数返回所创建的互斥对象的句柄。

    请求互斥对象所有权:调用函数WaitForSingleObject函数。线程必须主动请求共享对象的所有权才能获得所有权。

    释放指定互斥对象的所有权:调用ReleaseMutex函数。线程访问共享资源结束后,线程要主动释放对互斥对象的所有权,使该对象处于已通知状态。

    CreateMutex函数原型

    HANDLE WINAPI CreateMutexW(

        _In_opt_ LPSECURITY_ATTRIBUTES lpMutexAttributes,   //指向安全属性

        _In_ BOOL bInitialOwner,   //初始化互斥对象的所有者  TRUE 立即拥有互斥体,false表示创建的这个mutex不属于任何线程;所以处于激发状态,也就是有信号状态

        _In_opt_ LPCWSTR lpName    //指向互斥对象名的指针  L“Bingo”

        );

    例程代码 

    1. #include <stdio.h>
    2. #include <windows.h>
    3. #include <process.h>
    4. #define NUM_THREAD 50
    5. unsigned WINAPI threadInc(void * arg);
    6. unsigned WINAPI threadDes(void * arg);
    7. long long num=0;
    8. HANDLE hMutex;
    9. int main(int argc, char *argv[])
    10. {
    11. HANDLE tHandles[NUM_THREAD];
    12. int i;
    13. hMutex=CreateMutex(NULL, FALSE, NULL);
    14. for(i=0; i<NUM_THREAD; i++)
    15. {
    16. if(i%2)
    17. tHandles[i]=(HANDLE)_beginthreadex(NULL, 0, threadInc, NULL, 0, NULL);
    18. else
    19. tHandles[i]=(HANDLE)_beginthreadex(NULL, 0, threadDes, NULL, 0, NULL);
    20. }
    21. WaitForMultipleObjects(NUM_THREAD, tHandles, TRUE, INFINITE);
    22. CloseHandle(hMutex);
    23. printf("result: %lld \n", num);
    24. return 0;
    25. }
    26. unsigned WINAPI threadInc(void * arg)
    27. {
    28. int i;
    29. WaitForSingleObject(hMutex, INFINITE);
    30. for(i=0; i<500000; i++)
    31. num+=1;
    32. ReleaseMutex(hMutex);
    33. return 0;
    34. }
    35. unsigned WINAPI threadDes(void * arg)
    36. {
    37. int i;
    38. WaitForSingleObject(hMutex, INFINITE);
    39. for(i=0; i<500000; i++)
    40. num-=1;
    41. ReleaseMutex(hMutex);
    42. return 0;
    43. }

     事件对象

     事件对象的使用

    事件对象也属于内核对象,它包含以下三个成员:

            ●    使用计数;

            ●    用于指明该事件是一个自动重置的事件还是一个人工重置的事件的布尔值;

            ●   用于指明该事件处于已通知状态还是未通知状态的布尔值。

    事件对象有两种类型:人工重置的事件对象自动重置的事件对象。这两种事件对象的区别在于当人工重置的事件对象得到通知时,等待该事件对象的所有线程均变为可调度线程;而当一个自动重置的事件对象得到通知时,等待该事件对象的线程中只有一个线程变为可调度线程。

    1.      创建事件对象

           调用CreateEvent函数创建或打开一个命名的或匿名的事件对象。

    2.      设置事件对象状态

           调用SetEvent函数把指定的事件对象设置为有信号状态。

    3.      重置事件对象状态

           调用ResetEvent函数把指定的事件对象设置为无信号状态。

    4.      请求事件对象

    线程通过调用WaitForSingleObject函数请求事件对象。

    函数原型

    HANDLE CreateEvent(   

    LPSECURITY_ATTRIBUTES lpEventAttributes, // 安全属性   


    BOOL bManualReset,   // 复位方式  TRUE 必须用ResetEvent手动复原  FALSE 自动还原为无信号状态

    BOOL bInitialState,   // 初始状态   TRUE 初始状态为有信号状态  FALSE 无信号状态

    LPCTSTR lpName     //对象名称  NULL  无名的事件对象 

    );

    例程代码

    1. /*
    2. 输入一个全局字串: ABCD AAADDD
    3. 通过多线程的方式来判断有几个字母A,必须用线程同步的方式实现;
    4. 事件对象来实现:
    5. */
    6. #include <stdio.h>
    7. #include <windows.h>
    8. #include <process.h>
    9. #define STR_LEN 100
    10. unsigned WINAPI NumberOfA(void* arg);
    11. unsigned WINAPI NumberOfOthers(void* arg);
    12. static char str[STR_LEN];
    13. static HANDLE hEvent;
    14. int main(int argc, char* argv[])
    15. {
    16. HANDLE hThread1, hThread2;
    17. fputs("Input string: ", stdout);
    18. fgets(str, STR_LEN, stdin);
    19. //NUll 默认的安全符 手动 FALSE 初始状态为无信号状态
    20. hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
    21. hThread1 = (HANDLE)_beginthreadex(NULL, 0, NumberOfA, NULL, 0, NULL);
    22. hThread2 = (HANDLE)_beginthreadex(NULL, 0, NumberOfOthers, NULL, 0, NULL);
    23. WaitForSingleObject(hThread1, INFINITE);
    24. WaitForSingleObject(hThread2, INFINITE);
    25. //直到2个线程执行完之后,再把事件设置为无信号状态
    26. ResetEvent(hEvent);
    27. CloseHandle(hEvent);
    28. system("pause");
    29. return 0;
    30. }
    31. unsigned WINAPI NumberOfA(void* arg)
    32. {
    33. int i, cnt = 0;
    34. //再没有执行fputs("Input string: ", stdout);
    35. //fgets(str, STR_LEN, stdin);SetEvent(hEvent);之前,卡在
    36. //WaitForSingleObject
    37. WaitForSingleObject(hEvent, INFINITE);
    38. for (i = 0; str[i] != 0; i++)
    39. {
    40. if (str[i] == 'A')
    41. cnt++;
    42. }
    43. printf("Num of A: %d \n", cnt);
    44. return 0;
    45. }
    46. unsigned WINAPI NumberOfOthers(void* arg)
    47. {
    48. int i, cnt = 0;
    49. //再没有执行fputs("Input string: ", stdout);
    50. //fgets(str, STR_LEN, stdin);SetEvent(hEvent);之前,卡在
    51. //WaitForSingleObject
    52. // WaitForSingleObject(hEvent, INFINITE);
    53. for (i = 0; str[i] != 0; i++)
    54. {
    55. if (str[i] != 'A')
    56. cnt++;
    57. }
    58. printf("Num of others: %d \n", cnt - 1);
    59. //把事件对象设置为有信号状态
    60. SetEvent(hEvent);
    61. return 0;
    62. }

     关键代码段-(临界区)

    关键代码段的使用

    关键代码段,也称为临界区,工作在用户方式下。它是指一个小代码段,在代码能够执行前,它必须独占对某些资源的访问权。通常把多线程中访问同一种资源的那部分代码当做关键代码段。

    初始化关键代码段-调用InitializeCriticalSection函数初始化一个关键代码段。

    InitializeCriticalSection(

        _Out_ LPCRITICAL_SECTION lpCriticalSection

        );

           该函数只有一个指向CRITICAL_SECTION结构体的指针。在调用InitializeCriticalSection函数之前,首先需要构造一个CRITICAL_SECTION结构体类型的对象,然后将该对象的地址传递给InitializeCriticalSection函数。

     

    进入关键代码段-调用EnterCriticalSection函数,以获得指定的临界区对象的所有权,该函数等待指定的临界区对象的所有权,如果该所有权赋予了调用线程,则该函数就返回;否则该函数会一直等待,从而导致线程等待。

     

    退出关键代码段-线程使用完临界区所保护的资源之后,需要调用LeaveCriticalSection函数,释放指定的临界区对象的所有权。之后,其他想要获得该临界区对象所有权的线程就可以获得该所有权,从而进入关键代码段,访问保护的资源。

    删除临界区-当临界区不再需要时,可以调用DeleteCriticalSection函数释放该对象,该函数将释放一个没有被任何线程所拥有的临界区对象的所有资源。

    例程代码

    1. #include <stdio.h>
    2. #include <windows.h>
    3. #include <process.h>
    4. int iTickets = 5000;
    5. CRITICAL_SECTION g_cs;
    6. // A窗口 B窗口
    7. DWORD WINAPI SellTicketA(void* lpParam)
    8. {
    9. while (1)
    10. {
    11. EnterCriticalSection(&g_cs);//进入临界区
    12. if (iTickets > 0)
    13. {
    14. Sleep(1);
    15. iTickets--;
    16. printf("A remain %d\n", iTickets);
    17. LeaveCriticalSection(&g_cs);//离开临界区
    18. }
    19. else
    20. {
    21. LeaveCriticalSection(&g_cs);//离开临界区
    22. break;
    23. }
    24. }
    25. return 0;
    26. }
    27. DWORD WINAPI SellTicketB(void* lpParam)
    28. {
    29. while (1)
    30. {
    31. EnterCriticalSection(&g_cs);//进入临界区
    32. if (iTickets > 0)
    33. {
    34. Sleep(1);
    35. iTickets--;
    36. printf("B remain %d\n", iTickets);
    37. LeaveCriticalSection(&g_cs);//离开临界区
    38. }
    39. else
    40. {
    41. LeaveCriticalSection(&g_cs);//离开临界区
    42. break;
    43. }
    44. }
    45. return 0;
    46. }
    47. int main()
    48. {
    49. HANDLE hThreadA, hThreadB;
    50. hThreadA = CreateThread(NULL, 0, SellTicketA, NULL, 0, NULL); //2
    51. hThreadB = CreateThread(NULL, 0, SellTicketB, NULL, 0, NULL); //2
    52. CloseHandle(hThreadA); //1
    53. CloseHandle(hThreadB); //1
    54. InitializeCriticalSection(&g_cs); //初始化关键代码段
    55. Sleep(40000);
    56. DeleteCriticalSection(&g_cs);//删除临界区
    57. system("pause");
    58. return 0;
    59. }

     信号量 Semaphore

    信号量详细介绍

    -内核对象的状态:

    触发状态(有信号状态),表示有可用资源。

    未触发状态(无信号状态),表示没有可用资源

    -信号量的组成

      计数器:该内核对象被使用的次数

      最大资源数量:标识信号量可以控制的最大资源数量(带符号的32位)

      当前资源数量:标识当前可用资源的数量(带符号的32位)。即表示当前开放资源的个数(注意不是剩下资源的个数),只有开放的资源才能被线程所申请。但这些开放的资源不一定被线程占用完。比如,当前开放5个资源,而只有3个线程申请,则还有2个资源可被申请,但如果这时总共是7个线程要使用信号量,显然开放的资源5个是不够的。这时还可以再开放2个,直到达到最大资源数量。

    信号量的规则如下:

    (1)如果当前资源计数大于0,那么信号量处于触发状态(有信号状态),表示有可用资源。

    (2)如果当前资源计数等于0,那么信号量属于未触发状态(无信号状态),表示没有可用资源。

    (3)系统绝对不会让当前资源计数变为负数

    (4)当前资源计数绝对不会大于最大资源计数

     

    信号量与互斥量不同的地方是,它允许多个线程在同一时刻访问同一资源,但是需要限制在同一时刻访问此资源的最大线程数目。信号量对象对线程的同步方式与前面几种方法不同,信号允许多个线程同时使用共享资源。

    函数原型

    创建信号量 CreateSemaphoreW

    HANDLE WINAPI CreateSemaphoreW(

        _In_opt_ LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,  // Null 安全属性

       _In_ LONG lInitialCount,  //初始化时,共有多少个资源是可以用的。 0:未触发状//态(无信号状态),表示没有可用资源

        _In_ LONG lMaximumCount,  //能够处理的最大的资源数量   

        _In_opt_ LPCWSTR lpName   //NULL 信号量的名称

    );

     增加信号量 ReleaseSemaphore

    WINAPI ReleaseSemaphore(

        _In_ HANDLE hSemaphore,   //信号量的句柄

        _In_ LONG lReleaseCount,   //将lReleaseCount值加到信号量的当前资源计数上面

        _Out_opt_ LPLONG lpPreviousCount  //当前资源计数的原始值

    );

    例程代码 

    1. #include <stdio.h>
    2. #include <windows.h>
    3. #include <process.h>
    4. unsigned WINAPI Read(void* arg);
    5. unsigned WINAPI Accu(void* arg);
    6. static HANDLE semOne;
    7. static HANDLE semTwo;
    8. static int num;
    9. int main(int argc, char* argv[])
    10. {
    11. HANDLE hThread1, hThread2;
    12. semOne = CreateSemaphore(NULL, 0, 1, NULL);
    13. //semOne 没有可用资源 只能表示0或者1的二进制信号量 无信号
    14. semTwo = CreateSemaphore(NULL, 1, 1, NULL);
    15. //semTwo 有可用资源,有信号状态 有信号
    16. hThread1 = (HANDLE)_beginthreadex(NULL, 0, Read, NULL, 0, NULL);
    17. hThread2 = (HANDLE)_beginthreadex(NULL, 0, Accu, NULL, 0, NULL);
    18. WaitForSingleObject(hThread1, INFINITE);
    19. WaitForSingleObject(hThread2, INFINITE);
    20. CloseHandle(semOne);
    21. CloseHandle(semTwo);
    22. system("pause");
    23. return 0;
    24. }
    25. unsigned WINAPI Read(void* arg)
    26. {
    27. int i;
    28. for (i = 0; i < 5; i++)
    29. {
    30. fputs("Input num: ", stdout); // 1 5 11
    31. printf("begin read\n"); // 3 6 12
    32. //等待内核对象semTwo的信号,如果有信号,继续执行;如果没有信号,等待
    33. WaitForSingleObject(semTwo, INFINITE);
    34. printf("beginning read\n"); //4 10 16
    35. scanf("%d", &num);
    36. ReleaseSemaphore(semOne, 1, NULL);
    37. }
    38. return 0;
    39. }
    40. unsigned WINAPI Accu(void* arg)
    41. {
    42. int sum = 0, i;
    43. for (i = 0; i < 5; i++)
    44. {
    45. printf("begin Accu\n"); //2 9 15
    46. //等待内核对象semOne的信号,如果有信号,继续执行;如果没有信号,等待
    47. WaitForSingleObject(semOne, INFINITE);
    48. printf("beginning Accu\n"); //7 13
    49. sum += num;
    50. printf("sum = %d \n", sum); // 8 14
    51. ReleaseSemaphore(semTwo, 1, NULL);
    52. }
    53. printf("Result: %d \n", sum);
    54. return 0;
    55. }

     线程同步的四种方式对比

    比较

    互斥量

    Mutex

    事件对象

    Event

    信号量对象

    Semaphore

    关键代码段

    Criticalsection

    是否为内核对象

    速度

    较慢

    较慢

    多个进程中的线程同步

    支持

    支持

    支持

    不支持

    发生死锁

    组成

    一个线程ID;用来标识哪个线程拥有该互斥量;一个计数器:用来知名该线程用于互斥对象的次数

    一个使用计数;一个布尔值:用来标识该事件是自动重置还是人工重置;一个布尔值:标识该事件处于有信号状态还是无信号状态

    一个使用计数;

    最大资源数;

    标识当前可用的资源数

    一个小代码段;

    在代码能够执行前,必须占用对某些资源的访问权

    相关函数

    CreateMutex

    WaitForSingleObjects

    //被保护的内容

    ReleaseMutex

    CreateEvent

    ResetEvent

    WaitforSingleobject

    //保护内容

    SetEvent

    CreateSemaphore

    WaitForsingleobject

    //被保护的内容

    ReleaseSemaPhore

    InitialCriticalSection

    EnterCritionSection

    //被保护的内容

    LeaveCritialSection

    DeleteCritialSection

    注意事项

    谁拥有互斥对象,谁释放;

    如果多次在同一个线程中请求同一个互斥对象,需要多次调用releaseMutex

    为了实现线程间的同步,不应该使用人工重置,应该把第二个参数设置false;设置为自动重置

    它允许多个线程在同一时间访问同一个资源;但是需要限制访问此资源的最大线程数目;

    防止死锁:使用多个关键代码段变量的时候

    类比

    一把钥匙

    钥匙(自动/人工)

    停车场和保安

    电话亭

     

    线程死锁

    死锁是指多个线程因竞争资源而造成的一种僵局(互相等待),若无外力作用,这些进程都将无法向前推进

    使用关键代码段时,很容易进入死锁状态,因为在等待进入关键代码段时无法设定超时值

    代码举例

    1. #include <stdio.h>
    2. #include <windows.h>
    3. #include <process.h>
    4. int iTickets = 5000;
    5. CRITICAL_SECTION g_csA;
    6. CRITICAL_SECTION g_csB;
    7. // A窗口 B窗口
    8. DWORD WINAPI SellTicketA(void* lpParam)
    9. {
    10. while (1)
    11. {
    12. EnterCriticalSection(&g_csA);//进入临界区A
    13. Sleep(1);
    14. EnterCriticalSection(&g_csB);//进入临界区B
    15. if (iTickets > 0)
    16. {
    17. Sleep(1);
    18. iTickets--;
    19. printf("A remain %d\n", iTickets);
    20. LeaveCriticalSection(&g_csB);//离开临界区B
    21. LeaveCriticalSection(&g_csA);//离开临界区A
    22. }
    23. else
    24. {
    25. LeaveCriticalSection(&g_csB);//离开临界区B
    26. LeaveCriticalSection(&g_csA);//离开临界区A
    27. break;
    28. }
    29. }
    30. return 0;
    31. }
    32. DWORD WINAPI SellTicketB(void* lpParam)
    33. {
    34. while (1)
    35. {
    36. EnterCriticalSection(&g_csB);//进入临界区B
    37. Sleep(1);
    38. EnterCriticalSection(&g_csA);//进入临界区A
    39. if (iTickets > 0)
    40. {
    41. Sleep(1);
    42. iTickets--;
    43. printf("B remain %d\n", iTickets);
    44. LeaveCriticalSection(&g_csA);//离开临界区A
    45. LeaveCriticalSection(&g_csB);//离开临界区B
    46. }
    47. else
    48. {
    49. LeaveCriticalSection(&g_csA);//离开临界区A
    50. LeaveCriticalSection(&g_csB);//离开临界区B
    51. break;
    52. }
    53. }
    54. return 0;
    55. }
    56. int main()
    57. {
    58. HANDLE hThreadA, hThreadB;
    59. hThreadA = CreateThread(NULL, 0, SellTicketA, NULL, 0, NULL); //2
    60. hThreadB = CreateThread(NULL, 0, SellTicketB, NULL, 0, NULL); //2
    61. CloseHandle(hThreadA); //1
    62. CloseHandle(hThreadB); //1
    63. InitializeCriticalSection(&g_csA); //初始化关键代码段A
    64. InitializeCriticalSection(&g_csB); //初始化关键代码段B
    65. Sleep(40000);
    66. DeleteCriticalSection(&g_csA);//删除临界区
    67. DeleteCriticalSection(&g_csB);//删除临界区
    68. system("pause");
    69. return 0;
    70. }

  • 相关阅读:
    项目中根据excel文件生成json多语言文件
    分享5个和安全相关的 VSCode 插件
    【JavaSE】继承和多态
    linux中利用fork复制进程,printf隐藏的缓冲区,写时拷贝技术,进程的逻辑地址与物理地址
    ubuntu安装node.js 2023年最新
    zabbix配置钉钉告警(附含钉钉告警脚本 · 实战亲测无任何问题)
    dubbo 问题整理
    empty、arange、linspace、rand/randn、normal/uniform_、randperm、加减乘除幂对数、取整/余、比较运算
    VSCode 配置 C 语言编程环境
    引导流程分析——BIOS 与 UEFI
  • 原文地址:https://blog.csdn.net/weixin_40582034/article/details/125554618