Bootstrap

【Linux】多线程(上)

目录

一、概念

1.1 线程的概念

1.2 线程周边概念

二、线程的优缺点

2.1 优点

2.2 缺点

三、线程与进程

四、线程控制

4.1 POSIX线程库

4.2 线程创建

pthread_create

pthread_t及地址空间的布局

4.3 线程终止

pthread_exit

pthread_cancel

4.4 线程等待

pthread_join

4.5 线程分离

pthread_detach

pthread_self


一、概念

1.1 线程的概念

  • 线程(thread)是进程内的一个执行流,能够执行进程代码的一部分。线程只创建PCB,同一进程内的线程共用进程地址空间和页表
  • 一个进程内部至少有一个线程
  • 线程是CPU调度的基本单位,进程是分配系统资源的基本实体
  • 进程包含多个执行流、地址空间、页表、物理内存分配的空间等系统资源,线程是进程内部的执行流资源

不同操作系统对线程的实现方案可能是不一样的。在部分其他的操作系统中,线程有自己独立的线程控制模块TCB(Thread Control Block),但是这样就要为进程单独重新设计各种调度算法

而Linux中选择直接复用进程的数据结构和管理算法来实现线程,即Linux中的线程是由进程模拟的线程,所以认为Linux没有真正意义上的线程,又称为轻量级进程(LWP)。这也是为何上面说线程也要创建PCB,因为复用了进程的结构 

进程与线程是1:n的关系,因此操作系统中线程的数量一定比进程要多

进程与线程的关系可以类比为家庭与家人的关系,家庭中的家人可以共享家庭内的资源,并且各司其职维持家庭的运行,家庭中至少存在一个家人

我们过去学习的进程,实际上就是只有一个线程执行流的进程

1.2 线程周边概念

说线程是轻量级进程,首先在于线程的创建与释放更加轻量化。线程在创建时只需要创建新的PCB,不需要创建新的地址空间和页表,释放时也只需要释放PCB

其次,线程的切换也更加轻量化

  • CPU中存在cache缓存,会进行热数据缓存,即将一些被高频访问的数据存到cache缓存中,提高效率
  • 切换进程时,需要重新在cache中缓存新的数据,数据由冷变热是需要时间的。而切换线程不需要切换cache内的数据


二、线程的优缺点

2.1 优点

  • 创建和释放的成本比进程更低,切换需要的工作量更小
  • 占用资源比线程少
  • 对于多处理器系统,可以充分利用可并行数量
  • 对于在多处理器系统上运行的计算密集型应用,可将计算任务分解到多个进程中运行
  • 对于I/O密集型应用,多线程可同时等待不同的I/O操作

2.2 缺点

  • 若计算密集型线程较少被外部事件阻塞,且线程数量多于处理器数量,会增加额外的同步和调度消耗
  • 如果不加保护,多线程程序中可能存在线程冲突,影响代码健壮性
  • 线程是CPU调度的基本单元,在一个线程中执行了某些系统调用可能会影响到整个进程
  • 多线程程序的编写和调试难度高


三、线程与进程

  • 线程是CPU调度的基本单位,进程是操作系统分配资源的基本单位
  • 线程共享进程的数据,但也有自己独立的数据,如线程ID、寄存器、线程栈、错误码、信号屏蔽字、调度优先级

线程需要自己独立的一批寄存器存放线程的上下文

线程有自己独立的栈结构以避免线程之间出现错乱,不同线程之间的栈不共享!这与我们过去的认知有些区别

  • 同一进程中的多个线程共享线程地址空间,因此定义一个函数,在所有线程中都能调用;定义一个全局变量,所有线程都可以访问到
  • 线程间还共享进程的文件描述符表、不同信号的处理动作、当前工作目录、用户id和组id
  • 线程是进程的执行分支,一旦某个线程出现异常导致线程崩溃,整个进程也会随之崩溃


四、线程控制

4.1 POSIX线程库

Linux中没有明确的线程的概念,因此内核没有给我们提供线程的系统调用,只有轻量级进程的系统调用

