首页 > 系统相关 >Linux多线程

Linux多线程

时间:2024-07-16 21:54:16浏览次数:17  
标签:函数 thread void Linux 任务 线程 pthread 多线程

目录

一、认识线程

1.1 线程的概念

1.2 线程 vs 进程

1.3 地址空间详解

二、线程函数 

2.1 pthread_create 线程创建函数

2.2 pthread库的引入

2.3 pthread_exit 线程退出函数

2.4 pthread_cancel 线程退出函数

2.5 pthread_join 线程等待函数

2.6 pthread_detach 线程分离函数 

三、mutex 线程互斥 

四、Linux线程同步

五、Thread的封装

5.1 类的整体框架

5.2 Start 函数

5.3 Join Detach 函数

5.4 Thread 类

六、线程池ThreadPool的封装

6.1 认识线程池

6.2 封装线程池的思路

6.3 ThreadPool整体框架

6.4 对任务的管理

6.4.1 处理任务HandlerTask

6.4.2 任务的添加

6.5 对线程的管理 

6.5.1 线程池的初始化

6.5.2 线程池的启动与等待

6.5.3 线程池的关闭 


一、认识线程

1.1 线程的概念

线程是进程内部的一个执行分支,线程是CPU调度的基本单位。

线程是进程内部的一个执行分支:

线程是CPU调度的基本单位:

1.2 线程 vs 进程

在Linux系统中,认为线程和进程具有极大的相似性,所以没有独立设计一套线程的数据结构和算法,而是直接复用的进程的代码,用进程来模拟线程。所以在Linux中,其实并没有线程,有的是轻量级进程 (lightweight process) 。

1.进程是资源分配的基本单位
2.线程是调度的基本单位
3.线程共享进程数据,但也拥有自己的一部分数据:
        线程ID
        一组寄存器
        栈
        errno
        信号屏蔽字
        调度优先级

进程的多个线程共享同一地址空间,因此Text Segment、Data Segment都是共享的,如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境:
1.文件描述符表
2.每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)
3.当前工作目录
4.用户id和组id

1.3 地址空间详解

虚拟地址空间如何对应到实际的物理内存:

二、线程函数 

2.1 pthread_create 线程创建函数

包含库函数<pthread.h>,编译时加入-pthread库

功能:创建一个新的线程
原型: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;失败返回错误码

创建线程,传入需要线程回调的函数指针,线程就会自动去执行该函数,而主线程则会继续向下执行。传入的第一个参数是一个输出型参数,用于标明该线程的线程tid,以便后续操作。

#include <iostream>
#include <unistd.h>

void *threadStart(void *args)
{
    while (true)
    {
        std::cout << "new thread is running " << "pid: " << getpid() << std::endl;
        sleep(1);
    }
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadStart, (void *)"thread-new");
    while (true)
    {
        std::cout << "main thread is running " << "pid: " << getpid() << std::endl;
        sleep(1);
    }
    return 0;
}


在进行查询的时候,也可以查询到两个LWP不同但是PID相同的进程:

2.2 pthread库的引入

Thread : Thread.cc
	g++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:
	rm -rf Thread

2.3 pthread_exit 线程退出函数

 功能:终止调用它的线程,并返回一个指针作为线程的退出状态。
原型:void pthread_exit(void *value_ptr);
参数:
value_ptr:value_ptr不要指向一个局部变量。
可以传递给任何等待该线程终止的线程(例如,通过 pthread_join)。
返回值:
无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)

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

2.4 pthread_cancel 线程退出函数

功能:取消目标线程的执行。

原型:int pthread_cancel(pthread_t thread);

参数:

thread:目标线程的线程 ID。

返回值:成功返回0;失败返回错误码

2.5 pthread_join 线程等待函数

功能:等待线程结束
原型:int pthread_join(pthread_t thread, void **value_ptr);
参数:
thread:线程ID
value_ptr:它指向一个指针,后者指向线程的返回值
返回值:成功返回0;失败返回错误码

以阻塞态等待直到全部线程退出: 

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

