Skip to content

线程操作函数

pthread_create

函数作用

创建一个新线程

函数原型

cpp
#include <pthread.h>

int pthread_create(pthread_t* thread, const pthread_attr_t* attr, void* (*start_routine) (void*), void* arg);

函数返回值

  • 成功,返回0
  • 失败,返回错误号

函数参数

  • pthread_t:传出参数,保存系统为我们分配好的线程ID

    • 当前Linux中可理解为:typedef unsigned long int pthread_t
  • attr:通常传NULL,表示使用线程默认属性。若想使用具体属性也可以修改该参数。

  • start_routine:函数指针,指向线程主函 数(线程体),该函数运行结束,则线程结束。

  • arg:线程主函数执行期间所使用的参数。

注意点

  • 由于pthread_create的错误码不保存在errno中,因此不能直接用perror()打印错误信息,可以先用strerror()把错误码转换成错误信息再打印。
  • 如果任意一个线程调用了exit_exit,则整个进程的所有线程都终止,由于从main函数return也相当于调用exit,为了防止新创建的线程还没有得到执行就终止,我们在main函数return之前延时1秒,这只是一种权宜之计,即使主线程等待1秒,内核也不一定会调度新创建的线程执行,下一节我们会看到更好的办法。

练习题

1 编写程序创建一个线程。

cpp
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>

void *pthreadMain(void *args);

int main(int argc, char *argv[]) {
    printf("This is main thread. tid is %lu.\n", pthread_self());

    pthread_t tid;
    pthread_create(&tid, NULL, pthreadMain, NULL);

    sleep(1);

    return 0;
}

void *pthreadMain(void *args) {
    printf("This is child thread. tid is %lu.\n", pthread_self());

    return NULL;
}

程序输出:

shell
$ gcc 01.c -pthread 
$ ./a.out 
This is main thread. tid is 139849667266368.
This is child thread. tid is 139849667262208.

2 编写程序创建一个线程,并给线程传递一个int参数

cpp
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>

void *pthreadMain(void *args);

int main(int argc, char *argv[]) {
    printf("This is main thread. tid is %lu.\n", pthread_self());

    int a = 10;
    pthread_t tid;
    pthread_create(&tid, NULL, pthreadMain, (void *) &a);

    sleep(1);

    return 0;
}

void *pthreadMain(void *args) {
    printf("This is child thread. tid is %lu.\n", pthread_self());
    printf("This is child thread. args is %d.\n", *(int *) args);

    return NULL;
}

程序输出:

shell
$ gcc 02.c -pthread 
$ ./a.out 
This is main thread. tid is 139816628172608.
This is child thread. tid is 139816628168448.
This is child thread. args is 10.

3 编写程序创建一个线程,并给线程传递一个结构体参数。

cpp
#include <pthread.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>

void *pthreadMain(void *args);

typedef struct _Person {
    char name[64];
    int age;
} Person;

int main(int argc, char *argv[]) {
    printf("This is main thread. tid is %lu.\n", pthread_self());

    Person p1;
    strcpy(p1.name, "张三");
    p1.age = 20;

    pthread_t tid;
    pthread_create(&tid, NULL, pthreadMain, (void *) &p1);

    sleep(1);

    return 0;
}

void *pthreadMain(void *args) {
    Person p = *(Person *) args;
    printf("This is child thread. tid is %lu.\n", pthread_self());
    printf("This is child thread. name is %s and age is %d.\n", p.name, p.age);

    return NULL;
}

程序输出:

shell
$ gcc 03.c -pthread 
$ ./a.out 
This is main thread. tid is 140242008282944.
This is child thread. tid is 140242008278784.
This is child thread. name is 张三 and age is 20.

4 编写程序,主线程循环创建5个子线程,并让子线程判断自己是第几个子线程。

cpp
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>

void *pthreadMain(void *args);

int main(int argc, char *argv[]) {
    printf("This is main thread. tid is %lu.\n", pthread_self());

    for (int i = 0; i < 5; ++i) {
        pthread_t tid;
        pthread_create(&tid, NULL, pthreadMain, &i);
    }

    sleep(1);

    return 0;
}

void *pthreadMain(void *args) {
    printf("This is child thread. tid is %lu. I'm thread %d.\n", pthread_self(), *(int *) args);

    return NULL;
}