但我们用户又需要线程的接口,因此开发者在应用层对轻量级进程的接口进行了封装,为用户提供了许多线程的接口——pthread线程库。几乎所有的Linux平台都默认自带这个第三方库,而我们在编写多线程代码时也需要使用这个库

要使用pthread线程库,需要引入头文件<pthread.h>,并且在编译链接时需要带-lpthread选项

4.2 线程创建

pthread_create

#include <pthread.h>

int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                   void *(*start_routine) (void *), void *arg);

pthread_create函数用于创建一个新进程,成功返回0,失败返回错误码

其中:

  • thread:输出型参数,返回新线程的pthread_t(线程ID)
  • attr:设置线程的属性,一般设置为nullptr表示使用默认属性
  • start_routine:函数的地址,线程启动后将要执行的函数
  • arg:传给start_routine函数的参数,一般将对应参数强转为void*后传入

例子:

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

using namespace std;

void* threadRoutine(void* arg) //新线程的例程
{
    while(true)
    {
        cout << "new thread, pid:" << getpid() << endl;
        sleep(2);
    }
    return nullptr;
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRoutine, nullptr); //创建线程
    while(true) //主线程
    {
        cout << "main thread, pid:" << getpid() << endl;
        sleep(2);
    }
    return 0;
}

运行结果: 

可以看到主线程和新线程的PID是一样的,操作系统怎么区分呢?

运行程序,输入 ps -aL 可以看到当前所有线程的信息

可以看到同一进程内的线程虽然PID相同,但LWP是不同的,操作系统根据LWP对线程进行调度

主线程的PID与LWP相同

pthread_t及地址空间的布局

需要额外一提,pthread_t所说的线程ID与我们前面指的线程ID不是一回事,前面提到的线程ID属于线程调度的范畴,用于表示线程的唯一性。而pthread_t类型的参数指向一个虚拟内存单元,该内存单元的地址即为新创建线程的线程ID

线程是如何在进程地址空间中布局的?

我们的线程库需要被加载到内存中,线程库也需要维护我们创建的线程,因此在线程库中就需要对线程进行管理。因此,我们创建的线程,在线程库中还有库级别的TCB,其起始地址就是线程的tid

线程的独立栈结构是在线程库内部进行维护的,线程库是动态库,需要被加载到共享区

线程需要有自己独立的栈结构,因为每个线程都有自己独立的调用链。除了主线程,其他所有线程的独立栈结构都在共享区——准确来说是在pthread库中tid指向的用户TCB内部

4.3 线程终止

前面提到,线程如果被异常终止,那整个进程也会随之崩溃,那如何正常的使线程终止呢?

  • 对于非主线程的线程,可以直接在线程的例程中return
  • 线程调用pthread_exit终止自己
  • 线程调用pthread_cancel终止同一进程中的其他线程

我们先来看pthread_exit函数

pthread_exit

#include <pthread.h>

void pthread_exit(void *retval);

retval是一个输出型参数,用于带出线程例程的返回值,pthread_exit函数本身无返回值

注意,调用pthread_exit时,retval指向的内存单元一定得是全局的或者是用malloc分配的,而不能是线程例程中的一个局部变量,否则线程终止后变量被销毁,retval就变成一个野指针了

包括如果我们在线程的例程中return了一个指针,这个指针指向的内存单元也必须符合上面的要求

例子:

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

using namespace std;

void* threadRoutine(void* arg) //新线程的例程
{
    int cnt = 0;
    while(true)
    {
        if(cnt == 3)
        {
            cout << "new thread exit!" << endl;
            pthread_exit(nullptr);
        }
        cout << "new thread, pid:" << getpid() << endl;
        cnt++;
        sleep(1);
    }
    return nullptr;
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRoutine, nullptr); //创建线程
    while (true) // 主线程
    {
        cout << "main thread, pid:" << getpid() << endl;
        sleep(1);
    }
    return 0;
}

运行结果:

pthread_cancel

#include <pthread.h>