void *ThreadRun(void* args)
{
    int cnt = 5;
    while (cnt--)
    {
        std::cout << "new thread is running ..." << "cnt :" << cnt << std::endl;
        sleep(1);
    }
    return nullptr;
}
int main()
{
    pthread_t tid;
    int n  = pthread_create(&tid, nullptr, ThreadRun, (void*)"thread 1");
    
    std:: cout << "main process start to join ..." << std::endl;
    n = pthread_join(tid, nullptr);
    if (n == 0)
        std::cout << "main process wait success!" << std::endl;
    return 0;
}

调用该函数的线程将挂起等待,直到 id 为 thread 的线程终止。thread 线程以不同的方法终止,通过pthread_join 得到的终止状态是不同的,总结如下:
1. 如果 thread 线程通过 return 返回, value_ ptr 所指向的单元里存放的是 thread 线程函数的返回值。
2. 如果thread线程被别的线程调用 pthread_ cancel 异常终掉, value_ ptr 所指向的单元里存放的是常数 PTHREAD_ CANCELED。
3. 如果 thread 线程是自己调用 pthread_exit 终止的, value_ptr 所指向的单元存放的是传给pthread_exit 的参数。
4. 如果对 thread 线程的终止状态不感兴趣,可以传 NULL 给value_ ptr参数。

2.6 pthread_detach 线程分离函数 

功能:pthread_detach 用于将目标线程的状态设置为分离状态。分离状态的线程在终止时会自动释放其资源,不需要通过 pthread_join 来回收。 

语法:int pthread_detach(pthread_t thread);
参数:

thread:目标线程的线程 id,
也可以主动分离,即在需要分离的线程内部使用,需要使用pthread_self()函数:pthread_detach(pthread_self());

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

三、mutex 线程互斥 

这部分在之前的博客生产消费模型中有详细介绍,这里简单复用一下部分内容:


Linux_生产消费模型_Block_Queue-CSDN博客

四、Linux线程同步

这部分在上一篇博客中也有详细介绍,具体参见上述博客中条件变量部分,

五、Thread的封装

从线程创建函数来看,一个线程需要有自身的 tid ,需要有要执行的回调方法,同时,为了方便观察,还可以给每个线程一个特定的名字 string,所以就可以得到其成员变量:1个 thread_t,1个回调方法(这里可以使用function进行函数方法的封装, 如下),1个 string。

using func_t = std::function<void(std::string)>

在Thread封装中,期待完成线程的创建、线程的等待、线程的退出以及最重要的线程执行回调方法。我们的思路是,分别写一个Start、Join、Cancel函数,其中,当 Start 时,顺势创建出线程,并直接让线程执行回调方法。

除此之外,当线程需要等待或分离时,首先需要确定的是该线程是否在执行,所以还可以添加一个标明线程是否在已经停止的变量,_stop。 

5.1 类的整体框架

#include <iostream>
#include <pthread.h>
#include <functional>

namespace ThreadModule
{
    using func_t = std::function<void(std::string)>;
    class Thread
    {
    public:
        Thread(func_t func, std::string name = "none_name") : _func(func), _name(name)
        {
        }
        ~Thread()
        {}
    private:
        pthread_t _tid;
        func_t _func;
        std::string _name;
        bool _stop = true;
    };
};

5.2 Start 函数

期待Start是一个布尔类型的函数,当线程被正确的创建并执行时,返回true;反之,返回false。

1.创建线程,执行 pthread_create 函数
2.pthread_create 函数的传入参数:成员变量_tid的地址, nullptr, 线程要执行的回调方法指针, 回调方法的传入参数。其中,线程要执行的回调方法指针是这里着重要写的。

3.完成线程要执行的回调方法的书写。
4.用 n 记录 pthread_create 的返回值,并判断是否创建成功。

int n = pthread_create(&_tid, nullptr, ThreadRoutine, this);

以下是回调函数的书写格式:

static void *ThreadRoutine(void *args)

 其中,
为什么返回值是void* ?
这种函数通常作为线程的入口函数,供 pthread_create 调用。返回值 void* 用于传递线程的退出状态。

为什么传入参数是 void* ?

这种函数可以接受任意类型的数据作为参数。在调用 pthread_create 创建线程时,传递给线程函数的参数就是这个 void* 类型的指针。

为什么要使用静态成员函数?

