Linux线程--线程创建、等待及退出

1. pthread_create

功能

创建一个新的线程。

原型
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg);
参数
  • thread: 指向pthread_t类型变量的指针,用于存储新创建线程的标识符。
  • attr: 线程属性,通常为NULL表示默认属性。如果需要指定线程属性,可以使用pthread_attr_t结构。
  • start_routine: 新线程的入口函数,即新线程将从这个函数开始执行。线程开始执行的函数类型为void *(*start_routine)(void *),它接受一个void *参数并返回一个void *值。
  • arg: 传递给新线程入口函数的参数。
返回值
  • 成功:返回0。
  • 失败:返回错误代码。
pthread_t t1;
int param = 100;
int ret = pthread_create(&t1, NULL, func1, (void *)&param);
if (ret != 0) {
    printf("Error creating thread: %d\n", ret);
}

2. pthread_self

功能:获取调用线程的线程标识符。
原型
pthread_t pthread_self(void);
参数:无。
返回值:返回调用线程的线程标识符。

用途: 常用于调试、日志记录以及需要区分线程的情景。

pthread_t tid = pthread_self();
printf("Current thread ID: %ld\n", (unsigned long)tid);

3. pthread_join

功能:等待指定的线程终止。pthread_join会阻塞调用它的线程,直到指定的线程结束。这对于确保所有线程在主线程退出之前完成工作非常有用。
原型
int pthread_join(pthread_t thread, void **retval);
参数
  • thread: 要等待的线程的标识符。
  • retval: 存储线程返回值的指针。如果不需要获取线程的返回值,可以传递NULL。如果需要获取线程的返回值,retval参数不能为NULL,应该传递一个指向指针的指针。
返回值
  • 成功:返回0。
  • 失败:返回错误代码。
示例
pthread_t t1;
// 创建线程 t1
pthread_create(&t1, NULL, func1, (void *)&param);

// 等待线程 t1 结束
pthread_join(t1, NULL);

4. pthread_exit

功能:终止调用
它的线程。该函数用于显式地终止线程,通常用于线程需要提前结束的情况。

自动清理: 调用pthread_exit的线程会自动释放分配给它的资源。

原型
void pthread_exit(void *retval);
参数
  • retval: 线程的返回值,传递给pthread_joinretval参数。
返回值

无,因为此函数不会返回到调用线程。

示例
void *func1(void *arg) {
    printf("Thread is exiting.\n");
    pthread_exit(NULL);
}

示例代码

结合上述函数,这里做个演示

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

void * func1(void *arg)
{
        //关于为什么要使用static,请看下面的解释
        static char *p = "t1 is run"; 
        //为什么转换成unsigned long,请看下面的解释
        printf("func1: %ld\n", (unsigned long)pthread_self());
        //退出当前线程,并返回一个指针给pthread_join
        pthread_exit((void *)p);
}

int main()
{
        //定义一个线程标识符
        pthread_t t1;
        int data = 100;
        char *pret = NULL;
        int ret = pthread_create(&t1, NULL, func1, (void *)&data);
        if(ret == 0){
                printf("successfully creat a pthread!\n");
        }
        else{
                printf("faild to creat a pthread\n");
        }

        printf("I am main: %ld\n", (unsigned long)pthread_self());
        pthread_join(t1, (void **)&pret);
        printf("main: t1 quit: %s\n", pret);
        return 0;
}

问题1:为什么要使用static静态变量?

非静态变量的行为

考虑一个没有使用 static 的情况:

#include <stdio.h>

void *func1(void *arg) {
    char p[] = "t1 is run";  // 非静态局部变量
    return (void *)p;
}

int main() {
    void *result = func1(NULL);
    printf("Result: %s\n", (char *)result);  // 未定义行为,可能导致错误
    return 0;
}

在这个例子中:

  • char p[] 是一个局部数组变量,存储在栈中。
  • func1 返回时,p 指向的内存可能已无效,因为函数返回后栈帧被销毁。
  • 访问返回的指针会导致未定义行为,可能会崩溃或输出垃圾值。

         C 语言标准中规定,对于局部变量(自动变量),当函数返回时,它们的内存会被释放。如果你返回一个指向局部变量的指针,这个指针会指向无效内存。虽然在某些情况下,内存内容可能未被立即覆盖或销毁,因此可以“侥幸”地正确打印,但这种做法是不安全的。

使用静态变量

现在使用 static 关键字:

#include <stdio.h>

void *func1(void *arg) {
    static char p[] = "t1 is run";  // 静态局部变量
    return (void *)p;
}

int main() {
    void *result = func1(NULL);
    printf("Result: %s\n", (char *)result);  // 正常输出 "t1 is run"
    return 0;
}

