pthread库,线程创建/查看/等待/分离,线程终止的多种方式,pthread_cancel(等待一个被取消的线程),线程id的本质(pthread_t类型,pid_t类型),__thread

目录

线程操作

引入 -- pthread库

-lpthread

pthread_create() -- 创建线程

函数原型

thread

attr

start_routine

arg 

返回值

示例 

查看线程

ps -aL

LWP

线程等待

引入

pthread_join 

函数原型

thread

retval

返回值​编辑

示例

验证阻塞等待

线程退出

return 

exit()

终止线程 与 终止进程的区别

发生异常 

pthread_exit()

函数原型

reval 

示例 

pthread_cancel()

函数原型 

 示例

示例 -- 等待一个已经取消的进程 

原因

PTHREAD_ CANCELED

pthread_detach -- 分离线程

引入

介绍 

函数原型

返回值

线程id

引入

pthread_t类型 

引入

pthread_self()

函数原型

示例 

pthread_t类型的线程id本质

引入

地址空间分布图

本质

pid_t类型

gettid()

函数原型

介绍

如何调用gettid

syscall()

 SYS_gettid

示例 

__thread -- 局部存储

引入

示例

​编辑

介绍 


线程操作

引入 -- pthread库

  • 我们前面说过,linux中只有轻量化进程,而没有真正的线程
  • 所以linux只能提供操作轻量化进程的接口
  • 但是用户并不了解linux的特性,他们只知道应该有单独操作线程的接口
  • 所以为了满足用户需要,就在用户层封装出了多线程操作的函数库 -- pthread库,也被叫做原生线程库
  • 我们在使用时,需要带上头文件<pthread.h>

-lpthread

  • 因为不是所有的程序都需要用到多线程操作的库
  • 所以编译器默认情况下不会自动包含pthread库
  • 如果我们需要用的话,需要手动让编译器知道我们使用了这个库的函数
  • 由于这是linux提供的库,所以链接器肯定可以找到这个库 

pthread_create() -- 创建线程

函数原型

thread

是一个输出型参数,用于保存线程id

其中,pthread_t类型是线程id的类型

attr

用于指定新线程的属性的指针

我们不用管这个,传入nullptr使用默认属性即可

start_routine

一个函数指针,该函数作为该线程要执行的任务

参数和返回值都是void*

  • 参数 -- 系统自动将arg参数传入进去
  • 返回值 -- 返回给主线程想要返回的信息(由于类型是void*,所以想传什么类型的信息都可以)
arg 

作为start_routine的参数,传入进去

也就是说,我们创建出的新线程,可以使用arg这个变量

返回值
  • 如果创建成功,返回0
  • 失败,返回错误码,并且线程id是未定义的 (pthreads函数出错时不会设置全局变量errno,而是直接返回错误码)

示例 

#include <iostream>
#include <string>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>

using namespace std;

void *func(void *args)
{
    string arr = (char *)args;
    while (true)
    {
        cout << "im " << arr << " : " << getpid() << endl;
        sleep(1);
    }
}
void test1()
{
    pthread_t tid;
    char arr[64];
    for(int i=0;i<2;++i){
        snprintf(arr,sizeof(arr),"%s %d","thread",i+1);
        pthread_create(&tid, nullptr, func, (void *)arr);
        sleep(1);
    }
    while (true)
    {
        cout << "im main thread  : " << getpid() << endl;
        sleep(3);
    }
}

我们创建了两个线程,都往显示器中打印自己的pid

  • 会发现,他们的pid都是一样的
  • 并且存在打印格式乱了的情况,说明他们在互相争抢资源(也就是说,并不存在访问控制,具体谁先执行由调度器决定)

查看线程

如果使用平时的ps -axj来显示进程,会发现只出现了一个,但我们应该有三个线程的:

ps -aL