程序输出:

shell
$ gcc 03.c -pthread 
$ ./a.out 
This is main thread. tid is 140154772158272.
This is child thread. tid is 140154772154112. I'm thread 1.
This is child thread. tid is 140154738583296. I'm thread 5.
This is child thread. tid is 140154763761408. I'm thread 5.
This is child thread. tid is 140154755368704. I'm thread 5.
This is child thread. tid is 140154746976000. I'm thread 5.

最后每个子线程打印出来的值并不是想象中的值,比如都是5,分析其原因:

  • 在创建子线程的时候使用循环因子作为参数传递给子线程,这样主线程和多个子线程就会共享变量i(变量i在main函数中定义,在整个进程都一直有效)所以在子线程看来变量i是合法的栈内存空间。

那么为什么最后每个子线程打印出来的值都是5呢?

是由于主线程可能会在一个cpu时间片内连续创建了5个子线程,此时变量i的值变成了5,当主线程失去cpu的时间片后,子线程得到cpu的时间片,子线程访问的是变量i的内存空间的值,所以打印出来值为5.

主线程和子线程共享同一块内存空间

主线程和子线程分时使用cpu资源

解决办法:不能使多个子线程都共享同一块内存空间,应该使每个子线程访问不同的内存空间,可以在主线程定义一个数组:int arr[5];,然后创建线程的时候分别传递不同的数组元素,这样每个子线程访问的就是互不相同的内存空间,这样就可以打印正确的值。

如下图:

多个子线程各自访问不同的内存空间

cpp
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>

void *pthreadMain(void *args);

int main(int argc, char *argv[]) {
    printf("This is main thread. tid is %lu.\n", pthread_self());

    int arr[5];
    for (int i = 0; i < 5; ++i) {
        arr[i] = i + 1;
        pthread_t tid;
        pthread_create(&tid, NULL, pthreadMain, &arr[i]);
    }

    sleep(1);

    return 0;
}

void *pthreadMain(void *args) {
    printf("This is child thread. tid is %lu. I'm thread %d.\n", pthread_self(), *(int *) args);

    return NULL;
}

程序输出:

shell
$ gcc 04_.c -pthread 
$ ./a.out 
This is main thread. tid is 140560627607360.
This is child thread. tid is 140560627603200. I'm thread 1.
This is child thread. tid is 140560619210496. I'm thread 2.
This is child thread. tid is 140560602425088. I'm thread 4.
This is child thread. tid is 140560594032384. I'm thread 5.
This is child thread. tid is 140560610817792. I'm thread 0.
This is child thread. tid is 140560610817792. I'm thread 0.
$ ./a.out 
This is main thread. tid is 140438114756416.
This is child thread. tid is 140438114752256. I'm thread 1.
This is child thread. tid is 140438106359552. I'm thread 2.
This is child thread. tid is 140438089574144. I'm thread 4.
This is child thread. tid is 140438081181440. I'm thread 5.
$ ./a.out 
This is main thread. tid is 140549498812224.
This is child thread. tid is 140549498808064. I'm thread 1.
This is child thread. tid is 140549482022656. I'm thread 3.

根据测试程序还可以得出结论:

  • 如果主线程早于子线程退出,则子线程可能得不到执行,因为主线程退出,整个进程空间都会被回收,子线程没有了生存空间,所以也就得不到执行。
  • 线程之间(包含主线程和子线程)可以共享同一变量,包含全局变量或者非全局变量(但是非全局变量必须在其有效的生存期内)
  • 如果子进程已经将内容输出到了屏幕上,但是 stdout 的缓冲区还没有清空时进程退出了,就会导致多输出一次。解决此问题的方法就是将主函数的 return返回更换为_exit()

pthread_exit

在线程中禁止调用exit函数,否则会导致整个进程退出,取而代之的是调用pthread_exit函数,这个函数是使一个线程退出,如果主线程调用pthread_exit函数也不会使整个进程退出,不影响其他线程的执行。

函数描述

将单个线程退出

函数原型

cpp
#include <pthread.h>

void pthread_exit(void* retval);

函数参数

  • retval 表示线程退出状态,通常传NULL

另注意,pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了,栈空间就会被回收。

练习:编写程序测试pthread_exit函数使一个线程退出。

cpp
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>