在类中,类的方法默认都会传入this指针,pthread_create 期望一个普通的函数指针,而不能直接使用类的成员函数(非静态成员函数)作为回调函数,静态成员函数不需要 this 指针,可以直接传递给 pthread_create

以下是为什么需要向pthread_create的回调方法中传入this指针的原因: 

在 C++ 中封装线程时,调用 pthread_create 时将 this 指针传递给回调函数是为了使该函数能够访问对象的成员变量和成员函数。由于 pthread_create 需要一个普通的函数指针(即不带类成员上下文的函数),我们需要通过静态成员函数或全局函数来适配这种需求。通过传递 this 指针,可以在静态成员函数中恢复对象的上下文,从而调用非静态成员函数。 

        
        void Execute()
        {
            _func(_name);
        }
        static void *ThreadRoutine(void *args)
        {
            Thread *thread = static_cast<Thread *>(args);
            thread->Execute();
            return nullptr;
        }
        bool Start()
        {
            int n = pthread_create(&_tid, nullptr, ThreadRoutine, this);
            if (n == 0)
            {
                _stop = false;
                return true;
            }
            else
            {
                return false;
            }
        }

5.3 Join Detach 函数

        void Join()
        {
            if (!_stop)
            {
                pthread_join(_tid, nullptr);
            }
        }
        void Detach()
        {
            if(!_stop)
            {
                pthread_detach(_tid);
            }
        }

5.4 Thread 类

#ifndef __THREAD_HPP__
#define __THREAD_HPP__

#include <iostream>
#include <pthread.h>
#include <functional>

namespace ThreadModule
{
    using func_t = std::function<void(std::string)>;
    class Thread
    {
    private:
        void Execute()
        {
            _func(_name);
        }
        static void *ThreadRoutine(void *args)
        {
            Thread *thread = static_cast<Thread *>(args);
            thread->Execute();
            return nullptr;
        }

    public:
        Thread(func_t func, std::string name = "none_name") : _func(func), _name(name)
        {
        }
        bool Start()
        {
            int n = pthread_create(&_tid, nullptr, ThreadRoutine, this);
            if (n == 0)
            {
                _stop = false;
                return true;
            }
            else
            {
                return false;
            }
        }
        void Join()
        {
            if (!_stop)
            {
                pthread_join(_tid, nullptr);
            }
        }
        void Detach()
        {
            if(!_stop)
            {
                pthread_detach(_tid);
            }
        }
        ~Thread()
        {
        }

    private:
        pthread_t _tid;
        func_t _func;
        std::string _name;
        bool _stop = true;
    };
};

#endif

六、线程池ThreadPool的封装

6.1 认识线程池

线程池的概念:
一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量。

线程池的应用场景:
1. 需要大量的线程来完成任务,且完成任务的时间比较短。 WEB服务器完成网页请求这样的任务,使用线程池技术是非常合适的。因为单个任务小,而任务数量巨大,你可以想象一个热门网站的点击次数。 但对于长时间的任务,比如一个Telnet连接请求,线程池的优点就不明显了。因为Telnet会话时间比线程的创建时间大多了。
2. 对性能要求苛刻的应用,比如要求服务器迅速响应客户请求。
3. 接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用。突发性大量客户请求,在没有线程池情况下,将产生大量线程,虽然理论上大部分操作系统线程数目最大值不是问题,短时间内产生大量线程可能使内存到达极限,出现错误。

6.2 封装线程池的思路

线程池是管理线程的数据结构,所以要使用vector进行管理,vector中的元素类型是Thread,此外还要存在线程池管理的线程数量 _threadnum 。
线程池维护着多个进程来处理管理者分配的任务,任务可以使用queue来管理。
为了保证线程安全与效率,还要添加 mutex 与 cond。
与封装Thread一样,可以引入一个布尔值_isrunning表示线程池的运行情况。
最后,为了让线程与任务之间有一个追赶机制,还可以引入一个空闲线程的情况,_waitnum

关于线程池,计划有Start、Stop、Wait等。关于任务,计划有Enqueue、HandlerTask等。
同时,为了让代码更加优雅,可以把与mutex和cond相关的函数也进行相应的封装。

6.3 ThreadPool整体框架

对于 _mutex 与 _cond,采取的是在构建线程池时就进行初始化,同时在析构时把锁和成员变量销毁。 