在这个例子中:

  • static char p[] 是一个静态变量,存储在静态数据区而不是栈中。
  • 变量 p 在第一次调用时初始化,并在程序的整个运行期间保留其值和内存地址。
  • func1 返回时,返回的指针 p 始终有效,可以安全地访问。

关键点总结

  • 持久性:静态变量在整个程序运行期间存在,不会因函数返回而销毁。
  • 唯一初始化:静态变量只在第一次声明时初始化,后续调用不会重新初始化。
  • 作用域限制:静态局部变量的作用域限制在函数内部,无法被外部访问。

使用静态变量的场景

静态变量适用于以下场景:

  1. 返回局部数据的地址:当函数需要返回局部数据的地址时,可以使用静态变量确保数据在函数返回后依然有效。
  2. 保持函数调用间的状态:当函数需要在多次调用之间保持某些状态时,使用静态变量可以实现状态的持久化。

 问题2:为什么将pthread_self()的返回值转换为unsigned long类型?

  1. 不透明类型pthread_t 是一个不透明类型,其实现依赖于具体的操作系统和线程库。在某些系统上,pthread_t 可能是一个整数类型,在其他系统上可能是一个指针类型。
  2. 类型大小不同:不同系统上,pthread_t 的大小可能不同(例如,32位系统和64位系统上类型大小可能不同)。

使用 unsigned long 的理由

  1. 通用格式化unsigned long 是一种标准的整数类型,在C语言中占用固定的字节数(通常是32位或64位)。这使得它在大多数系统上都可以以一种一致的方式打印。
  2. 格式化支持printf 函数支持 %lu 格式说明符来打印 unsigned long 类型,这在C语言标准库中是通用的。
  3. 避免类型问题:将 pthread_t 转换为 unsigned long 可以避免由于不同系统上 pthread_t 类型不同带来的编译和运行时问题。

问题3:关于线程函数

void *thread_func(void *arg) {
  
}

pthread_create(&t1, NULL, thread_func, (void *)&param);
  • 线程函数: thread_func 是符合 void *(*)(void *) 类型的函数,它接受一个 void * 类型的参数,并返回一个 void * 类型的值。

  • 创建线程: pthread_create(&t1, NULL, thread_func, (void *)&param);

    • 第一个参数 &t1 是指向线程标识符的指针。
    • 第二个参数 NULL 表示使用默认线程属性。
    • 第三个参数 thread_func 是线程函数的指针。
    • 第四个参数 (void *)&param 是传递给线程函数的参数,强制转换为 void * 类型。为什么要强制转换呢,接着往下看问题4,就明白了。

 问题4:pthread_create的第三个参数类型void *(*start_routine)(void *),怎么去理解?

我们可以拆分来理解:

  1. void *: 表示函数的返回类型是一个void *指针。线程函数可以返回任意类型的数据,通过指针的方式返回。

  2. (*start_routine): 表示这是一个指向函数的指针,指向的函数名为start_routine

  3. (void *): 表示这个函数接受一个void *类型的参数。这个参数可以是任意类型的数据,通过指针的方式传递。

void *(*start_routine)(void *) 表示一个指向函数的指针(*start_routine),这个函数接受一个void *类型的参数(void *),并返回一个void *类型的值。也就是说对应的函数要为指针函数。

问题5:关于多次使用到void *

void * 类型

void * 是一种通用指针类型,可以指向任意类型的数据。它不携带任何类型信息,这意味着你可以将任何类型的数据指针转换为 void *,并且可以从 void * 类型转换回原始类型的指针。

为什么使用 void *

在需要传递不确定类型的数据时,void * 非常有用。例如,在多线程编程中,你可能希望线程函数能够处理不同类型的参数,而 void * 允许你传递任意类型的数据指针。

问题6:pthread_join的第二个参数

pthread_join 内部如何工作

pthread_join 的实现中,内部逻辑大概如下:

  1. 等待线程结束:主线程会阻塞等待子线程 t1 结束。
  2. 获取线程的返回值:当线程调用 pthread_exit((void *)p) 退出时,返回的指针 p 会被传递回 pthread_join
  3. 存储返回值pthread_join 将返回的指针 p 存储在 retval 所指向的地址中。

        在 pthread_join 调用中传递 &pret 的目的是让线程库在 pthread_join 内部通过这个指针来修改 pret 的值(意思是把pret的地址穿给线程库,线程库知道了pret在哪,然后才能找到pret去修改pret的值),使 pret 指向线程函数返回的地址。意思是把pret的地址穿给线程库,线程库知道了pret在哪,然后才能找到pret去修改pret的值。

  • 26
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值