线程ID获取,处理线程函数错误码,线程函数单例
线程ID获取,处理线程函数错误码,线程函数单例
线程ID获取
pthread_self函数介绍
函数作用:获取当前线程的线程ID
头文件:#include <pthread.h>
函数原型:pthread_t pthread_self(void);
参数:无参数
返回值:返回调用该函数的线程的线程ID,返回值类型为pthread_t
注意:
如果想输出线程ID的时候应该进行强制类型转换为unsigned long
类型的
pthread_t my_id = pthread_self();
printf("Thread ID: %lu\n", (unsigned long)my_id);
线程函数错误码
回顾之前学习过的errno错误码
前面我们学习的线程相关函数,比如pthread_create
和pthread_join
在执行失败的时候都会返回一个错误码。一说到错误码我们可以想到之前在学习文件I/O部分知识的时候学习过那一块的函数在发生执行错误的时候,返回值为-1,并且会对错误码errno
进行设置,我们可以通过错误码来更精确地知道具体是什么原因导致了函数执行错误。
注意:并不是所有的系统函数在执行发生错误的时候都会设置errno,比如这里pthread_xxx
系列函数执行错误的时候通常直接返回错误码,而不是设置errno
。下面我将简单列举一下常见的Linux中执行错误会设置errno
的函数。
Linux中常见执行错误会设置errno的函数
- 系统调用:这是最常见的情况。大多数系统调用,如文件操作、进程管理、网络通信等,在出错时都会设置
errno
。 - 文件操作:
open()
、read()
、write()
、close()
等函数在操作文件时,如果遇到权限问题、文件不存在等错误,会设置errno
。 - 进程管理:
fork()
、exec()
、wait()
等函数在创建、执行、等待子进程时,如果遇到内存不足、进程不存在等错误,会设置errno
。 - 网络通信:
socket()
、bind()
、connect()
、accept()
等函数在进行网络通信时,如果遇到网络错误、地址错误等,会设置errno
。 - 标准库函数:C标准库中的一些函数也会设置
errno
,例如: - 数学函数:
sqrt()
、sin()
、cos()
等函数在参数超出定义域或计算结果无法表示时,会设置errno
。 - 字符串函数:
strlen()
、strcpy()
、strcat()
等函数在操作字符串时,如果遇到空指针、缓冲区溢出等错误,可能会设置errno
。
如何使用errno
- 包含头文件:在使用
errno
之前,需要包含<errno.h>
头文件。 - 检查返回值:大多数会设置
errno
的函数,其返回值通常为-1,表示调用失败。 - 检查errno:在函数返回-1后,可以检查
errno
的值,以确定具体的错误原因。 - 使用strerror():可以使用
strerror(errno)
函数将errno
转换为对应的错误信息字符串,方便输出和调试。或者可以简单地使用perror("打开a.txt失败");
//输出提示字符串+详细错误原因
关于如何使用errno
并不是我们这里讨论的重点,想要了解更多errno
的知识,可以查看相关资料。
pthread_xxx系列线程函数错误码
pthread
函数返回的错误码通常是直接返回错误码,而不是通过errno
来设置错误状态。
- pthread函数的错误处理:
- 返回值:大多数
pthread
函数直接通过返回值来传递错误码。如果调用成功,返回值通常是0。如果调用失败,则会返回一个对应的错误码(如EINVAL
、ENOMEM
、ESRCH
等),而不会修改errno
。 - 为什么不使用
errno
? - 线程的局部性:
errno
是线程的全局变量,而pthread
函数的错误码需要在每个线程中单独处理。多个线程同时运行时,如果都依赖errno
,会导致不同线程之间的冲突。为了避免这种问题,pthread
函数设计上选择通过返回值来传递错误信息,避免了依赖errno
的问题。 - 标准化:POSIX线程库的设计和其他POSIX API(例如文件操作、进程管理等)有所不同。其他POSIX函数通常会在失败时设置
errno
,但pthread
函数则选择通过返回值来提供错误信息,这是POSIX线程库的一种设计规范。
错误码使用代码格式
这里需要注意的是:使用错误码的时候需要用到一个变量来接收pthread_xxx
函数的返回值。这里用的不是errno
。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
void *count(void *arg)
{
printf("子线程的ID: %lu\n", pthread_self());
//pthread_detach(pthread_self());
// 子线程先退出
char *msg = "error2";
pthread_exit(msg);
}
int main()
{
pthread_t tid;
pthread_create(&tid, NULL, count, NULL);
printf("主线程的ID: %lu\n", pthread_self());
sleep(2);
// 阻塞等待接合
void *retval;
int err = pthread_join(tid, &retval);
if (err == 0)
printf("返回值: %s\n", (char *)retval);
else
printf("接合失败: %s\n", strerror(err));
}
线程函数单例
函数单例适用背景
许多时候,我们希望某个函数只被严格执行一次,这种需求在一些初始化功能模块中尤为常见。
考虑这么一种情形:
假设某程序内含多条线程,这些线程使用信号量(不管是system-V信号量组还是POSIX信号量)进行协同合作,由于信号量使用前必须进行初始化,为了使程序性能最优,我们希望线程们启动时谁跑得快谁就对信号量执行初始化的工作,且要确保初始化的工作被严格执行一遍。
在上述情形中,由于线程的并发特性,我们无法预先知晓哪条线程会对信号量进行初始化,于是就希望有一种只执行一遍的函数单例,可以被众多的并发线程放心去调用。这种机制可以用如下函数达成:
#include <pthread.h>
// 函数单例控制变量
pthread_once_t once_control = PTHREAD_ONCE_INIT;
// 函数单例启动接口
int pthread_once(pthread_once_t *once_control, void (*init_routine)(void));
pthread_once函数使用详细介绍:
函数作用:确保某个初始化函数在多线程环境下只被执行一次
函数原型:int pthread_once(pthread_once_t *once_control, void (*init_routine)(void));
参数说明:
pthread_once_t *once_control
:这是一个指向pthread_once_t
类型变量的指针。pthread_once_t
类型的变量用于控制初始化过程,必须使用PTHREAD_ONCE_INIT
宏进行初始化。void (*init_routine)(void)
:这是一个函数指针,指向需要执行的初始化函数。这个函数没有参数,也没有返回值。
工作方式:
- 当一个线程第一次调用
pthread_once
函数时,如果once_control
变量的状态表明初始化尚未完成,则该线程会执行init_routine
函数。 init_routine
函数执行完毕后,pthread_once
函数会更新once_control
变量的状态,标记初始化已完成。- 后续其他线程调用
pthread_once
函数时,如果发现once_control
变量的状态表明初始化已经完成,则不会再次执行init_routine
函数,而是直接返回。
使用场景pthread_once
函数通常用于在多线程程序中初始化一些只需要执行一次的资源,例如:
- 初始化互斥锁、条件变量、信号量等同步原语
- 加载配置文件
- 建立数据库连接
once_control变量
从这个工作模式上来看,once_control
变量可以被看作一个“信号量”,但它比普通的信号量更特殊,因为它只能被“触发”一次。系统为我们提供了pthread_once
接口,大大简化了对这个“信号量”的操作,确保初始化函数只会被执行一次。
关于once_control
变量的存储位置:once_control
变量不能定义在某个线程的栈空间中,它必须放置到线程之间的共享资源中,例如全局变量或静态变量。
这是因为:
- 共享性:
once_control
变量需要在多个线程之间共享,以便每个线程都能访问到它,并判断初始化是否已经完成。如果once_control
变量位于某个线程的栈空间中,其他线程将无法访问,就无法实现单例执行的目的。 - 生命周期:
once_control
变量的生命周期必须足够长,以确保在所有需要访问它的线程都执行完毕之前,它一直有效。如果once_control
变量位于某个线程的栈空间中,当该线程结束时,栈空间会被释放,once_control
变量也会失效,导致其他线程无法正确判断初始化状态。
总结:
once_control
变量可以看作一个特殊的“信号量”,用于控制初始化函数的单例执行。once_control
变量必须放置在线程之间的共享资源中,例如全局变量或静态变量。
pthread_once函数使用示例
#include <stdio.h>
#include <pthread.h>
// `pthread_once`控制变量,用于确保初始化函数只执行一次
pthread_once_t once_control = PTHREAD_ONCE_INIT;
// 需要执行的初始化函数
void initialize_resource() {
// 这里进行一些初始化操作,例如:
// - 初始化互斥锁、条件变量、信号量等
// - 加载配置文件
// - 建立数据库连接
// - 初始化全局变量
printf("资源初始化完成!\n");
}
// 线程函数
void* thread_function(void* arg) {
// 每个线程都会尝试调用`pthread_once`
pthread_once(&once_control, initialize_resource);
// 只有初始化完成后,线程才能继续执行
printf("线程开始工作...\n");
// ... 线程的其他工作 ...
return NULL;
}
int main() {
pthread_t threads[5];
// 创建多个线程
for (int i = 0; i < 5; ++i) {
pthread_create(&threads[i], NULL, thread_function, NULL);
}
// 等待线程结束
for (int i = 0; i < 5; ++i) {
pthread_join(threads[i], NULL);
}
return 0;
}
通过观察上面代码的运行结果,我们可以发现虽然有5个线程,但是initialize_resource()
初始化函数仅仅被调用了一次。