#include <iostream>
#include <pthread.h>
#include <vector>
#include <queue>
#include <string>
#include "Thread.hpp"

using namespace ThreadModule;
const static int gthreadnum = 5;
template <class T>
class ThreadPool
{
private:
    void LockQueue()
    {
        pthread_mutex_lock(&_mutex);
    }
    void UnlockQueue()
    {
        pthread_mutex_unlock(&_mutex);
    }
    void ThreadSleep()
    {
        pthread_cond_wait(&_cond, &_mutex);
    }
    void ThreadWakeup()
    {
        pthread_cond_signal(&_cond);
    }
    void ThreadWakeupAll()
    {
        pthread_cond_broadcast(&_cond);
    }

public:
    ThreadPool(int threadnum = gthreadnum) : _threadnum(threadnum)
    {
        pthread_mutex_init(_mutex);
        pthread_cond_init(_cond);
    }
    ~ThreadPool()
    {
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_cond);
    }

private:
    int _threadnum;
    std::vector<Thread> _threadpool;
    std::queue<T> _task_queue;
    pthread_mutex_t _mutex;
    pthread_cond_t _cond;
    bool _isrunning = false;
    int _waitnum;
};

6.4 对任务的管理

6.4.1 处理任务HandlerTask

这里分为以下四种情况:
        // 线程池未退 任务队列还有任务 ——> 处理任务
        // 线程池未退 任务队列没有任务 ——> 等待任务
        // 线程池已退 任务队列还有任务 ——> 处理任务
        // 线程池已退 任务队列没有任务 ——> 成功退出
使用了两个判断语句判断此时是应该等待任务到来还是线程池的退出,当两个判断都不成立的时候,就意味着一定走到了处理任务的这步,使用模板对象取出队列中的任务并执行。

    void HandlerTask()
    {
        // 线程池未退 任务队列还有任务 ——> 处理任务
        // 线程池未退 任务队列没有任务 ——> 等待任务
        // 线程池已退 任务队列还有任务 ——> 处理任务
        // 线程池已退 任务队列没有任务 ——> 成功退出
        while (true)
        {
            LockQueue();
            // 线程池已退 任务队列没有任务 ——> 成功退出
            if (!_isrunning && _task_queue.empty())
            {
                UnlockQueue();
                break;
            }
            // 线程池未退 任务队列没有任务 ——> 等待任务
            else if (_isrunning && _task_queue.empty())
            {
                _waitnum++;
                ThreadSleep(); // 当条件变量被唤醒时从该处继续向下执行
                _waitnum--;
            }
            // 处理任务
            T task = _task_queue.front();
            _task_queue.pop();
            UnlockQueue();
            task();
        }
    }

6.4.2 任务的添加

任务的添加也有几种情况:
1.线程池退出 2.线程池未退
1.有休眠线程 2.无休眠线程

    bool Enqueue(const T &task)
    {
        bool ret = false;
        LockQueue();
        if (_isrunning)
        {
            _task_queue.push(task);
            if (_waitnum > 0)
            {
                ThreadWakeup();
            }
            ret = true;
        }
        UnlockQueue();
        return ret;
    }

6.5 对线程的管理 

6.5.1 线程池的初始化

使用 for 循环依次遍历每个线程,并给予其唯一的命名。将线程添加到线程池,使用 std::bind 绑定成员函数 HandlerTask 作为线程的任务函数,传入 this 指针将当前对象传递给绑定函数,以便在 HandlerTask 函数中使用该对象的成员变量和方法。

    void InitThreadPool()
    {
        for (int num = 0; num < _threadnum; num++)
        {
            std::string name = "thread- " + std::to_string(num + 1);
            _threads.emplace_back(std::bind(&ThreadPool::HandlerTask, this, std::placeholders::_1), name);
        }
        _isrunning = true;
    }

6.5.2 线程池的启动与等待

    void Start()
    {
        for (auto &thread : _threadpool)
        {
            thread.Start();
        }
    }
    void Wait()
    {
        for (auto &thread : _threadpool)
        {
            thread.Join();
        }
    }

6.5.3 线程池的关闭 

因为设置了单独的成员变量_isrunning,所以线程池关闭时就不需要挨个线程关闭,只需要将其设为 false。

    void Stop()
    {
        LockQueue();
        _isrunning = false; // 线程池退出
        ThreadWakeupAll();
        UnlockQueue();
    }