void threadMain(void *arg) {
    printf("Hello, I'm a child thread and I'm starting to execute.\n");
    sleep(3);
    printf("Hello, I'm a child thread. I'm done.\n");
}


int main(int argc, char const *argv[]) {
    pthread_t tid;
    pthread_create(&tid, NULL, (void *) threadMain, NULL);

    sleep(1);
    printf("I'm main thread. I'm done.\n");

    pthread_exit(0);
}

程序输出:

shell
$ gcc thread_exit.c -o a -pthread 
$ ./a
Hello, I'm a child thread and I'm starting to execute.
I'm main thread. I'm done.
Hello, I'm a child thread. I'm done.

通过程序测试得知,pthread_exit函数只是使一个线程退出,假如子线程里面调用了exit函数,会使整个进程终止;如果主线程调用了pthread_exit函数,并不影响子线程,只是使主线程自己退出。

pthread_join

函数描述

阻塞等待线程退出,获取线程退出状态。其作用,对应进程中的 waitpid() 函数。

函数原型

cpp
#include <pthread.h>

int pthread_join(pthread_t thread, void** retval);

函数返回值

  • 成功:0
  • 失败:错误号

函数参数

  • thread:线程ID
  • retval:存储线程结束状态,整个指针和pthread_exit的参数是同一块内存地址。

练习:编写程序,使主线程获取子线程的退出状态。

一般先定义void *ptr; 然后pthread_join(threadid, &ptr);

示例1:获取int类型的返回值

cpp
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>

void threadMain(void *arg) {
    printf("Hello, I'm a child thread and I'm starting to execute.\n");
    sleep(3);
    printf("Hello, I'm a child thread. I'm done.\n");
    pthread_exit((void *) 20);
}

int main(int argc, char const *argv[]) {

    pthread_t tid;
    pthread_create(&tid, NULL, (void *) threadMain, NULL);

    void *ptr;
    pthread_join(tid, &ptr);
    printf("child thread is exit, and num is %d.\n", (int *) ptr);

    return 0;
}

程序输出:

shell
$ gcc thread_join.c -o a -pthread 
thread_join.c: In function ‘main’:
thread_join.c:19:47: warning: format ‘%d’ expects argument of type ‘int’, but argument 2 has type ‘int * [-Wformat=]
19 |     printf("child thread is exit, and num is %d.\n", (int *) ptr);
   |                                              ~^      ~~~~~~~~~~~
   |                                               |      |
   |                                               int    int *
   |                                              %ls
$ ./a
Hello, I'm a child thread and I'm starting to execute.
Hello, I'm a child thread. I'm done.
child thread is exit, and num is 20.

示例2:获取自定义类型的返回值

cpp
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

typedef struct _Person {
    char name[64];
    int age;
} Person;


void threadMain(void *arg) {
    printf("Hello, I'm a child thread and I'm starting to execute.\n");
    sleep(3);
    printf("Hello, I'm a child thread. I'm done.\n");

    // 在堆区开辟空间存储要返回的内容
    Person *p = malloc(sizeof(Person));
    strcpy(p->name, "lisi");
    p->age = 20;
    pthread_exit(p);
}

int main(int argc, char const *argv[]) {

    pthread_t tid;
    pthread_create(&tid, NULL, (void *) threadMain, NULL);

    void *ptr;
    pthread_join(tid, &ptr);
    Person *p = ptr;
    printf("child thread is exit, and name is %s, age is %d.\n", p->name, p->age);

    // 使用完子线程返回的内容后释放子线程开辟的堆区的空间
    free(ptr);
    ptr = NULL;

    return 0;
}

程序输出:

shell
$ gcc thread_join_struct.c -o a -pthread 
$ ./a
Hello, I'm a child thread and I'm starting to execute.
Hello, I'm a child thread. I'm done.
child thread is exit, and name is lisi, age is 20.

pthread_detach

线程分离状态:指定该状态,线程主动与主控线程断开关系。线程结束后,其退出状态不由其他线程获取,而直接自己自动释放。网络、多线程服务器常用。

进程若有该机制,将不会产生僵尸进程。僵尸进程的产生主要由于进程死后,大部分资源被释放,一点残留资源仍存于系统中,导致内核认为该进程仍存在。