所以,我们需要用新的选项来查看:

  • 他们的pid都是一样的,但lwp有所不同(所以实际上,cpu调度时看的是lwp编号)
  • 其中,有一个线程的pid和lwp是一样的,它是我们的主线程
  • (对于之前学习的进程概念来说,它只有一个线程,是主线程
  • 所以[进程操作的接口使用pid]和今天学习的新内容并不矛盾,因为那个线程的pid=lwp)
LWP
  • LWP(Lightweight Process,轻量级进程)
  • 也就是说,显示的那个LWP其实就代表了linux中轻量级进程的编号

线程等待

引入

  • 和父子进程类似,线程也需要被主线程等待,不然也会导致内存泄漏
  • 其中,父子进程的内存泄漏问题,我们是可以观察到的 -- 子进程的状态变成Z状态
  • 而线程导致的问题,我们无法查看,因为线程只是作为进程的分支,我们只能看到整个进程的状态
  • 所以,内存泄漏问题我们只要知道它存在就行

pthread_join 

只能阻塞式等待

函数原型

thread

要等待线程的id

retval
  • 输出型参数
  • 用于获取线程结束时的返回值 (对,没错,就是线程任务函数的返回值)
  • 返回值是void*类型,所以获取它就要用void**类型
  • 使用该变量,强转类型即可
返回值
  • 失败时返回错误码
  • 可以通过strerror() / perror()来获取错误信息
示例

当副线程先行退出时:

void *func2(void *args) // 5s后退出
{
    int count = 0;
    while (true)
    {
        cout << "im new thread " << endl;
        sleep(1);
        ++count;
        if (count == 4)
        {
            break;
        }
    }
    cout << "new thread quit" << endl;
    char *arr = new char[20];
    strcpy(arr, "im dead");
 // 注意,返回的信息最好是全局变量/动态申请的资源,否则在其他线程使用该变量时,该线程已经退出了,属于野指针
    return (void *)arr;
}
void test2()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, func2, nullptr);
    int count = 0;
    while (true)
    {
        cout << "im main thread  : " << getpid() << endl;
        sleep(2);
        count++;
        if (count == 3)
        {
            break;
        }
    }
    void *ret = nullptr;
    pthread_join(tid, &ret);
    cout << "join success , the message : " << (char *)ret << endl;
    delete[] (char*)ret;
    cout << "main quit " << endl;
}

可以看到,我们成功等待,且获取到线程的返回值:

验证阻塞等待

如果主线程进行等待时,副线程还未退出:

void test2()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, func2, nullptr);
    int count = 0;
    void *retval = nullptr;  //验证阻塞等待
    int ret = pthread_join(tid, &retval);
    cout << "join " << strerror(ret) << ", the message : " << (char *)retval << endl;
    while (true)
    {
        cout << "im main thread  : " << getpid() << endl;
        sleep(2);
        count++;
        if (count == 3)
        {
            break;
        }
    }
    delete[] (char*)retval;
    cout << "main quit " << endl;
}

会看到,主线程并未执行join后面的打印语句,而是阻塞等待副线程的退出:

线程退出

return 

线程执行函数中,return语句就代表了该线程的结束

exit()

如果在线程中使用exit():

void *func2(void *args) // 5s后退出
{
    int count = 0;
    while (true)
    {
        cout << "im new thread " << endl;
        sleep(1);
        ++count;
        if (count == 4)
        {
            break;
        }
    }
    cout << "new thread quit" << endl;
    exit(1);
    return (void *)"im dead";
}

无论主线程在干什么,都会直接被终止:

因为exit终止的是整个进程,所以进程中的所有线程都会被终止

终止线程 与 终止进程的区别

当我们终止进程时,所有线程都会退出

  • 其实我们终止的是主线程,我们看似使用的是进程pid,实际上使用的是主线程的lwp
  • 因为线程需要使用进程(也就是主线程)的资源,主线程都退了,资源哪里来呢?
  • 线程们自然也就退出了

发生异常 

如果线程中发生了异常,无论是哪个线程,都会让整个进程直接退出:

void *func2(void *args) // 5s后退出
{
    int count = 0;
    int a = 1;
    a /= 0;
    char *arr = new char[20];
    strcpy(arr, "im dead"); // 注意,返回的信息最好是全局变量/动态申请的资源,否则在其他线程使用该变量时,该线程已经退出了,属于野指针
    while (true)
    {
        cout << "im new thread " << endl;
        sleep(1);
        ++count;
        if (count == 4)
        {
            break;
        }
    }
    cout << "new thread quit" << endl;
    // exit(1);
    return (void *)arr;
}

可以看到,整个进程都因为异常退出了:

  • 说明,无论是哪个线程引起的,信号是发给整个进程的,都会让整个进程退出
  • 这也就是为什么线程的健壮性比较差,因为线程的异常会影响到整个进程

pthread_exit()

这是库提供的线程退出接口

函数原型

reval 
  • 用于保存退出信息 (和线程执行的函数的返回值有同样的作用)
  • 可以被主线程中pthread_join函数的输出型参数获取
示例 
void *func2(void *args) // 5s后退出
{
    int count = 0;
    char *arr = new char[20];
    strcpy(arr, "im dead"); // 注意,返回的信息最好是全局变量/动态申请的资源,否则在其他线程使用该变量时,该线程已经退出了,属于野指针
    pthread_exit((void *)arr); //终止线程,并返回错误信息
    while (true)
    {
        cout << "im new thread " << endl;
        sleep(1);
        ++count;
        if (count == 4)
        {
            break;
        }
    }
    cout << "new thread quit" << endl;
    return (void *)arr;
}
void test2()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, func2, nullptr);
    int count = 0;
    void *retval = nullptr; // 验证阻塞等待
    int ret = pthread_join(tid, &retval); 
    cout << "join " << strerror(ret) << ", the message : " << (const char *)retval << endl;
    while (true)
    {
        cout << "im main thread  : " << getpid() << endl;
        sleep(2);
        count++;
        if (count == 3)
        {
            break;
        }
    }

    cout << "main quit " << endl;
}

可以看到,我们同样可以通过retval获取返回信息:

pthread_cancel()

  • pthread_exit()用于结束自身的执行
  • 而cancel用于在外部控制下结束线程,例如在主线程中请求取消某个工作线程
函数原型 

只有一个参数,用于指定要取消的线程

 示例
void *func3(void *args)
{
    while (true)
    {
        cout << "im new thread " << endl;
        sleep(1);
    }
    cout << "new thread quit" << endl;
    return nullptr;
}
void test3()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, func3, nullptr);
    int count = 0;

    sleep(2);

    pthread_cancel(tid);
    cout << "cancel success " << endl;

    while (true)
    {
        cout << "im main thread  : " << getpid() << endl;
        sleep(1);
        count++;
        if (count == 3)
        {
            break;
        }
    }
    cout << "main quit " << endl;
}

可以看到,我们成功取消了创建的线程:

示例 -- 等待一个已经取消的进程 
void *func3(void *args) // 验证取消
{
    char *arr = new char[20];
    strcpy(arr, "im dead");
    while (true)
    {
        cout << "im new thread " << endl;
        sleep(1);
    }
    cout << "new thread quit" << endl;
    return (void *)arr;
}
void test3()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, func3, nullptr);
    int count = 0;
    while (true)
    {
        cout << "im main thread  : " << getpid() << endl;
        sleep(2);
        count++;
        if (count == 3)
        {
            break;
        }
    }

    pthread_cancel(tid);
    cout << "cancel success " << endl;

    void *retval = nullptr; // 验证阻塞等待
    int ret = pthread_join(tid, &retval);
    cout << "join " << strerror(ret) << ", the message : " << (char *)retval << endl;
   
    cout << "main quit " << endl;
}

会发现,我们的进程因为越界访问退出了:

