21.Linux 线程库的使用与封装

发布于:2025-03-13 ⋅ 阅读:(16) ⋅ 点赞:(0)

在linux内核中并没有线程的概念,只有轻量级进程LWP的概念,linux下的线程都是是由LWP进行模拟实现的。因此linux操作系统中不会提供线程的相关接口,只会提供轻量级线程的接口(如vfork,clone等)。但是在我们的学习过程中实际上学到的都是线程的概念,故而linux的设计者在用户和操作系统之间将轻量级进程的的系统调用进行了封装成库并提供给了用户,这样的线程我们称之为用户级线程库

接下来我们介绍一下linux系统中线程库的使用与封装。

一、线程库的使用

1.1 POSIX线程库

在linux中,与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以pthread_ 打头的。要想使用这些函数库,要通过引入头文件 <pthread.h>,而在链接这些线程函数库时要使用编译器命令的-lpthread选项。

1.2 线程库的接口

1.2.1 线程创建

int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void* (*start_routine)(void*), void *arg);
//功能:创建一个新的线程
//参数:
	//thread:输出型参数,用以返回线程ID
	//attr:设置线程的属性,attr为NULL表示使⽤默认属性
	//start_routine:是个函数地址,即线程启动后要执⾏的函数,它的返回值也可以是任意类型(整型,字符型,对象)
	//arg:传给线程启动函数的参数,可以是任意类型(整型,字符型,对象……)
//返回值:成功返回0,失败返回错误码

示例:

#include<iostream>
#include<pthread.h>
#include<unistd.h>
using namespace std;

void* run(void* arg)
{
    while(true)
    {
        cout<<"new thread :"<<(char* )arg<<endl;
        sleep(1);
    }
}

int main()
{
    pthread_t thread1;
    pthread_create(&thread1,nullptr,run,(void*)"new thread1");
    pthread_t thread2;
    pthread_create(&thread2,nullptr,run,(void*)"new thread2");
    while(true)
    {
        cout<<"main thread"<<endl;
        sleep(1);
    }
    return 0;
}

注意:
• 新线程和主线程的运行顺序是不确定的。
• 多线程的调度时间是基于对进程的时间片进行瓜分。
• 多个线程向同一个文件(这里是显示器)进行写入时,在不加保护的情况下是会发生重入的,也就会产生数据不一致问题。
• 线程是共享进程地址空间的。
• 线程出现异常会导致当前进程的其他线程全部崩溃。

1.2.2 线程终止

和进程一样,线程创建之后也是要被等待和回收的!

如果需要只终止某个线程而不终止整个进程的话,有三种方法:
• 线程执行函数return。这种方法对主线程不适用,main函数return相当于调用exit。
• 线程可以调用pthread_exit终止自己。

void pthread_exit(void *value_ptr);
//功能:线程终⽌
//参数:
	//value_ptr:线程退出时的返回值。

需要注意pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配。因为当其它线程得到这个返回指针时线程函数已经退出了。

• 一个线程可以调用pthread_cancel终止同一进程中的另一个线程。

int pthread_cancel(pthread_t thread);
//功能:取消一个执⾏中的线程
//参数:
	//thread:要取消的线程ID
//返回值:成功返回0,失败返回错误码

1.2.3 线程等待

为什么需要线程等待?
因为已经退出的线程其空间没有被释放,仍然在进程的地址空间内。而创建新的线程不会复用刚才退出线程的地址空间。

int pthread_join(pthread_t thread, void **value_ptr);
//功能:等待线程结束
//参数:
	//thread:线程ID
	//value_ptr:用以保存线程的返回值的地址
//返回值:成功返回0;失败返回错误码

调用该函数的线程将挂起等待,直到id为thread的线程终止。

thread线程以不同的方法终止,通过pthread_join得到的终止状态是不同的:
• 如果thread线程通过return返回,那么value_ptr所指向的单元里存放的是thread线程函数的返回值。
• 如果thread线程被别的线程调用pthread_cancel异常终掉,value_ptr所指向的单元里存放的是常数PTHREAD_CANCELED((void*)-1)。
• 如果thread线程是自己调用pthread_exit终止的,value_ptr所指向的单元存放的是传给pthread_exit的参数。
• 如果对thread线程的终止状态不感兴趣,则可以传NULL给value_ptr参数。

在这里插入图片描述
示例:

#include<iostream>
#include<pthread.h>
#include<unistd.h>
using namespace std;

void* run1(void* arg)
{
    int count=5;
    while(count--)
    {
        cout<<"new thread :"<<(char* )arg<<endl;
        sleep(1);
    }
    pthread_exit((void*)100);
    cout<<"退出成功!"<<endl;

}

void* run2(void* arg)
{
    while(true)
    {
        cout<<"new thread :"<<(char* )arg<<endl;
        sleep(1);
    }
}
int main()
{
    //创建线程1,线程2
    pthread_t thread1;
    pthread_create(&thread1,nullptr,run1,(void*)"new thread1");
    pthread_t thread2;
    pthread_create(&thread2,nullptr,run2,(void*)"new thread2");
    sleep(3);
    //取消线程2
    pthread_cancel(thread2);
    cout<<"取消成功!"<<endl;

    //线程等待
    void *ret1,*ret2;
    pthread_join(thread1,&ret1);
    pthread_join(thread2,&ret2);
    cout<<"等待成功!"<<endl;
    cout<<"ret1 :"<<(long long)ret1<<";ret2 :"<<(long long)ret2<<endl;

    return 0;
}
caryon@VM-24-10-ubuntu:~/linux/thread$ ./thread 
new thread :new thread1
new thread :new thread2
new thread :new thread1
new thread :new thread2
new thread :new thread1
new thread :new thread2
取消成功!
new thread :new thread1
new thread :new thread1
等待成功!
ret1 :100;ret2 :-1