标签:函数,thread,void,Linux,任务,线程,pthread,多线程
From: https://blog.csdn.net/m0_75186846/article/details/139151747

相关文章

  • Linux自己制作rpm包
    制作rpm包由源码包---->rpm包安装制作rpm包工具包rpm-build在制作过程中需要源码包和配置文件rpmbuild制作rpm包的原理:1、首先rpmbuild会先将源码包进行编译安装2、再将编译安装好的文件打包为rpm包#安装rpm-buildyuminstall-yrpm-build#执行rpmbuild,虽然目前执......
  • Linux 【disk】磁盘管理
    Linux磁盘管理好坏直接关系到整个系统的性能问题。Linux磁盘管理常用三个命令为df、du和fdiskdf:diskfree:列出文件系统的整体磁盘使用量du:diskused:检查磁盘空间使用量fdisk:用于磁盘分区磁盘管理磁盘分区-->格式化(获得文件系统)-->挂载磁盘的分类:SCSI硬盘......
  • Linux 【systemctl 】服务管理器
    1.start/stop#启动一个服务并在后台运行它systemctlstart[service]#停止当前正在运行的服务systemctlstop[service]#停止正在运行的服务,然后重新启动它systemctlrestart[service]#-------------------------------#示例:开启sshd服务systemctlstartsshd#示例:......
  • Linux基础命令
    1.pwd查看当前所在目录(用处:拷贝目录到一些配置文件)2.cd切换目录(需要到特定的目录执行命令),用法:cd目录名:进入指定目录绝对路径,相对路径./当前../上一级cd..:退回上一级cd~:切到用户home目录3.tab补全文件名或者命令4.ls/ll=ls-al查看目录文件信息ls......
  • 基于Linux的Flappy bird游戏开发
    gitee源码获取链接:一、项目功能按下空格键小鸟上升,不按空格键小鸟下降。搭建小鸟需要穿过的管道。管道自动左移和创建。小鸟与管道碰撞游戏结束。二、知识储备C语言。数据结构——链表。Ncurses库。信号机制。三、项目框图四、Ncurses库问题引入?如何显示游戏界......
  • Linux命令行指令大全(Ⅰ)
    前言:     首先,我们需要明白为什么要掌握linux命令行指令。因为在日常生活中我们最为熟悉的还是windows操作系统和IOS操作系统,所以为了能对Linux操作系统可以更方便的使用,所以我们需要掌握相关的指令来让开发的过程更加便捷。    此外在本文中像ls,cd,pwd这几个......
  • Linux安装mongodb
    1.安装包下载wgethttps://fastdl.mongodb.org/linux/mongodb-linux-x86_64-rhel70-4.0.27.tgz2.安装和启动2.1解压tar-zxvfmongodb-linux-x86_64-rhel70-4.0.27.tgz2.2将解压后的目录移动到/usr/local目录下,并改名为mongodbmvmongodb-linux-x86_64-rhel7......
  • Xdown 多功能多线程并发下载工具
    下载地址:https://www.mediafire.com/file/942px42bad7exdf/Xdown%25E4%25B8%258B%25E8%25BD%25BD%25E5%25B7%25A5%25E5%2585%25B7.zip/fileXdown是一款超级强大且免费无广告的Torrent(BT)/磁力链/Aria2/HTTP下载工具。Xdown不光如此还支持BT做种,使用Xdown下载器让我们跟迅雷下载的......
  • Linux的top命令参数详解
    简介top命令是Linux下常用的性能分析工具,能够实时显示系统中各个进程的资源占用状况,类似于Windows的任务管理器。top显示系统当前的进程和其他状况,是一个动态显示过程,即可以通过用户按键来不断刷新当前状态.如果在前台执行该命令,它将独占前台,直到用户终止该程序为止.比较准......
  • Linux 配置gitlab步骤
    最近在玩gitlab,记录一下配置gitlab的过程一、安装gitlab相关的依赖环境   (1) yuminstall-ycurlpolicycoreutilsopenssh-serveropenssh-clientspostfixpolicycoreutils-pythoncronie           (2) 启动Postfix        systemct......