美文网首页
linux多线程编程学习记录

linux多线程编程学习记录

作者: sgy1993 | 来源:发表于2019-02-26 19:24 被阅读0次

    并发:是指在同一时刻,只能有一条指令执行,但多个进程指令被快速轮换执行,使得宏观上具有多个进程同时执行的效果

    并行:是指在同一时刻,有多条指令在多个处理器上同时执行

    image.png

    pthread_self()用来获取线程id

    image.png image.png

    主线程退出的话,其他的线程也会跟着退出,如果使用pthread_exit()则不会导致其他线程退出。

    主线程打印奇数,子线程打印偶数,两个线程交替进行

    #include <stdio.h>
    #include <sys/types.h>          /* See NOTES */
    #include <sys/socket.h>
    #include <string.h>
    #include <stdlib.h>
    #include <netinet/in.h>
    #include <pthread.h>
    
    void *client_request_handler(void *param)
    {
        int i = 0;
        
        while (1) {
            i += 2;        
            printf("func:%s, line:%d, i:%d\n", __func__, __LINE__, i);
            sleep(1);
        }
        
        return (void *)0;
    }
    
    int main(int argc, char *argv[])
    {
        pthread_t tl_Tid;
    
        int l_iRet = 0;
        int i = 1;
    
        l_iRet = pthread_create(&tl_Tid, NULL, client_request_handler, &i);
        if (l_iRet < 0) {
            printf("can't create pthread\n");
            return -1;
        }
        while (1) {
            printf("func:%s, line:%d, i:0x%x\n", __func__, __LINE__, i);
            i += 2;
            sleep(1);
        }
        return 0;
    }
    

    线程的分离属性

    1. 创建一个线程默认是非分离的
    2. 分离和非分离的区别
      2.1 如果线程具有分离属性,线程终止时,资源会被立刻回收

    exit会导致整个进程退出

    image.png

    pthread_join()指定的线程必须是未分离的,如果是分离的就会失败。

    join的返回值需要注意,他返回的值指向的还是join的那个线程的内部,容易出现问题,有可能那个变量被释放了或者更改

    #include <stdio.h>
    #include <sys/types.h>          /* See NOTES */
    #include <sys/socket.h>
    #include <string.h>
    #include <stdlib.h>
    #include <netinet/in.h>
    #include <pthread.h>
    
    void *client_request_handler1(void *param)
    {
        int i = 1;
        printf("我是client_request_handler1, &i:0x%x\n", &i);
        return (void *)(&i);
    }
    
    void *client_request_handler2(void *param)
    {
        pthread_detach(pthread_self());
        return (void *)2;
    }
    
    int main(int argc, char *argv[])
    {
        pthread_t tl_Tid1;
        pthread_t tl_Tid2;
        int *lp_iVal1;
        int *lp_iVal2;
    
    
        int l_iRet = 0;
        int i = 1;
    
        l_iRet = pthread_create(&tl_Tid1, NULL, client_request_handler1, NULL);
        if (l_iRet < 0) {
            printf("can't create pthread\n");
            return -1;
        }
    
        l_iRet = pthread_create(&tl_Tid2, NULL, client_request_handler2, NULL);
        if (l_iRet < 0) {
            printf("can't create pthread\n");
            return -1;
        }
    //int pthread_join(pthread_t thread, void **retval);
    //int pthread_detach(pthread_t thread);
    
        l_iRet = pthread_join(tl_Tid1, &lp_iVal1);//如果线程已经分离,会join失败
        printf("client_request_handler1 join的结果是:%d\n", l_iRet);
        
        l_iRet = pthread_join(tl_Tid2, &lp_iVal2);
        printf("client_request_handler2 join的结果是:%d\n", l_iRet);
    
        printf("client_request_handler1 返回的结果是地址还是指针:0x%x\n", lp_iVal1);
    
        printf("client_request_handler1 返回的结果是:%d\n", lp_iVal1);
        printf("client_request_handler1 返回的结果是:%d\n", lp_iVal2);
    
        printf("func:%s, line:%d, i:0x%x\n", __func__, __LINE__, i);
        return 0;
    }
    

    输出的结果

    sgy@ubuntu:~/sgy/user_program/pthread$ ./test
    我是client_request_handler2
    我是client_request_handler1, &i:0xb75d734c
    client_request_handler1 join的结果是:0
    client_request_handler2 join的结果是:22
    client_request_handler1 返回的结果是地址还是指针:0xb75d734c
    client_request_handler1 返回的结果是:-1218612404
    client_request_handler1 返回的结果是:-1216454656
    func:main, line:59, i:0x1
    sgy@ubuntu:~/sgy/user_program/pthread$
    

    tid1 join成功,tid2先detach成了分离状态,所以tid2,join会失败,而tid1的返回值得地址就是和tid1里面i的地址是一样的,指向线程内部的局部变量。

    出错的number在哪里


    image.png

    线程的取消
    pthread_cancel ---只是发送一个请求,并不意味着等待线程终止,而且成功也不一定意味着tid一定会终止

    线程可以设置响不响应cancle信号,可以设置是立即响应还是延迟响应,默认是响应取消,延迟取消。

    那到底延时到什么时候呢?
    通过 man pthreads, 遇到取消点就会检查是否有取消信号。

    Cancellation points
    

    具体的函数说明

    #include <pthread.h>
    int pthread_setcancelstate( int state, int* oldstate );
    
    描述:
            pthread_setcancelstate() f函数设置线程取消状态为state,并且返回前一个取消点状态oldstate。
    
    取消点有如下状态值:
            PTHREAD_CANCEL_DISABLE:取消请求保持等待,默认值。
            PTHREAD_CANCEL_ENABLE:取消请求依据取消类型执行;参考pthread_setcanceltype()。
    
    参数:
            state:        新取消状态。
            oldstate:   指向本函数所存储的原取消状态的指针。
    

    test.c

    #include <stdio.h>
    #include <sys/types.h>          /* See NOTES */
    #include <sys/socket.h>
    #include <string.h>
    #include <stdlib.h>
    #include <netinet/in.h>
    #include <pthread.h>
    
    void *client_request_handler1(void *param)
    {
        int l_iOldState = 0;
        //int pthread_setcancelstate( int state, int* oldstate );
        pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &l_iOldState);
        printf("我是子线程client_request_handler1\n");
        sleep(4);//这个时候转过去执行main线程
    
        pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, &l_iOldState);
    
        printf("第1个取消点\n");
        printf("第2个取消点\n");
        return (void *)1;
    }
    
    
    
    int main(int argc, char *argv[])
    {
        pthread_t tl_Tid1;
        pthread_t tl_Tid2;
        int *lp_iVal1;
        int *lp_iVal2;
        int *lp_Rval;
    
        int l_iRet = 0;
        int i = 1;
    
        l_iRet = pthread_create(&tl_Tid1, NULL, client_request_handler1, NULL);
        if (l_iRet < 0) {
            printf("can't create pthread\n");
            return -1;
        }
        sleep(2);//去执行新线程
    
        pthread_cancel(tl_Tid1);
        pthread_join(tl_Tid1, &lp_Rval);
        printf("pthread_join的返回值是:%d\n", (int *)lp_Rval);
        return 0;
    }
    

    子线程里面将其取消状态设为了忽略取消,所以只有等到子线程将取消状态设为enable的时候,才会响应,在第一个printf的时候取消这个线程

    pthread_kill()---信号发送函数

    #include <stdio.h>
    #include <sys/types.h>          /* See NOTES */
    #include <sys/socket.h>
    #include <string.h>
    #include <stdlib.h>
    #include <netinet/in.h>
    #include <pthread.h>
    #include <signal.h>
    #include <errno.h>
    void *client_request_handler1(void *param)
    {
        int l_iOldState = 0;
        return (void *)1;
    }
    
    
    
    int main(int argc, char *argv[])
    {
        pthread_t tl_Tid1;
        pthread_t tl_Tid2;
        int *lp_iVal1;
        int *lp_iVal2;
        int *lp_Rval;
    
        int l_iRet = 0;
        int i = 1;
    
        l_iRet = pthread_create(&tl_Tid1, NULL, client_request_handler1, NULL);
        if (l_iRet < 0) {
            printf("can't create pthread\n");
            return -1;
        }
        sleep(2);
        l_iRet = pthread_kill(tl_Tid1, 0);
        if (ESRCH == l_iRet) {
            printf("不存在这个一个线程\n");
        }
        return 0;
    }
    

    main线程sleep 2s中,子线程退出,此时发送信号,返回值肯定是表示线程不存在

    线程的信号处理

    #include <stdio.h>
    #include <sys/types.h>          /* See NOTES */
    #include <sys/socket.h>
    #include <string.h>
    #include <stdlib.h>
    #include <netinet/in.h>
    #include <pthread.h>
    #include <signal.h>
    #include <errno.h>
    
    void pthread1_sig_handler(int arg)
    {
        printf("thread1 get signal\n");
        return;
    }
    
    void pthread2_sig_handler(int arg)
    {
        printf("thread2 get signal\n");
        return;
    }
    
    void *client_request_handler1(void *param)
    {
        printf("我是client_request_handler1\n");
        //注册新号处理函数
        struct sigaction l_stAct;
        memset(&l_stAct, 0, sizeof(l_stAct));
        sigaddset(&l_stAct.sa_mask, SIGQUIT);
        l_stAct.sa_handler = pthread1_sig_handler;
        sigaction(SIGQUIT, &l_stAct, NULL);
    
        //线程内屏蔽掉这个信号,对这个信号不作处理
        pthread_sigmask(SIG_BLOCK, &l_stAct.sa_mask, NULL);
        sleep(2);//转回去执行主线程
        return (void *)1;
    }
    
    void *client_request_handler2(void *param)
    {
        printf("我是client_request_handler2\n");
        //注册新号处理函数
        struct sigaction l_stAct;
        memset(&l_stAct, 0, sizeof(l_stAct));
        sigaddset(&l_stAct.sa_mask, SIGQUIT);
        l_stAct.sa_handler = pthread2_sig_handler;
        sigaction(SIGQUIT, &l_stAct, NULL);
    
        //线程内屏蔽掉这个信号,对这个信号不作处理
        //pthread_sigmask(SIG_BLOCK, &l_stAct.sa_mask, NULL);
        sleep(2);//转回去执行主线程
        return (void *)1;
    }
    
    
    
    
    int main(int argc, char *argv[])
    {
        pthread_t tl_Tid1;
        pthread_t tl_Tid2;
        int *lp_iVal1;
        int *lp_iVal2;
        int *lp_Rval;
    
        int l_iRet = 0;
        int i = 1;
    
        l_iRet = pthread_create(&tl_Tid1, NULL, client_request_handler1, NULL);
        if (l_iRet < 0) {
            printf("can't create pthread\n");
            return -1;
        }
        
        l_iRet = pthread_create(&tl_Tid2, NULL, client_request_handler2, NULL);
        if (l_iRet < 0) {
            printf("can't create pthread\n");
            return -1;
        }
    
        sleep(1);
        pthread_kill(tl_Tid1, SIGQUIT);
        pthread_kill(tl_Tid2, SIGQUIT);
    
        pthread_join(tl_Tid1, NULL);
        pthread_join(tl_Tid2, NULL);
        return 0;
    }
    

    如果是pthread2先执行,最终sigaction注册的是pthread1的信号处理函数,又因为pthread1 阻塞了信号,所以不会有打印,但是由于pthread2会对信号进行处理,所以会出现thread1 get signal

    sgy@ubuntu:~/sgy/user_program/pthread$ ./test
    我是client_request_handler2
    我是client_request_handler1
    thread1 get signal
    sgy@ubuntu:~/sgy/user_program/pthread$
    

    pthread_sigmask的那些参数的意思


    image.png

    线程清理函数

    在下面三种情况下,pthread_cleanup_push()压栈的"清理函数"会被调用:
    1, 线程调用pthread_exit()函数,而不是直接return.
    2, 响应取消请求时,也就是有其它的线程对该线程调用pthread_cancel()函数。
    3, 本线程调用pthread_cleanup_pop()函数,并且其参数非0
    

    使用return 0;的时候并不会导致线程清理函数被调用
    下面是示例代码

    #include <stdio.h>
    #include <sys/types.h>          /* See NOTES */
    #include <sys/socket.h>
    #include <string.h>
    #include <stdlib.h>
    #include <netinet/in.h>
    #include <pthread.h>
    #include <signal.h>
    #include <errno.h>
    
    /*
    void pthread_cleanup_push(void (*routine)(void *),
                                     void *arg);
    */
    void cleanup1(void *arg)
    {
        printf("执行cleanup1\n");
        return;
    }
    
    void cleanup2(void *arg)
    {
        printf("执行cleanup2\n");
        return;
    }
    
    int main(int argc, char *argv[])
    {
        pthread_cleanup_push(cleanup1, NULL);
        pthread_cleanup_push(cleanup2, NULL);
    
        pthread_cleanup_pop(0);
        pthread_cleanup_pop(0);
        return 0;
    }
    

    运行结果

    sgy@ubuntu:~/sgy/user_program/pthread$ ./test
    sgy@ubuntu:~/sgy/user_program/pthread$
    

    说明pthread_cleanup_pop(0);并不执行清理函数,那么是否会从栈上移走一个清理函数呢?
    修改一下源代码

        pthread_cleanup_push(cleanup1, NULL);
        pthread_cleanup_push(cleanup2, NULL);
    
        pthread_cleanup_pop(0);
        pthread_cleanup_pop(1);
    

    如果,pop(0)即便不执行,也会删除一个清理函数,那么下面一句pop(1)会执行cleanup2函数,看下面的运行结果

    sgy@ubuntu:~/sgy/user_program/pthread$ ./test
    执行cleanup1
    sgy@ubuntu:~/sgy/user_program/pthread$
    

    上面的运行结果说明了,pop(0),不会从栈上面删除线程清理函数

    线程的同步

    1. 互斥量机制
      1.1 互斥量变量定义


      image.png
    image.png

    下面的示例代码演示了出现竞争的现象

    #include <stdio.h>
    #include <sys/types.h>          /* See NOTES */
    #include <sys/socket.h>
    #include <string.h>
    #include <stdlib.h>
    #include <netinet/in.h>
    #include <pthread.h>
    #include <signal.h>
    #include <errno.h>
    
    struct student {
        int id;
        int age;
    }stu;
    
    int i = 0;
    
    void *client_request_handler1(void *param)
    {
        printf("我是client_request_handler1\n");
        while (1) {
            stu.age = i;
            stu.id = i;
            i++;
            if (stu.age != stu.id) {
                printf("出现了不同步的现象, func:%s, age:%d, id:%d\n", __func__, stu.age, stu.id);
                break;
            }
        }
        return (void *)1;
    }
    
    void *client_request_handler2(void *param)
    {
        printf("我是client_request_handler2\n");
        while (1) {
            stu.age = i;
            stu.id = i;
            i++;
            if (stu.age != stu.id) {
                printf("出现了不同步的现象, func:%s, age:%d, id:%d\n", __func__, stu.age, stu.id);
                break;
            }
        }
    
        return (void *)2;
    }
    
    int main(int argc, char *argv[])
    {
        
        pthread_t tl_Tid1;
        pthread_t tl_Tid2;
        
        int l_iRet = 0;
        l_iRet = pthread_create(&tl_Tid1, NULL, client_request_handler1, NULL);
        if (l_iRet < 0) {
            printf("can't create pthread\n");
            return -1;
        }
        
        l_iRet = pthread_create(&tl_Tid2, NULL, client_request_handler2, NULL);
        if (l_iRet < 0) {
            printf("can't create pthread\n");
            return -1;
        }
    
        pthread_join(tl_Tid1, NULL);
        pthread_join(tl_Tid2, NULL);
    
        return 0;
    }
    

    理论上来讲,age和id永远都不可能会出现不相等的情况,但是由于线程间存在同步竞争,确实会出现这样的情况

    sgy@ubuntu:~/sgy/user_program/pthread$ ./test
    我是client_request_handler2
    我是client_request_handler1
    出现了不同步的现象, func:client_request_handler1, age:6070011, id:6070012
    出现了不同步的现象, func:client_request_handler2, age:6070012, id:6070013
    sgy@ubuntu:~/sgy/user_program/pthread$
    

    怎么使用互斥量

    pthread_mutex_t t_gMutex;//定义
    pthread_mutex_init(&tg_Mutex, NULL);//初始化pthread_mutex_lock(&tg_Mutex);//加锁
    pthread_mutex_unlock(&tg_Mutex);//解锁
    

    读写锁

    1. 读状态加锁时,再有以写模式加锁的话,这个线程必须要等到所有的读锁释放,而且如果这个时候又来了读锁,会阻塞,防止写锁拿不到请求
    2. 写状态加锁时,如果有线程想要操作的话,所有的线程都会阻塞

    条件变量
    下面是一个使用条件变量的具体例子

    #include <stdio.h>
    #include <sys/types.h>          /* See NOTES */
    #include <sys/socket.h>
    #include <string.h>
    #include <stdlib.h>
    #include <netinet/in.h>
    #include <pthread.h>
    #include <signal.h>
    #include <errno.h>
    
    #define BUFFER_SIZE     3
    #define PRODUCT_CNT     10
    
    struct products {
        int szBuffer[BUFFER_SIZE];
        pthread_mutex_t t_Mutex;
        int iReadPos;
        int iWritePos;
        pthread_cond_t t_HaveData;
        pthread_cond_t t_NotFull;
    }buffer_ops;
    
    void init(struct products *p)
    {
        //初始化互斥量,保证对这个结构体的独占性
        pthread_mutex_init(&p->t_Mutex, NULL);
        pthread_cond_init(&p->t_HaveData, NULL);
        pthread_cond_init(&p->t_NotFull, NULL);
        p->iReadPos = 0;
        p->iWritePos = 0;
    }
    
    void finish(struct products *p)
    {
        pthread_mutex_destroy(&p->t_Mutex);
        pthread_cond_destroy(&p->t_HaveData);
        pthread_cond_destroy(&p->t_NotFull);
        p->iReadPos = 0;
        p->iWritePos = 0;
    }
    
    void put(struct products *p, int data)
    {
        pthread_mutex_lock(&p->t_Mutex);
        //判断是不是满的状态,下一个写的位置就是读,说明是满
        if ((p->iWritePos + 1)% BUFFER_SIZE == p->iReadPos) {
            printf("已经满了,等待非满的状态\n");
            pthread_cond_wait(&p->t_NotFull, &p->t_Mutex);
        }
    
        p->szBuffer[p->iWritePos] = data;
        p->iWritePos++;
        if (p->iWritePos >= BUFFER_SIZE) {
            p->iWritePos = 0;
        }
        
        //说明有数据了,应该提示消费者线程
        pthread_cond_signal(&p->t_HaveData);
        
        pthread_mutex_unlock(&p->t_Mutex);
    }
    
    int get(struct products *p)
    {
        int num = 0;
        pthread_mutex_lock(&p->t_Mutex);
    
        //为空
        if (p->iReadPos == p->iWritePos) {
            printf("现在是空的,等待数据\n");
            pthread_cond_wait(&p->t_HaveData, &p->t_Mutex);
        }
    
        num = p->szBuffer[p->iReadPos];
        p->iReadPos++;
        if (p->iReadPos >= BUFFER_SIZE) {
            p->iReadPos = 0;
        }
        //读出去一个说明没有满
        pthread_cond_signal(&p->t_NotFull);
        pthread_mutex_unlock(&p->t_Mutex);
        return num;
    }
    void *producer(void *param)
    {
        printf("我是生产者\n");
        int i = 0;
        //
        for (i = 0; i < 10; i++) {
            sleep(1);
            put(&buffer_ops, i);
            printf("生产%d成功, 现在的读位置:%d, 写位置:%d\n", i, buffer_ops.iReadPos, buffer_ops.iWritePos);
        }
        printf("生产线程准备退出!\n");
        return (void *)1;
    }
    
    void *consumer(void *param)
    {
        printf("我是消费者\n");
        int num = 0;
        static int cnt = 0;
        while (1) {
            sleep(2);
            num = get(&buffer_ops);
            printf("消费的数据是:%d, 现在的读位置:%d, 写位置:%d\n", num, buffer_ops.iReadPos, buffer_ops.iWritePos);
            if (PRODUCT_CNT == ++cnt) {
                break;
            }
        }
        printf("消费者准备好退出了!\n");
        return (void *)2;
    }
    
    int main(int argc, char *argv[])
    {
        
        pthread_t tl_Tid1;
        pthread_t tl_Tid2;
        
        int l_iRet = 0;
    
        l_iRet = pthread_create(&tl_Tid1, NULL, producer, NULL);
        if (l_iRet < 0) {
            printf("can't create pthread\n");
            return -1;
        }
        
        l_iRet = pthread_create(&tl_Tid2, NULL, consumer, NULL);
        if (l_iRet < 0) {
            printf("can't create pthread\n");
            return -1;
        }
    
        pthread_join(tl_Tid1, NULL);
        pthread_join(tl_Tid2, NULL);
    
        finish(&buffer_ops);
        return 0;
    }
    

    程序的运行结果

    sgy@ubuntu:~/sgy/user_program/pthread$ ./test
    我是消费者
    我是生产者
    生产0成功, 现在的读位置:0, 写位置:1
    消费的数据是:0, 现在的读位置:1, 写位置:1
    生产1成功, 现在的读位置:1, 写位置:2
    生产2成功, 现在的读位置:1, 写位置:0
    消费的数据是:1, 现在的读位置:2, 写位置:0
    生产3成功, 现在的读位置:2, 写位置:1
    已经满了,等待非满的状态
    消费的数据是:2, 现在的读位置:0, 写位置:1
    生产4成功, 现在的读位置:0, 写位置:2
    已经满了,等待非满的状态
    消费的数据是:3, 现在的读位置:1, 写位置:2
    生产5成功, 现在的读位置:1, 写位置:0
    已经满了,等待非满的状态
    消费的数据是:4, 现在的读位置:2, 写位置:0
    生产6成功, 现在的读位置:2, 写位置:1
    已经满了,等待非满的状态
    消费的数据是:5, 现在的读位置:0, 写位置:1
    生产7成功, 现在的读位置:0, 写位置:2
    已经满了,等待非满的状态
    消费的数据是:6, 现在的读位置:1, 写位置:2
    生产8成功, 现在的读位置:1, 写位置:0
    已经满了,等待非满的状态
    消费的数据是:7, 现在的读位置:2, 写位置:0
    生产9成功, 现在的读位置:2, 写位置:1
    生产线程准备退出!
    消费的数据是:8, 现在的读位置:0, 写位置:1
    消费的数据是:9, 现在的读位置:1, 写位置:1
    消费者准备好退出了!
    sgy@ubuntu:~/sgy/user_program/pthread$
    

    一次性初始化
    有的变量或者结构体只能初始化一次
    pthread_once


    image.png

    6074_线程的分离属性
    6075_线程栈属性
    6076_线程同步属性
    6077_线程私有数据

    线程与fork
    父进程锁住了互斥量,这个时候创建子进程的时候继承过来的

    Linux系统下可以通过procfs或sysctl命令来查看pid_max的值:

    cat /proc/sys/kernel/pid_max
    

    或者

    sgy@ubuntu:~$ sysctl kernel.pid_max
    kernel.pid_max = 32768
    sgy@ubuntu:~$
    

    其实, 此上限值是可以调整的, 系统管理员可以通过如下方法来修改此上限值:

    root@manu-rush:~# sysctl -w kernel.pid_max=4194304
    kernel.pid_max = 4194304
    

    进程组和会话
    进程组和会话在进程之间形成了两级的层次: 进程组是一组相关进程的集合, 会话是一组相关进程组的集合。 用人来打比方, 会话如同一个公司, 进程组如同公司里的部门, 进程则如同部门里的员工。 尽管每个员工都有父亲, 但是不影响员工同时属于某个公司中的某个部门

    可以调用如下指令来查看所有进程的层次关系:

    ps -ejH
    ps axjf
    

    新进程默认继承父进程的进程组ID和会话ID

    #include <unistd.h>
    int setpgid(pid_t pid, pid_t pgid);
    

    那么setpgid函数会将一个进程从原来所属的进程组迁移到pgid对应的进程组

    当前进程的pid号是20253

    sgy@ubuntu:~/sgy/user_program/aiworld_server$ ps
      PID TTY          TIME CMD
    20523 pts/27   00:00:00 bash
    21221 pts/27   00:00:00 ps
    

    另外开一个终端来跟踪这个终端运行的情况

    sgy@ubuntu:~$ sudo strace -f -p 20523
    
    manu@manu-hacks:~$ setsid sleep 100
    manu@manu-hacks:~$ ps ajxf
    PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND…
    1 4469 4469 4469 ? -1 Ss 1000 0:00 sleep 100
    

    相关文章

      网友评论

          本文标题:linux多线程编程学习记录

          本文链接:https://www.haomeiwen.com/subject/gxhzyqtx.html