也可使用 pthread_create函数参2(线程属性)来设置线程分离。pthread_detach函数是在创建线程之后调用的。

函数描述

实现线程分离

函数原型

cpp
#include <pthread.h>

int pthread_detach(pthread_t thread);

函数返回值

  • 成功:0
  • 失败:错误号

一般情况下,线程终止后,其终止状态一直保留到其它线程调用pthread_join获取它的状态为止。但是线程也可以被置为detach状态,这样的线程一旦终止就立刻回收它占用的所有资源,而不保留终止状态。不能对一个已经处于detach状态的线程调用pthread_join,这样的调用将返回EINVAL错误。也就是说,如果已经对一个线程调用了pthread_detach就不能再调用pthread_join了。

练习:编写程序,在创建线程之后设置线程的分离状态。

说明:如果线程已经设置了分离状态,则再调用pthread_join就会失败,可用这个方法验证是否已成功设置分离状态。

cpp
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>

void threadMain(void *arg) {
    printf("Hello, I'm a child thread and I'm starting to execute.\n");
    sleep(30);
    printf("Hello, I'm a child thread. I'm done.\n");
}

int main(int argc, char const *argv[]) {
    pthread_t tid;
    pthread_create(&tid, NULL, (void *) threadMain, NULL);
    pthread_detach(tid);

    int ret = pthread_join(tid, NULL);
    if (ret != 0) {
        printf("pthread_join error num is %d, Thread is detach.\n", ret);
    }

    printf("Hello, I'm a main thread. I'm done.\n");

   // 此处主线程不能调用return返回,否则进程结束 子线程也无法继续执行。 
    pthread_exit(NULL);
}

程序输出:

shell
$ gcc thread_detach.c -o a -pthread 
$ ./a
pthread_join error num is 22, Thread is detach.
Hello, I'm a main thread. I'm done.
Hello, I'm a child thread and I'm starting to execute.
Hello, I'm a child thread. I'm done.

pthread_cancel

函数描述

杀死(取消)线程。其作用,对应进程中 kill() 函数。

函数原型

cpp
#include <pthread.h>

int pthread_cancel(pthread_t thread);

函数返回值

  • 成功:0
  • 失败:错误号

注意

线程的取消并不是实时的,而有一定的延时。需要等待线程到达某个取消点(检查点)。

类似于玩游戏存档,必须到达指定的场所(存档点,如:客栈、仓库、城里等)才能存储进度。杀死线程也不是立刻就能完成,必须要到达取消点。

取消点:是线程检查是否被取消,并按请求进行动作的一个位置。通常是一些系统调用creat,open,pause,close,read,write..... 执行命令man 7 pthreads可以查看具备这些取消点的系统调用列表。可粗略认为一个系统调用(进入内核)即为一个取消点。还以通过调用pthread_testcancel函数设置一个取消点。

函数原型:void pthread_testcancel(void);

练习:编写程序,让主线程取消子线程的执行。

先测试一下没有取消点看看能否使线程取消;然后调用pthread_testcancel设置一个取消点,看看能够使线程取消。

示例1:子线程中没有取消点的情况

cpp
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>

void threadMian(void *arg) {
    long sum = 0;
    for (int i = 0; i < 1000000000; ++i) {
        sum += i;
    }
    pthread_exit((void *) sum);
}

int main(int argc, char const *argv[]) {
    pthread_t tid;
    pthread_create(&tid, NULL, (void *) threadMian, NULL);

    pthread_cancel(tid);
    printf("Cancel completion.\n");

    void *ptr;
    int ret = pthread_join(tid, &ptr);
    if (ret == 0) {
        printf("Join finish.\n");
    } else {
        printf("Join faild.\n");
    }

    printf("thread return %ld.\n", (long *) ptr);

    return 0;
}

程序输出:

shell
$ gcc thread_cancel.c -o a -pthread 
thread_cancel.c: In function ‘main’:
thread_cancel.c:28:29: warning: format ‘%ld’ expects argument of type ‘long int’, but argument 2 has type ‘long int * [-Wformat=]
   28 |     printf("thread return %ld.\n", (long *) ptr);
      |                           ~~^      ~~~~~~~~~~~~
      |                             |      |
      |                             |      long int *
      |                             long int
      |                           %ln
$ ./a
Cancel completion.
Join finish.
thread return 499999999500000000.