为什么呢?

  • 我们成功取消了,说明问题出现在join之后
  • 那应该就是我们打印出了问题
原因

实际上,我们如果等待一个已经取消的线程,join函数会立即返回,并且返回PTHREAD_ CANCELED

  • 也就是说,join函数并不能获取返回信息,它的retval是固定的
  • 自然也就不能被我们强转为char*后使用了
PTHREAD_ CANCELED
  • 是个宏,从-1强转得到

pthread_detach -- 分离线程

引入

  • 已经退出的线程,其空间并没有被释放,仍然在占用进程的资源
  • 那么当我们创建新的线程时,就无法复用退出线程的地址空间
  • 所以我们需要等待线程,以清理资源
  • 但我们有时候并不想等待怎么办,毕竟线程只能阻塞式等待
  • 所以我们就引入了分离线程

介绍 

  • 将指定的一个线程标记为"可分离的"
  • 也就是说,线程退出时会自动释放资源,而不需要等待其他线程调用pthread_join函数

函数原型

返回值

线程id

引入

  • 前面一直在说线程id,但我们并没有特意看过它的值
  • 所以,我们接下来就来介绍线程id

pthread_t类型 

引入

线程id的类型(是一个抽象的线程标识符类型)

  • 为什么这么说呢?原因将在后面介绍
  • 它实际上只是普通类型进行包装了下:

pthread_self()

函数原型

用于获取当前线程的线程id(Thread ID)

线程id是一个唯一标识符,用于区分不同的线程

示例 
void *func4(void *args)
{
    sleep(1);
    pthread_t tid2 = pthread_self();
    cout << "my tid : " << tid2 << endl;
    sleep(5);
    return nullptr;
}
void test4()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, func4, nullptr);
    cout << "tid : " << tid << endl;
    pthread_join(tid, nullptr);
}

会发现pthread_self() 和 pthread_create接口为我们提供的线程id是一样的,且都是pthread_t类型:

  • 都是很大的数字 
  • 但是,我们之前见到的编号都挺小的(文件fd,进程id),他们完全达不到这样的量级

为什么会这样呢?

  • 其实这个并不是传统意义上的编号,而是从地址转换来的数字
  • 转换为地址后:
  • 那为什么会是地址呢?这就要说到线程在地址空间的分布了

pthread_t类型的线程id本质

引入

前面我们说了,线程有自己的独立栈,那这个独立栈结构是如何实现的呢?

  • 如果直接使用进程的栈:
  • 因为会进行上下文切换,所以线程之间不会互相影响
  • 似乎可以
  • 但要是计算机是多核的,多个线程的压栈操作就会混乱起来了

  • 如果将进程的栈拆分成多个栈区域:
  • 那就会让os察觉到有线程的存在(这和我们在用户层封装出线程矛盾了)
  • 所以不行

  • 既然线程是用户层的概念,那栈结构也就可以在用户层实现:
  • 也就是下面的分布图了
地址空间分布图
  • 首先我们明确,我们使用pthread库,就需要将它映射到进程的地址空间中
  • 而我们一般使用的是动态链接
  • 也就是说,pthread库被映射到我们的共享区(mmap(memory map)区域)

除此之外,这个动态库不仅要给我们提供接口,还会给我们提供一些资源

  • 比如上面说要在用户层实现的线程独立栈结构
  • 以及其他的资源
  • 如图:

本质
  • 所以,其实我们是将地址空间中,该线程所在空间的起始地址作为线程id
  • 也就是说,pthread_ create函数第一个参数和pthread_ self()获取到的都是这个进程id
  • 线程库的后续操作,就是根据该线程id来操作线程的

pid_t类型

gettid()

函数原型

注意,这里返回的id是pid_t类型

介绍

它也可以用来获取线程id,但这个id是真实id(也就是轻量级进程的id)

  • (是不是和getpid()很像,它用来获取进程id)
  • 该函数并不是标准 POSIX 函数,而是 Linux 特有的(因为轻量级进程是linux特有)
  • 但是我们似乎不能直接调用它:
  • 似乎意思是这个系统调用并不包括在gilbc(GNU C Library)库中(?也许吧)
