目录
一、认识线程
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