在多执行流的情况下,主执行流往往是最后退出的!

1.2.4 线程分离

默认情况下,新创建的线程是joinable的,线程退出后需要对其进型pthread_join操作,否则无法释放资源从而造成系统泄漏。如果不关心线程的返回值的话join就是一种负担。这个时候我们可以告诉系统,当线程退出时,自动释放线程资源。

int pthread_detach(pthread_t thread);
//功能:进行线程分离
//参数:
	//thread:被分离的线程ID
//返回值:成功返回0;失败返回错误码

可以是线程组内其他线程对目标线程进型分离,也可以是线程自己分离。joinable和分离是冲突的,一个线程不能既是joinable又是分离的。

#include <iostream>
#include <unistd.h>
#include <pthread.h>
using namespace std;

void *thread_run(void *arg)
{
    printf("%s\n", (char *)arg);
    return NULL;
}
int main(void)
{
    //创建线程
    pthread_t tid;
    pthread_create(&tid, NULL, thread_run, (void*)"thread1 run...") ;
    //分离线程
    pthread_detach(tid);

    int ret = 0;
    sleep(1); 
    if (pthread_join(tid, NULL) == 0)
    {
        printf("pthread wait success\n");
        ret = 0;
    }
    else
    {
        printf("pthread wait failed\n");
        ret = 1;
    }
    return ret;
}
caryon@VM-24-10-ubuntu:~/linux/thread$ ./thread 
thread1 run...
pthread wait failed

二、线程id与进程空间布局

在线程创建、终止、等待时我们都有注意到pthread_t thread(线程id)这个参数的存在,这个id表示的是什么意思呢?

#include<iostream>
#include<pthread.h>
using namespace std;

void* run(void* args)
{
    while(true);

    return nullptr;    
}
int main()
{
    pthread_t tid;
    pthread_create(&tid,nullptr,run,(void*)"newthread");
    printf("0x%lx\n",tid);
    return 0;
}
caryon@VM-24-10-ubuntu:~/linux/thread$ ./thread 
0x7fae58e006c0

我们可以看到这个id的值是一个地址,那这个地址是什么地址呀?

通过ldd查看我们所形成的可执行程序是依赖pthread库的,这个库在程序运行时会加载到内存中,进而映射到进程地址空间里被所有的线程所共享。我们又知道linux系统中只有轻量级进程,线程是使用轻量级进程模拟实现的。但是用户想要查看当前线程的属性怎么办呢?
linux内核中是存在这些属性的,但是Linux中不存在线程呀!这需要内核层与用户层进行解耦啊!故而线程的相关属性也是由pthread库进行维护的,用户可以通过pthread库来查看线程的属性。

所以这个地址就是该线程的属性所对应的进程地址空间中的地址!

我们可以使用下面这个函数获取当前线程的id:

pthread_t pthread_self(void);

在这里插入图片描述
上图就是进程地址空间中的tcb了。
struct pthread
这个结构里面一定封装了LWP。
线程栈
进程地址空间中的栈是主线程的栈,新线程的栈是通过动态申请创建的。
线程局部存储
理论上来说全部变量是所有线程所共享的,即他们所访问的全局变量的地址是相同的!但是在声明全局变量是加上__thread进行修饰(修饰的只能是内置类型)就表示给每个线程都来一份,即线程的局部性存储。

#include<iostream>
#include<pthread.h>
using namespace std;

__thread int count=100;

void* run(void* args)
{
    printf("new thread &count : %p\n",&count);

    return nullptr;    
}

int main()
{
    pthread_t tid;
    pthread_create(&tid,nullptr,run,(void*)"newthread");
    pthread_join(tid,nullptr);
    printf("main thread &count : %p\n",&count);
    return 0;
}
caryon@VM-24-10-ubuntu:~/linux/thread$ ./thread 
new thread &count : 0x71e23b2006bc
main thread &count : 0x71e23ba8e4fc

三、线程的封装

#pragma once

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

using func_t = std::function<void()>;
static int number = 1;
enum class TSTATUS
{
    NEW,
    RUNNING,
    STOP
};

class Thread
{
private:
    // 成员方法!
    static void *Routine(void *args)
    {
        Thread *t = static_cast<Thread *>(args);
        t->_status = TSTATUS::RUNNING;
        t->_func();
        return nullptr;
    }
    void EnableDetach() { _joinable = false; }
    bool IsJoinable() { return _joinable; }
public:
    Thread(func_t func) : _func(func), _status(TSTATUS::NEW), _joinable(true)
    {
        _name = "Thread-" + std::to_string(number++);
        _pid = getpid();
    }
    bool Start()
    {
        if (_status != TSTATUS::RUNNING)
        {
            int n = ::pthread_create(&_tid, nullptr, Routine, this); 
            if (n != 0)
                return false;
            return true;
        }
        return false;
    }
    bool Stop()
    {
        if (_status == TSTATUS::RUNNING)
        {
            int n = ::pthread_cancel(_tid);
            if (n != 0)
                return false;
            _status = TSTATUS::STOP;
            return true;
        }
        return false;
    }
    bool Join()
    {
        if (_joinable)
        {
            int n = ::pthread_join(_tid, nullptr);
            if (n != 0)
                return false;
            _status = TSTATUS::STOP;
            return true;
        }
        return false;
    }
    void Detach()
    {
        EnableDetach();
        pthread_detach(_tid);
    }
    ~Thread()
    {
    }
private:
    std::string _name;
    pthread_t _tid;
    pid_t _pid;
    bool _joinable; // 是否是分离的,默认不是
    func_t _func;
    TSTATUS _status;
};

网站公告

今日签到

点亮在社区的每一天
去签到