Linux 多线程实践(一) | POSIX Thread 基础知识

990 阅读4分钟

「这是我参与11月更文挑战的第5天,活动详情查看:2021最后一次更文挑战

在Linux环境中使用POSIX Thread API可以很方便的写出多线程程序,本系列将对其API进行介绍, 本文为系列的第一篇文章, 主要介绍线程的基础操作。

在Linux中同一进程中的不同线程:

共享:

  • 打开的文件
  • 当前的工作目录
  • 用户和组
  • 信号和信号句柄

拥有自己的:

  • 线程Id
  • errno 错误码 (使用thread-local实现)
  • 优先级
  • 独立的线程栈

API

以下API均定义在头文件pthread.h中, 使用GCC编译时需要链接到pthread库上, 如下所示

gcc ./test.c -l pthread

pthread_create

pthread_create用于创建线程, 如果线程创建成功该函数返回0, 创建失败返回错误码,创建失败的原因可能是:

  • EAGAIN 系统资源不足或者进程中的线程数量达到限制, 我们可以通过cat /proc/sys/kernel/threads-max查看当前系统对于线程数量的限制 image.png
  • EINVAL attr参数中有非法的设置
  • EPERM attr参数中有不被允许的设置

该函数定义了以下参数:

  • thread 指向线程id的指针, 创建线程成功后会通过该指针将线程id保存到其中
  • attr 用于设置线程相关的参数, 如果想使用默认参数传NULL即可。如: 设置线程的栈大小以及栈的地址(此场景常见于嵌入式设备需要限制线程栈空间大小的场景)
  • start_routine 线程要执行的函数, 该函数的出入参均为一个无类型指针void*
  • arg 传给函数的参数
#include <pthread.h>

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

pthread_self

pthread_self 用于获取当前线程的id.

 pthread_t pthread_self(void);

pthread_join

调用pthread_join会等待目标线程执行完成之后才退出, 并获取线程的执行结果, 下文中我们将会给出Demo, 返回是否执行成功.

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

pthread_exit

终止当前线程的运行(调用此方法的线程), 并设置线程的返回值, 如果有其他线程正在阻塞等待(调用pthread_join)此线程返回它将接收到这一返回值。

void pthread_exit(void *retval);

pthread_detached

pthread_detached用于分离一个线程, 调用此方法的线程在终止后资源会被回收掉,并且不可等待(即其他线程不可调用pthread_join等待此线程)

int pthread_detach(pthread_t thread);

Demo

创建线程并传递参数

以下Demo展示了如何创建一个线程, 并传入参数以及取得该线程的返回值并进行打印。

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

void* just_print(void *arg) {
    char *str = (char *)arg;
    printf("Thread say: %s\n", str);
    char *res_str = (char *)malloc(256);
    sprintf(res_str, "My thread id is %ld", pthread_self());
    return (void *)res_str;
}

int main() {
    pthread_t tid;
    void* res;
    printf("thread create\n");
    int s = pthread_create(&tid, NULL, just_print, "Hello");
    if (s) {
        perror("thread create failed");
        return -1;
    }
    pthread_join(tid, &res);
    printf("thread exit \n");

    printf("Main thread recv msg: %s\n", (char *)res);
    return 0;
}

输出如下所示:

image.png

限制线程栈空间大小

上文中提到在创建线程时,我们可以通过pthread_attr_t结构体设置一些参数来改变线程的一些行为。 在一些特殊场景下这个功能是非常实用的, 比如:

  • 如果程序运行在嵌入式设备(内存受限), 为了节约内存我们可以通过pthread_attr_t来调小线程栈的大小以节约内存
  • 在执行一些比较复杂运算的情况下,我们也可通过此参数来调大线程的栈内存以获取更多的栈空间

操作系统的默认线程栈大小我们可以通过ulimit -s进行查看, 效果如下

image.png

如一下Demo所示, 我们通过pthread_attr_init来初始化pthread_attr_t结构体, 之后调用pthread_attr_set_stacksize来设置线程栈的大小。

注意设置线程栈大小的时候, 如果设置的栈大小小于PTHREAD_STACK_MIN时会返回非法参数的错误, 对于部分系统如果栈大小不是系统的页大小的倍数,也会引发错误。

如以下Demo所示, 我们使用THREAD_STACK_SIZE来表示分配给线程的栈空间大小, ALLOC_SIZE来表示线程的要执行的方法会申请的栈空间大小.

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

#define THREAD_STACK_SIZE 4096 * 6
#define ALLOC_SIZE 4096 * 4

void* malloc_mem(void *arg) {
    char msg[ALLOC_SIZE];
    printf("stack malloc done! \n");
}

int main() {
    pthread_t tid;
    pthread_attr_t attr;
    if (pthread_attr_init(&attr) != 0) {
        perror("init attr failed");
        return -1;
    }
    int r = pthread_attr_setstacksize(&attr, THREAD_STACK_SIZE);
    if(r != 0) {
        printf("set stack size failed, err=%s\n", strerror(r));
        return -1;
    }
    int size = 1024;
    int s = pthread_create(&tid, &attr, malloc_mem, &size);
    if (s) {
        perror("thread create failed:");
        return -1;
    }
    pthread_join(tid, NULL);
    return 0;
}

测试用例1, THREAD_STACK_SIZE = ALLOC_SIZE = 4096 * 6, 输出如下所示:

image.png

系统提示Segmentation fault, 原因是栈空间不足.

测试用例2, THREAD_STACK_SIZE = 4096 * 6, ALLOC_SIZE = 4096 * 4, 输出如下所示:

image.png

测试用例3 THREAD_STACK_SIZE = 4096 * 1, ALLOC_SIZE = 1024 输出如下所示

image.png

提示参数错误, 原因时分配给线程的栈空间小于PTHREAD_STACK_MIN