示例2:子线程中有取消点的情况

cpp
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>

void threadMian(void *arg) {
    long sum = 0;
    for (int i = 0; i < 1000000000; ++i) {
        // 增加取消点
        pthread_testcancel();
        sum += i;
    }
    pthread_exit((void *) sum);
}

int main(int argc, char const *argv[]) {
    pthread_t tid;
    pthread_create(&tid, NULL, (void *) threadMian, NULL);

    pthread_cancel(tid);
    printf("Cancel completion.\n");

    void *ptr;
    int ret = pthread_join(tid, &ptr);
    if (ret == 0) {
        printf("Join finish.\n");
    } else {
        printf("Join faild.\n");
    }

    printf("thread return %ld.\n", (long *) ptr);

    return 0;
}

程序输出:

shell
$ gcc thread_testcancel.c -o a -pthread 
thread_testcancel.c: In function ‘main’:
thread_testcancel.c:29:29: warning: format ‘%ld’ expects argument of type ‘long int’, but argument 2 has type ‘long int * [-Wformat=]
   29 |     printf("thread return %ld.\n", (long *) ptr);
      |                           ~~^      ~~~~~~~~~~~~
      |                             |      |
      |                             |      long int *
      |                             long int
      |                           %ln
$ ./a
Cancel completion.
Join finish.
thread return -1.

pthread_equal

函数描述

  • 比较两个线程ID是否相等。

函数原型

cpp
#include <pthread.h>

int pthread_equal(pthread_t t1, pthread_t t2);

注意:这个函数是为了以能够扩展使用的, 有可能Linux在未来线程ID pthread_t 类型被修改为结构体实现。

进程和线程的函数比较

进程线程
forkpthread_create
exitpthread_exit
wait/waitpidpthread_join
killpthread_cancel
getpidpthread_self

资源清理栈

自动根据申请的资源数量释放相应数量的资源。

cpp
#include <pthread.h>

void pthread_cleanup_push(void (*routine)(void *), void *arg);
void pthread_cleanup_pop(int execute);

这两个函数要配合使用,。申请资源之后立马进行 push 压栈;在压栈后 pop 弹栈之前的代码处于一种局部作用域的状态。

从其定义可以看出,push 定义中只有两个左花括号 而与其匹配的两个右花括号在 pop 的定义之中。

当线程因为自行调用pthread_exit()或者被 cancel 终止时,pop 才起作用(不包括在线程启动函数中 return);并且线程可以自行调用 pop 来释放资源。

使用示例

cpp
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void *pthreadMain(void *args);
void clean1(void *arg);

int main(int argc, char *argv[]) {
    pthread_t tid;
    int ret = pthread_create(&tid, NULL, pthreadMain, NULL);
    if (ret != 0) {
        printf("pthread_create: %s", strerror(ret));
        return -1;
    }
    pthread_join(tid, NULL);

    return 0;
}


void *pthreadMain(void *args) {
    printf("Child thread start.\n");

    int *p1 = malloc(sizeof(int));
    pthread_cleanup_push(clean1, p1);
    pthread_cleanup_pop(1);

#if 1
    printf("Child thread over.\n");
    pthread_exit((void *) 0);
#endif

    char *p2 = malloc(sizeof(char));
    pthread_cleanup_push(clean1, p2);
    pthread_cleanup_pop(1);
#if 0
    printf("Child thread over.\n");
    pthread_exit((void *) 0);
#endif
}

void clean1(void *arg) {
    free(arg);
    printf("clean over.\n");
}

程序输出:

shell
$ gcc pthread_cleanup.c -pthread 
$ ./a.out 
Child thread start.
clean over.
Child thread over.

cleanup 的目的

使线程无论在什么时刻终止,都会根据申请的资源执行合理的释放行为。

注意点总结

  1. 每次申请资源后,把清理操作入栈
  2. 弹栈的时机:A pthread_exit() pthread_cancel() B pthread_cleanup_pop(1); 注意参数要填正数,填 0 不会主动释放资源
  3. push 和 pop 成对出现
  4. 有了 pop 不要手动回收资源

多线程和信号不能同时使用

多线程会共享注册信号的信息,

创建线程的时间不能直接使用 perror 打印错误信息

而是用返回值的数值来确定报错的类型。