【Linux】多线程

目录

一、概念

(一)基本概念

(二)底层细节

1、页表表示形式

2、轻量级进程标识符

3、用户级线程ID

4、线程的私有数据及返回值问题

[5、底层 clone() 函数](#5、底层 clone() 函数)

(三)进程与线程的区别

1、进程

2、线程(轻量级进程)

(四)线程的优缺点

1、优点

2、缺点

二、线程的创建

三、线程的等待

(一)回收等待

(二)分离线程

四、线程的终止

[(一)return 终止](#(一)return 终止)

[(二)pthread_exit() 终止](#(二)pthread_exit() 终止)

[(三)pthread_cancel() 终止](#(三)pthread_cancel() 终止)

五、原生线程库的封装

(一)mythread.hpp

(二)mythread.cpp


一、概念

(一)基本概念

在Linux中,在一个程序的一个执行流称为线程,更准确的说:线程就是"一个进程内部的控制序列",在一个进程中至少有一个执行流,也就是线程。

在Linux系统中,线程在进程内容运行的本质是在进程的地址空间内运行,共享进程的资源。Windows系统中对线程有特定的数据结构进行管理,而在Linux的线程其实是"轻量级进程"(为方便描述以下简称为线程),所有线程都共享同一进程的资源,但每个线程都拥有独立的线程ID和私有栈。

由上文知Linux系统中不存在真正意义上的线程概念,因此Linux系统并没有提供有关线程的系统调用,因此Linux系统提供了一个原生的线程库 pthread,该库向上提供了有关线程操作的函数。

(二)底层细节

1、页表表示形式

其实页表并不是如上图中结构简单,考虑页表不能占有太多的内存空间且必须高效,实际页表是采用分级的思想。

在32位系统下,32位地址按照10-10-12的形式进行划分(不同平台下划分可能不同),即页目录索引、页表索引和页内偏移。

首先在32地址内的高10位用于在页目录中寻找页表,页目录包含多个页表的物理地址,找到指定页表后再根据地址的中10位寻找指定的页帧,页表项存储的是物理内存中页框的基地址,找到目标页框的基地址后再根据地址的低12位作偏移量得到目标数据。

2、轻量级进程标识符

文提到Linux系统中并没有单独为线程建立数据结构,而是复用进程的PCB进程管理。也就是如上文所述,Linux中并没有严格的线程,而是以轻量级进程替代线程的作用。

也就是说,在一个进程中可以包含着多个"线程",那么操作系统以及用户如何区分同一进程内的线程呢?

实际同一进程内的所有线程共用一个PID,但针对于同一进程内的不同线程,系统使用LWP(Light Weigh Process)来标识一个线程。

根据上图我们可以得知,同一进程内的线程的PID相同,PID与LWP相同的线程我们称为主线程。需要注意的是,我们可以在命令行对指定线程发送信号,但对应的进程内的线程都会执行递达动作。也就是当我们向LWP为18084的线程发送 kill 终止信号时,LWP为18093的线程也会终止。

3、用户级线程ID

上文提到了系统对于线程有一个独立的字段 LWP 对线程进行管理。而对于用户来说,对线程进行操作并不是使用该字段,在创建线程时会返回给出创建线程的ID(本文 二),用户则是通过该ID对线程进行操作。

其实用户级线程ID的值为每个线程对象在共享区的起始地址。

主线程使用进程地址空间中的独立栈结构,新线程使用线程库分配好地址的线程栈。而线程库位于进程地址空间中的共享区。

线程局部存储:现在有一个全局变量int a=100,存放于已初始化数据段;在a的前面加一个__pthread修饰,每个线程将会各有一份属于自己的变量a,互不干扰,此时a存放于共享区(共享区在pthread库中)。

4、线程的私有数据及返回值问题

上文我们提到,同一进程内的所有线程共享进程的资源,但对于线程而言,其自身也拥有一定的资源,这里我们简称为私有数据。

私有:(1)线程PCB属性私有;

(2)线程有一定的上下文结构;

(3)每个线程都有独立栈结构。

对于传统的函数,成功返回0,失败返回-1,并对全部变量 errno 进行设置;而对于 pthread库中的函数出错时,并不会设置 errno,而是将错误代码通过返回值进行返回。

上文我们提到同一进程内的所有线程共享进程资源,也就是如果在全局定义一份变量,该变量所有的线程都可以访问且修改,若线程对该变量进行修改,进程内的所有线程都会收到该变量修够的影响,但如果我们希望每个线程都拥有一份独立的变量,在该变量前面加一个 __thread 修饰,此时该变量对于每个线程来说都拥有一份,互不影响。

5、底层 clone() 函数

cpp 复制代码
CLONE(2)                                                                                  
NAME
       clone, __clone2 - create a child process
SYNOPSIS
       #include <sched.h>
       int clone(int (*fn)(void *), void *child_stack,
                 int flags, void *arg, ...
                 /* pid_t *ptid, struct user_desc *tls, pid_t *ctid */ );

该函数是fork()和vfork()的底层。区别在于 fork() 创建子进程将不会共享父进程的进程地址空间,而vfork()创建的进程会共享父子进程的进程地址空间。

当然pthread原生线程库就是使用了clone来为用户创建轻量级进程。

(三)进程与线程的区别

1、进程

进程是操作系统中承担分配资源的基本单位。 详见:【Linux】Linux操作系统------进程-CSDN博客

进程 = 若干线程PCB + 进程地址空间 + 页表 + 进程对应的代码与数据(存储在物理内存)。

2、线程(轻量级进程)

(1)线程是操作系统中CPU调度的基本单位;

(2)线程在进程的进程地址空间中运行,共享进程内的资源,进程粗粒度更粗,而线程更细;

(3)相较于操作系统windows,会给线程创建一个个TCB(线程控制块)来管理大量的线程;Linux中的线程并不像windows那样,而是直接复用了进程PCB的那套数据结构、管理方法。所以Linux中并没有真正意义上的线程,这些披着进程PCB外壳的"线程"被称为轻量级进程。

(四)线程的优缺点

1、优点

(1)进创建一个进程需要申请很多资源,而线程共享进程内的资源,因此创建一个线程的代价要远小于创建一个进程;

(2)与进程切换相比,线程切换操作系统需要做的工作要少。

进程切换:切换页表、进程地址空间、PCB切换、上下文切换等;

而线程切换仅需PCB切换、上下文切换。

(3)因为同一进程内的线程共享进程资源,因此同一进程内的线程通信非常便捷;

(4)能充分利用多处理器(多核)的可并行数量,对于计算密集型任务可以将计算分解到多个线程中实现,对于IO密集型任务,可以将不同的IO任务分给不同的线程执行。

2、缺点

(1)因为同一进程内的线程共享进程资源,而对于线程来说缺乏访问控制;

(2)鲁棒性降低,若一个进程内的某一线程发生异常崩溃,整个进程都会发生崩溃终止运行;

(3)在一些特定场景下可能会导致性能损失,如计算密集型线程的数量比可用的处理器多,那么在多线程的执行下可能会有较大的性能损失,产生额外的同步和调度开销;

(4) 编写与调试一个多线程程序比单线程程序困难得多。

二、线程的创建

cpp 复制代码
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.
RETURN VALUE
       On success, pthread_create() returns 0; on error, it returns an error 
number, and the contents of *thread are undefined.

pthread线程库为我们提供了创建线程的函数,其中 thread 为输出型参数,输出新创建线程的ID,该ID也就是新创新线程对象的起始地址,attr 为指定新建线程的属性(如私有栈大小),为空时为默认属性,start_routine则为新建线程的入口函数,线程将执行该函数,相应的arg 则为该函数的传入参数。成功时返回0,失败则会返回错误码。

cpp 复制代码
#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;
void *start_routine(void *arg)
{
    cout << "线程:" << getpid() << " 传入参数:" << (char *)arg << endl;
    return nullptr;
}
int main()
{
    pthread_t tid;
    cout << "主线程:" << getpid() << endl;
    pthread_create(&tid, nullptr, start_routine, (void *)"hello world");
    sleep(1); // 防止主线程执行太快程序直接执行结束
    return 0;
}

运行结果:

实际我们也可以使用C++容器对多线程进行管理。

cpp 复制代码
#include <iostream>
#include <pthread.h>
#include <vector>
#include <unistd.h>
#include <accert.h>
using namespace std;
#define NUM 1024
struct thread
{
    pthread_t tid; // 线程ID
    char namebuffer[NUM];
};
void *start_routine(void *arg)
{
    const char *str = static_cast<char *>(arg);
    cout << str << endl;
}
int main()
{
    vector<thread *> threads;
    for (int i = 1; i <= 10; ++i)
    {
        thread *th = new thread();
        snprintf(th->namebuffer, sizeof(th->namebuffer), "新建了第%d个线程", i);
        int ret = pthread_create(&th->tid, nullptr, start_routine, th->namebuffer);
        assert(ret == 0);
        threads.push_back(th);
        sleep(1);
    }
    // 进程线程操作
    //...
    for (auto &e : threads)
        cout << "线程" << e->tid << "创建成功" << endl;
    return 0;
}

三、线程的等待

(一)回收等待

与进程等待相同,线程等待的目的也是回收线程PCB等资源。

cpp 复制代码
PTHREAD_JOIN(3)
NAME
       pthread_join - join with a terminated thread
SYNOPSIS
       #include <pthread.h>
       int pthread_join(pthread_t thread, void **retval);
       Compile and link with -pthread.
RETURN VALUE
       On success, pthread_join() returns 0; on error, it returns an 
error number.

pthread_join() 函数会使主线程阻塞等待目标线程退出。参数thread为等待目标线程的ID,而 retval 作为输出型参数,输出目标线程的返回值。

cpp 复制代码
#include <iostream>
#include <pthread.h>
#include <vector>
#include <unistd.h>
#include <cassert>
using namespace std;
#define NUM 1024
struct thread
{
    pthread_t tid; // 线程ID
    char namebuffer[NUM];
};
void *start_routine(void *arg)
{
    const char *str = static_cast<char *>(arg);
    cout << str << endl;
    return nullptr; // 如有需要可返回目标数据
}
int main()
{
    vector<thread *> threads;
    for (int i = 1; i <= 10; ++i)
    {
        thread *th = new thread();
        snprintf(th->namebuffer, sizeof(th->namebuffer), "新建了第%d个线程", i);
        int ret = pthread_create(&th->tid, nullptr, start_routine, th->namebuffer);
        assert(ret == 0);
        threads.push_back(th);
        sleep(1);
    }
    // 进程线程操作
    //...
    for (auto &it : threads)
    {
        void *ret = nullptr; // 接收线程的返回值
        int n = pthread_join(it->tid, &ret);
        assert(n == 0);
        cout << "线程退出:" << it->tid << endl;
        delete it;
    }
    cout << "主线程退出" << endl;
    return 0;
}

需要注意的是 pthread_join() 的返回值是用于判断是否回收成功的,而其参数 retval 才是用于接收线程执行目标函数的返回值。线程的目标函数执行完成以后会将函数的返回值存储于线程的私有栈中,回收函数则是从私有栈中取出该返回值。

(二)分离线程

我们知道父进程可以设置 SIGCHILD 信号忽略来实现子进程执行完成以后由操作系统进行回收资源而无需由父进程进行等待回收。那么如上操作在线程内可实现吗?其实是可以通过分离线程来达到该目的。

cpp 复制代码
PTHREAD_SELF(3)
NAME
       pthread_self - obtain ID of the calling thread
SYNOPSIS
       #include <pthread.h>
       pthread_t pthread_self(void);
       Compile and link with -pthread.
RETURN VALUE
       This function always succeeds, returning the calling thread's ID.

pthread_self() 用于获取自身线程ID。

cpp 复制代码
PTHREAD_DETACH(3)
NAME
       pthread_detach - detach a thread
SYNOPSIS
       #include <pthread.h>
       int pthread_detach(pthread_t thread);
       Compile and link with -pthread.
RETURN VALUE
       On success, pthread_detach() returns 0; on error, it returns an 
error number.

pthread_detach() 用于分离线程。其中参数 thread 为分离指定线程ID的线程。成功返回0,失败返回错误码。

需要注意的是,建议分离线程的工作交给主线程来完成,因为线程其实是一个执行流,如在子线程内进行分离工作,有可能主线程在创建子线程以后仍继续执行,遇到 pthread_join() 函数仍然会阻塞等待子线程。

cpp 复制代码
#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;
void *start_routine(void *arg)
{
    cout << "线程:" << getpid() << " 传入参数:" << (char *)arg << endl;
    return nullptr;
}
int main()
{
    pthread_t tid;
    cout << "主线程:" << getpid() << endl;
    pthread_create(&tid, nullptr, start_routine, (void *)"hello world");
    pthread_detach(tid); // 分离线程
    sleep(3);
    return 0;
}

四、线程的终止

(一)return 终止

cpp 复制代码
void *start_routine(void *arg)
{
    cout << "线程创建成功" << endl;
    return nullptr; // 线程函数终止
}

(二)pthread_exit() 终止

cpp 复制代码
void *start_routine(void *arg)
{
    cout << "线程创建成功" << endl;
    pthread_exit(nullptr); // 线程函数终止
}

pthread_exit() 函数也可以和 return 作用相同,可传入参数用于线程函数执行返回。同样的,在返回时要注意动态申请的资源是否已经释放,否则会造成内存泄露。

(三)pthread_cancel() 终止

cpp 复制代码
PTHREAD_CANCEL(3)
NAME
       pthread_cancel - send a cancellation request to a thread
SYNOPSIS
       #include <pthread.h>
       int pthread_cancel(pthread_t thread);
       Compile and link with -pthread.
RETURN VALUE
       On success, pthread_cancel() returns 0; on error, it returns a nonzero 
error number.

该函数用于取消指定线程,线程如果被取消,那么这个线程的退出码是-1(宏PTHREAD_CANCELED)。

五、原生线程库的封装

(一)mythread.hpp

cpp 复制代码
#include <iostream>
#include <functional>
#include <string>
#include <cassert>
#include <pthread.h>
using namespace std;

class Thread;  // 声明一下
struct Context // 线程上下文类
{
    Thread *_this = nullptr;
    void *_args = nullptr;
};

class Thread
{
public:
    typedef function<void *(void *)> func_t;
    Thread(func_t func, void *args)
        : _func(func), _args(args)
    {

        Context *ctx = new Context();
        ctx->_this = this;
        ctx->_args = args;
        pthread_create(&_tid, nullptr, start_routine, ctx);
        _name = string("thread - ") + to_string(_tid);
    }
    void *run(void *args)
    {
        return _func(args);
    }
    static void *start_routine(void *args)
    {
        Context *ctx = static_cast<Context *>(args);
        void *ret = ctx->_this->run(ctx->_args);
        return ret;
    }
    void join()
    {
        int n = pthread_join(_tid, nullptr);
        assert(n == 0);
        cout << _name << "已退出" << endl;
    }

private:
    string _name;   // 线程名称
    pthread_t _tid; // 线程ID
    func_t _func;   // 回调函数
    void *_args;    // 线程函数参数
};

(二)mythread.cpp

cpp 复制代码
#include <iostream>
#include <pthread.h>
#include <memory>
#include "mythread.hpp"
#include <unistd.h>
using namespace std;
void *thread_run(void *args)
{
    const char *str = static_cast<char *>(args);
    cout << args << "已创建" << endl;
    return nullptr;
}
int main()
{
    unique_ptr<Thread> thread1(new Thread(thread_run, (void *)"thread 1"));
    sleep(1);
    unique_ptr<Thread> thread2(new Thread(thread_run, (void *)"thread 2"));
    sleep(1);
    unique_ptr<Thread> thread3(new Thread(thread_run, (void *)"thread 3"));
    sleep(1);

    thread1->join();
    thread2->join();
    thread3->join();
}
相关推荐
码农阿豪27 分钟前
从零开始搭建高效文档管理系统Moredoc打造私人某度文库
java·coplar
代码驿站52028 分钟前
PHP语言的并发编程
开发语言·后端·golang
毕业设计-0130 分钟前
0042.大学校园生活信息平台+论文
java·spring boot·后端·毕业设计·源代码管理
老大白菜30 分钟前
第1章:Go语言入门
开发语言·后端·golang
DevOpsDojo33 分钟前
MATLAB语言的正则表达式
开发语言·后端·golang
等一场春雨1 小时前
Java 23 集合框架详解:ArrayList、LinkedList、Vector
java·开发语言
Hello Dam1 小时前
分布式环境下定时任务扫描时间段模板创建可预订时间段
java·定时任务·幂等性·redis管道·mysql流式查询
javaweiming1 小时前
根据中文名称首字母进行分组
java·汉字转拼音
qincjun2 小时前
Qt仿音乐播放器:媒体类
开发语言·qt
小白编程95272 小时前
matlab离线安装硬件支持包
开发语言·matlab