int pthread_cancel(pthread_t thread);

pthread_cancel函数用于取消一个执行中的线程,成功返回0,失败返回错误码,参数thread传入指定线程的ID

被取消的线程本身在例程中会返回PTHREAD_CANCELED的宏,其内容是(void*)-1

例子:

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

using namespace std;

void* threadRoutine(void* arg) //新线程的例程
{
    while(true)
    {
        cout << "new thread, pid:" << getpid() << endl;
        sleep(1);
    }
    return nullptr;
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRoutine, nullptr); //创建线程
    int cnt = 0;
    while (true) // 主线程
    {
        if(cnt == 3)
        {
            int n = pthread_cancel(tid);
            cout << "new thread cancel!" << endl;
            break;
        }
        cout << "main thread, pid:" << getpid() << endl;
        cnt++;
        sleep(1);
    }
    return 0;
}

运行结果:

4.4 线程等待

pthread_join

#include <pthread.h>

int pthread_join(pthread_t thread, void **retval);

pthread_join函数用于阻塞式等待一个线程的结束,成功返回0,失败返回错误码

其中:

  • thread:要等待的线程ID
  • retval:指向一个指针,后者指向线程的返回值

pthread_join函数等待的线程必须是joinable的,即等待的线程不能已经分离(后面会提到)

调用该函数的线程将会阻塞式等待目标线程退出,其中又分为以下几种情况:

  • 目标线程通过return返回,则retval指向的内存空间存放return的返回值
  • 目标线程自己调用pthread_exit终止,则retval指向的内存单元存放pthread_exit的参数
  • 目标线程被其他线程通过pthread_cancel取消,则retval指向的内存单元存放PTHREAD_CANCELED
  • 如果对目标线程的返回值不感兴趣,可以将retval设置为nullptr

例子:

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

using namespace std;

int cnt = 0;

void* threadRoutine(void* arg) //新线程的例程
{
    while(true)
    {
        if(cnt == 3)
        {
            cout << "new thread exit!" << endl;
            pthread_exit(&cnt);
        }
        cout << "new thread, pid:" << getpid() << endl;
        cnt++;
        sleep(1);
    }
    return nullptr;
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRoutine, nullptr); //创建线程
    int *retval;
    pthread_join(tid, (void**)&retval);
    cout << *retval << endl;
    return 0;
}

运行结果:

4.5 线程分离

pthread_detach

如果一个线程终止,而我们又没有对其进行线程等待,那么其资源没有被释放就会造成系统泄露。

有的时候,我们可能不需要线程返回一个值,或者不关注线程的返回值,那么对其进行线程等待是一种负担。此时我们就可以选择让线程分离,即线程退出时自己释放线程资源

#include <pthread.h>

int pthread_detach(pthread_t thread);

pthread_detach函数成功返回0,失败返回错误码,参数thread传入线程ID

可以是线程自己让自己分离,也可以是线程组内的其他线程对目标线程进行分离。问题:如果线程自己让自己分离,它怎么知道自己的线程ID呢?

pthread_self

#include <pthread.h>

pthread_t pthread_self(void);

pthread_self函数会返回调用该函数的线程的ID,搭配pthread_detach函数使用就能让线程自己分离

一个线程不能既是joinable的又是分离的,即线程如果分离就不能再被线程等待

例子:

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

using namespace std;

void* threadRoutine(void* arg) //新线程的例程
{
    pthread_detach(pthread_self());
    int cnt = 0;
    while(true)
    {
        if(cnt == 3)
        {
            cout << "new thread exit!" << endl;
            pthread_exit(&cnt);
        }
        cout << "new thread, pid:" << getpid() << endl;
        cnt++;
        sleep(1);
    }
    return nullptr;
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRoutine, nullptr); //创建线程
    sleep(1);
    if(pthread_join(tid, nullptr) == 0)
        cout << "wait thread success" << endl;
    else
        cout << "wait thread failed" << endl;
    return 0;
}

运行结果:

如有错误,欢迎在评论区指出

【Linux】多线程(上)完.

;