如何调用gettid
  • 所以我们可以通过其他方式调用它:
  • pthread_t tid = syscall(SYS_gettid);
  • 通过syscall这个系统调用接口,直接通过宏定义,来调用这个接口
syscall()
  • 用于在 Linux 和一些其他类 Unix 操作系统上执行底层的系统调用
 SYS_gettid
  • 追根溯源后,这个宏其实是个整数,应该是内核中使用的某种编号
示例 
void *func4(void *args)
{
    sleep(1);
    pthread_t tid2 = pthread_self();    
    pthread_t tid1 = syscall(SYS_gettid);
    cout << "my tid : " << tid1 << endl;
    sleep(5);
    return nullptr;
}
void test4()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, func4, nullptr);
    pthread_join(tid, nullptr);
}

可以看到,我们用gettid拿到的id和lwp编号是一样的:

__thread -- 局部存储

引入

我们的线程某些资源是共享的,比如全局区

但如果有时候,我们想要让某个全局变量只是在线程内作为全局变量呢?

  • 这样就要用到局部存储的概念了:
  • (这是地址空间中的内容,pthread库会为每个线程保存其局部数据)

示例

int g_val = 0;
void* func6(void* args){
    while (true)
    {
        sleep(1);
        ++g_val;
        cout << "im new : "<< g_val << " " << &g_val << endl;
    }
}
void test6()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, func6, nullptr);

    while (true)
    {
        sleep(2);
        ++g_val;
        cout << "im main : "<< g_val << " " << &g_val << endl;
    }
}

这样,我们任何线程对其进行修改,都会影响其他线程:

如果我们不想这样,就可以使用__thread修饰该全局变量:

会看到,现在这两个进程就各自拥有自己的g_val

介绍 

  • 用于声明线程局部变量
  • 线程局部变量是每个线程独有的变量,每个线程都有自己的一份,互不干扰
  • 在多线程编程中,使用线程局部变量可以避免对全局变量的竞争条件,提高程序的并发性
  • 线程局部变量的生命周期与线程的生命周期相同
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
下面是一个使用pthread_cancel取消线程并避免资源泄露的完整示例: ```c #include <stdio.h> #include <stdlib.h> #include <pthread.h> #include <unistd.h> void* thread_func(void* arg) { int i = 0; pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS, NULL); pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL); while(1) { printf("thread_func: i=%d\n", i++); pthread_testcancel(); sleep(1); } return NULL; } int main() { pthread_t thread; int ret; ret = pthread_create(&thread, NULL, thread_func, NULL); if(ret != 0) { printf("pthread_create failed\n"); exit(-1); } sleep(5); ret = pthread_cancel(thread); if(ret != 0) { printf("pthread_cancel failed\n"); exit(-1); } ret = pthread_join(thread, NULL); if(ret != 0) { printf("pthread_join failed\n"); exit(-1); } printf("main thread exit\n"); return 0; } ``` 在该示例中,新线程使用while循环不断输出计数器的值,并在取消点处调用pthread_testcancel检测是否有取消请求。新线程在启动时通过调用pthread_setcanceltype和pthread_setcancelstate函数来设置线程取消类型取消状态,以便在收到取消请求时进行清理操作。主线程在5秒后发送取消请求,通过调用pthread_cancel函数来终止线程。最后,主线程通过pthread_join函数等待线程结束,并回收线程占用的资源。 需要注意的是,在使用pthread_cancel函数取消线程时,可能会出现资源泄露和程序异常终止等问题。为了避免这种情况,必须在适当的位置设置取消点,并在取消点处进行清理操作。在本例中,新线程取消点为pthread_testcancel函数,通过在该函数中调用pthread_exit函数来正常结束线程,避免了资源泄露和程序异常终止等问题。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值