pthread多线程使用
前言
一个进程,想要并行处理不同的事情,就可以使用多线程来实现。
比如有两个led灯,一个红色led、一个蓝色led。 想让红色led以2秒周期闪烁, 蓝色led以1秒周期闪烁。
在一个进程里面,当然可以通过一些技巧来实现一个while(1)实现两个led不同周期闪烁。
但我们可以使用两个线程来实现,一个线程控制红色led,一个线程控制蓝色led。
多线程的概念与实时操作系统,如freertos、ucos-ii的多任务类似。
pthread API相关介绍
pthread_create()
可以通过pthread_create
函数切入多线程的使用,该函数用于创建一个线程。
linux系统下,通过man手册,可以查询对应的使用方法。
man pthread_create # 终端输入man pthread_create 查询man 手册
PTHREAD_CREATE(3) Linux Programmer's Manual PTHREAD_CREATE(3)
NAME
pthread_create - create a new thread
SYNOPSIS
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
Compile and link with -pthread.
# 省略部分描述
函数原型
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
从函数原型中,可以看到,创建一个线程需要准备4个参数。
pthread_t *thread
const pthread_attr_t *attr
void *(*start_routine) (void *)
void *arg
1、pthread_t *thread --> 线程句柄 or 编号
相当于一个线程句柄, 线程编号。实际上是一个无符号长整型
typedef unsigned long int pthread_t;
定义变量时,可以一个个线程的变量进行定义,比如
pthread_t handle1;
pthread_t handle2;
也可以直接通过一个数组来定义多个句柄
#define MAX_HANDLE 1024
int handle_num = 0; //记录已经创建多少个句柄
pthread_t handle_list[MAX_HANDLE]; //创建1024个句柄,可以通过handle_list[handle_num]来创建线程。
线程编号(句柄)在一些线程操作中可以使用。
比如在int pthread_join(pthread_t thread, void **retval);
中可以使用。
pthread_join()
用来等待一个线程的结束。
2、const pthread_attr_t *attr --> 线程属性设置
对于简单利用的线程来说, 一般都无需设置attr,使用默认数值即可, 即将attr设置NULL。
man手册对于attr的描述
The attr argument points to a pthread_attr_t structure whose contents are used at thread
creation time to determine attributes for the new thread;
this structure is initialized using pthread_attr_init(3) and related functions.
If attr is NULL, then the thread is created with default attributes
可以通过attr设置一个新线程的属性, pthread_attr_t
结构体如下:
typedef struct
{
int detachstate; 线程的分离状态
int schedpolicy; 线程调度策略
struct sched_param schedparam; 线程的调度参数
int inheritsched; 线程的继承性
int scope; 线程的作用域
size_t guardsize; 线程栈末尾的警戒缓冲区大小
int stackaddr_set;
void * stackaddr; 线程栈的位置
size_t stacksize; 线程栈的大小
}pthread_attr_t;
man手册提到,默认状态下,不指定attr,即attr为NULL,创建的线程是joinable
By default, a new thread is created in a joinable state, unless attr was set to create the thread in a detached state (using pthread_attr_setdetachstate(3))
线程默认栈大小,会与不同的架构有关系,可以通过pthread_attr_setstacksize()
函数来设置
int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize);
stacksize的单位是bytes
不同架构默认栈大小表格如下:
架构 | 默认栈大小 |
---|---|
i386 | 2 MB |
IA-64 | 32 MB |
PowerPC | 4 MB |
S/390 | 2 MB |
Sparc-32 | 2 MB |
Sparc-64 | 4 MB |
x86_64 | 2 MB |
对于ubuntu20.04来说,可以用uname -a
查看对于架构,可以看到是x86_64,所以对于的默认栈大小是2MB
uname -a
Linux allen 5.13.0-41-generic #46~20.04.1-Ubuntu SMP Wed Apr 20 13:16:21 UTC 2022 x86_64 x86_64 x86_64 GNU/Linux
3、void *(*start_routine) (void *) --> 回调函数
void *(*start_routine) (void *)
对应的就是实际多线程运行的函数。
是一个形参为void *,返回为void *类型的函数
如:
void * led1_task(void *args)
void *args形参用于线程创建时的传参。在下面会说到
4、void *arg --> 线程传入参数
线程传入参数,是void *
形式,没有限定参数传入的类型。既可以是int、char等, 也可以传入一个结构体,最终会在回调函数的形参void *arg
中重新提取。
struct args {
char name[10];
int fd;
};
无论变量类型是什么,传入的都是参数的地址。
pthread_join()
int pthread_join(pthread_t thread, void **retval);
用于等待一个线程结束,并获取其返回值。
比如创建了两个线程,pthread1、pthread2
在线程2中可以等待线程1结束,在运行。
void **retval;
ret = pthread_join(thread_id2, retval);
pthread_exit()
pthread_exit()
用于线程任务退出。当线程任务运行完成,需要调用该函数将线程退出。
void pthread_exit(void *retval);
可以看到线程退出的时候,可以返回一个void *类型数值retval
。 同样也是返回的一个地址。
比如可以返回一个字符串
void * pthread_task(void *args)
{
pthread_exit("task finish");
}
结合pthread_join()
函数使用,等待pthread_task结束后, 调用pthread_join()
的程序,就可以获取到字符串task finish
。
代码实现
创建测试代码文件,命名为test.c
,将编写一个带有两个线程的代码, 分别测试整型与结构体传参。
#include <stdio.h>
#include <pthread.h> //引入pthread头文件
#define MAX_THREAD 1024
int thread_num = 0; //定义变量,记录当前有多少个线程。
pthread_t thread_list[MAX_THREAD]; //定义线程句柄数组,最多可以用于创建MAX_THREAD个线程
struct args_t { //自定义一个参数结构体
char name[10];
int number;
};
static void * thread_task1(void *args) // 线程1,用于测试整型传参
{
int number = *(int *)args; // 提取整型传参
printf("%s: get number = %d\n", __func__, number);
pthread_exit(0); //退出线程
}
static void *thread_task2(void *args) // 线程2,用于测试结构体传参
{
struct args_t arg = *(struct args_t *)args; //提取结构体传参
printf("%s: arg.name = %s\n, arg.number = %d\n", __func__, arg.name, arg.number);
pthread_exit(0); //退出线程
}
static void thread_create_task1(void){
int number = 100;
int ret = 0;
ret = pthread_create( &thread_list[thread_num], //设置线程句柄存储位置
NULL, // attr使用默认值
(void *)&thread_task1, //线程1回调函数
(void *)&number); // 传入整型参数number
if(ret != 0){
printf("%s create failed!\n", __func__);
return ;
}
thread_num++; //线程创建成功,记录加1.
}
static void thread_create_task2(void){
struct args_t arg = {
.name = "pthread",
.number = 1000
};
int ret = 0;
ret = pthread_create( &thread_list[thread_num], //设置线程句柄存储位置
NULL, // attr使用默认值
(void *)&thread_task2, //线程2回调函数
(void *)&arg); // 传入结构体参数arg
if(ret != 0){
printf("%s create failed!\n", __func__);
return ;
}
thread_num++; //线程创建成功,记录加1.
}
int main(int argc, char **argv)
{
thread_num = 0;
thread_create_task1();
thread_create_task2();
while(1);
return 0;
}
代码编译
使用man查看pthread相关的函数时,会提醒我们编译需要加上-l pthread
,即需要链接pthread库编译。
SYNOPSIS
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
Compile and link with -pthread. # 提醒需要添加链接编译
所以编译上述代码的命令如下:
gcc test.c -o a.out -l pthread
运行a.out测试结果
./a.out
thread_task1: get number = 100
thread_task2: arg.name = pthread, arg.number = 1000
可以看到两个线程都成功创建,并获取到了对应的参数。
总结
-
可以通过man手册来查询pthread_create,进而切入线程创建过程。
-
多线程的简单使用,需要准备4个参数,线程编号, 线程属性attr,线程回调函数,线程参数,一般attr参数可以设置为NULL。
-
理解线程传参是如何进行的,实际上传入的是一个(void *)类型的地址值,所以可以传递int变量、结构体,甚至是其他更复杂的变量。
-
实际开发过程中,出现过局部变量作为传入参数,会出现线程获取不到的问题,但后续代码测试正常。之前猜测原因是局部变量销毁时,线程任务还没开始获取传入参数,导致再去获取局部变量的地址,内容已经改变。保留在这里!