目录
1.线程
1.线程的概念
线程是进程内部的一个执行分支,线程是CPU调度的基本单位。
加载到内存中的程序,叫做进程。修正=内核数据结构+进程代码和数据。
2.线程的理解(Linux系统为例)---一般系统
地址空间是计算机内存管理中的一个基本概念,它指的是一个程序在运行时可以访问的内存地址的集合。地址空间为程序提供了一个抽象层,使得程序可以独立于其他程序和物理内存的实际情况来访问内存。
-
物理地址空间:指的是实际的物理内存地址,即内存条上的每一个存储单元都有一个唯一的物理地址。物理地址空间由硬件直接管理。
-
虚拟地址空间:是相对于物理地址空间的一个抽象层,它由操作系统创建和管理。程序通常使用虚拟地址来访问内存,然后由操作系统和硬件将这些虚拟地址映射到物理地址。
虚拟地址到物理地址的映射通常通过页表来实现。当程序访问一个虚拟地址时,内存管理单元会查找页表,找到对应的物理地址,然后进行实际的内存访问。
地址空间和地址空间上的虚拟地址,本质是一种资源。
3.进程vs线程
进程是操作系统进行资源分配和调度的基本单位。每个进程都拥有独立的地址空间、执行堆栈、程序计数器、寄存器集合以及系统资源。
线程是进程内的一个执行流,是CPU调度和分派的基本单位。同一进程中的线程共享地址空间和其他资源。
4.线程的控制
pthread_create是 POSIX 线程(pthread)库中的一个函数,用于在 POSIX 兼容的操作系统中创建新的线程。以下是关于pthread_create 的详细信息:
cpp
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
- thread:这是一个输出参数,用于存储新创建线程的线程 ID。
- attr:这是一个输入参数,用于设置新线程的属性。如果传递NULL,则新线程将使用默认属性。
- start_routine:这是一个函数指针,指向新线程将要执行的函数。该函数接受一个void*类型的参数,并返回一个 void* 类型的值。
- arg:这是传递给start_routine函数的参数。
- 成功 :返回 0。
- 失败 :返回错误编号,可以通过pthread_error获取。
cpp
#include<iostream> // 包含标准输入输出流库
#include<pthread.h> // 包含 POSIX 线程库
#include<unistd.h> // 包含 UNIX 标准函数库,如 sleep()
using namespace std; // 使用标准命名空间
// 线程函数
void *newthreadrun(void *args) {
while (true) { // 无限循环
// 输出新线程的信息和进程 ID
cout << "I am new thread, pid:" << getpid() << endl;
sleep(1); // 休眠 1 秒
}
// 这个函数不会退出,因此不需要 return 语句
}
int main() {
pthread_t tid; // 声明线程 ID 变量
// 创建新线程,传入线程函数和参数
pthread_create(&tid, nullptr, newthreadrun, nullptr);
while (true) { // 无限循环
// 输出主线程的信息和进程 ID
cout << "I am main thread, pid:" << getpid() << endl;
sleep(1); // 休眠 1 秒
}
// 主循环也不会退出,因此不会执行到这里
// 如果需要正常退出程序,应该添加适当的退出条件或信号处理
}
5.线程的等待
pthread_join是 POSIX 线程(pthread)库中的一个函数,用于等待线程结束并获取其返回值。以下是关于pthread_join的详细信息:
cpp
int pthread_join(pthread_t thread, void **retval);
- thread:这是要等待的线程的线程 ID。
- retval:这是一个输出参数,用于存储线程结束时返回的值。如果线程没有返回值,则将其设置为NULL。
- 成功:返回 0。
- 失败:返回错误编号,可以通过pthread_error获取。
cpp
#include<iostream>
#include<string>
#include<pthread.h>
#include<unistd.h>
//同一个进程内的线程,大部分资源都是共享的,地址空间是共享的
std::string ToHex(pthread_t tid)
{
char id[64];
snprintf(id,sizeof(id),"0x%lx",tid);
return id;
}
using namespace std;
void *threadrun(void *args)
{
string threadname=(char*)args;
int cnt=5;
while(cnt)
{
cout<<threadname<<"is running:"<<cnt<<",pid:"
<<getpid()<<"mythread id:"
<<ToHex(pthread_self())<<endl;
sleep(1);
cnt--;
}
return (void*)123;
}
//主线程退出==进程退出==所以线程都要退出
//1.往往我们需要main thread最后结束
//2.线程也要被"wait",不然可能内存泄漏
int main()
{
pthread_t tid;
pthread_create(&tid,nullptr,threadrun,(void*)"thread-1");
void* ret=nullptr;
int n=pthread_join(tid,&ret);
cout<<"main thread quit,n="<<n<<"main thread get a ret:"<<(int)ret<<endl;
sleep(5);
// int cnt=10;
// while(cnt)
// {
// cout<<"main thread is running:"<<cnt<<",pid:"
// <<getpid()<<"new thread id:"<<ToHex(tid)<<" "
// <<"main thread id:"<<ToHex(pthread_self())<<endl;
// sleep(1);
// cnt--;
// }
return 0;
}
6.线程的终止
pthread_exit是 POSIX 线程(pthread)库中的一个函数,用于使当前线程立即终止并返回指定的值。以下是关于pthread_exit的详细信息:
cpp
void pthread_exit(void *retval);
retval:这是线程退出时返回的值。这个值会被传递给调用pthread_join的线程
pthread_cancel是 POSIX 线程(pthread)库中的一个函数,用于请求取消另一个线程。当一个线程被取消时,它会收到一个取消请求,并可以选择如何响应这个请求。以下是关于pthread_cancel的详细信息:
cpp
int pthread_cancel(pthread_t thread);
thread :这是要取消的线程的线程 ID。
cpp
const int threadnum = 5; // 定义线程数量为5
// Task类,表示一个任务
class Task {
public:
Task() {} // 构造函数
void SetData(int x, int y) { // 设置任务数据
datax = x;
datay = y;
}
int Excute() { // 执行任务,返回数据x和数据y的和
return datax + datay;
}
~Task() {} // 析构函数
private:
int datax; // 任务数据x
int datay; // 任务数据y
};
// ThreadData类,继承自Task类,用于包装任务和线程名称
class ThreadData : public Task {
public:
ThreadData(int x, int y, const string &threadname)
: _threadname(threadname) { // 构造函数,设置线程名称
_t.SetData(x, y); // 设置任务数据
}
string threadname() { // 返回线程名称
return _threadname;
}
int run() { // 执行任务
return _t.Excute();
}
private:
string _threadname; // 线程名称
Task _t; // 任务数据
};
// Result类,用于存储线程执行结果
class Result {
public:
Result() {} // 构造函数
~Result() {} // 析构函数
void SetResult(int result, const string &threadname) { // 设置结果和线程名称
_result = result;
_threadname = threadname;
}
void Print() { // 打印结果
cout << _threadname << ":" << _result << endl;
}
private:
int _result; // 结果
string _threadname; // 线程名称
};
void *handlerTask(void *args) {
ThreadData *td = static_cast<ThreadData*>(args); // 获取线程数据
string name = td->threadname(); // 获取线程名称
Result* res = new Result(); // 创建结果对象
int result = td->run(); // 执行任务
res->SetResult(result, name); // 设置结果和线程名称
cout << name << "run result:" << result << endl; // 打印结果
delete td; // 删除线程数据
sleep(2); // 线程休眠2秒
return res; // 返回结果对象
}
int main() {
vector<pthread_t> threads; // 线程ID向量
for (int i = 0; i < threadnum; i++) { // 创建线程
char threadname[64]; // 线程名称缓冲区
snprintf(threadname, 64, "Thread-%d", i + 1); // 格式化线程名称
ThreadData *td = new ThreadData(10, 20, threadname); // 创建线程数据
pthread_t tid; // 线程ID
pthread_create(&tid, nullptr, handlerTask, td); // 创建线程
threads.push_back(tid); // 添加线程ID到向量
}
vector<Result*> result_set; // 结果集合
void *ret = nullptr;
for (auto& tid : threads) { // 等待每个线程完成
pthread_join(tid, &ret); // 等待线程完成
Result* res = static_cast<Result*>(ret); // 获取返回的结果对象
result_set.push_back(res); // 添加结果对象到集合
}
for (auto& res : result_set) { // 打印每个线程的结果
res->Print(); // 打印结果
delete res; // 删除结果对象
}
}
7.线程的分离
在Linux环境中,pthread_detach函数用于设置线程的分离属性。分离线程是指线程在完成其工作后立即退出,而不等待其他线程来收集它的返回值或清理它的资源。以下是关于pthread_detach的详细信息:
cpp
int pthread_detach(pthread_t thread);
当一个线程被设置为分离状态并完成其工作后,它会立即退出,并且不会返回任何值给调用pthread_join的线程。这意味着调用pthread_join的线程不会接收到任何值,也不会被阻塞。
2.线程的同步和互斥
线程互斥是指操作系统提供的一种机制,用于确保多个线程在访问共享资源时能够互不干扰,从而避免数据竞争和不一致性。线程互斥通常通过以下几种方式实现:
下面是封装了一个简单的线程:
cpp
#ifndef __THREAD_HPP__
#define __THREAD_HPP__
#include <iostream>
#include <string>
#include <unistd.h>
#include <functional>
#include <pthread.h>
namespace ThreadModule
{
// 定义了一个模板函数类型,用于线程执行函数
template<typename T>
using func_t = std::function<void(T&)>; // 使用引用传递,允许修改传入的数据
// 线程类模板
template<typename T>
class Thread
{
public:
// 执行传入的函数对象
void Excute()
{
_func(_data);
}
public:
// 构造函数,初始化线程所需的数据和函数
Thread(func_t<T> func, T &data, const std::string &name="none-name")
: _func(func), _data(data), _threadname(name), _stop(true)
{}
// 静态成员函数,作为线程的执行函数
static void *threadroutine(void *args) // 类成员函数,形参是有this指针的!!
{
Thread<T> *self = static_cast<Thread<T> *>(args); // 将void*指针转换为Thread<T>指针
self->Excute(); // 调用Excute函数执行传入的函数对象
return nullptr;
}
// 启动线程
bool Start()
{
int n = pthread_create(&_tid, nullptr, threadroutine, this); // 创建线程
if(!n) // 如果创建成功
{
_stop = false; // 设置停止标志为false
return true;
}
else
{
return false;
}
}
// 分离线程,线程结束后资源会被自动回收
void Detach()
{
if(!_stop) // 如果线程未停止
{
pthread_detach(_tid); // 分离线程
}
}
// 等待线程结束
void Join()
{
if(!_stop) // 如果线程未停止
{
pthread_join(_tid, nullptr); // 等待线程结束
}
}
// 获取线程名称
std::string name()
{
return _threadname;
}
// 设置线程停止标志
void Stop()
{
_stop = true;
}
// 析构函数
~Thread() {}
private:
pthread_t _tid; // 线程ID
std::string _threadname; // 线程名称
T &_data; // 引用传递的数据,所有线程可以访问同一个数据
func_t<T> _func; // 函数对象,线程执行的函数
bool _stop; // 线程停止标志
};
} // namespace ThreadModule
#endif
1.互斥锁
互斥锁(Mutex)是一种同步机制,用于保护共享资源,防止多个线程同时访问。以下是关于互斥锁的详细信息:
- 程序在访问共享资源之前,必须先获得互斥锁。
- 只有当线程持有互斥锁时,它才能访问共享资源。
- 线程在完成对共享资源的访问后,必须释放互斥锁,以便其他线程可以获取它。
- 互斥锁通常用于保护临界区,即那些只允许一个线程访问的代码段。
- 在多线程编程中,互斥锁是确保数据一致性和防止竞态条件的关键。
临界区的主要特点是:
-
共享资源:临界区访问的是共享资源,即多个线程可以访问的资源。
-
互斥访问:为了避免竞态条件和其他同步问题,临界区内的代码必须保证在同一时刻只有一个线程可以执行。
-
执行时间:临界区的执行时间应该尽可能短,以减少线程的阻塞和等待时间。
pthread_mutex_lock是 POSIX 线程(pthread)库中的一个函数,用于获取互斥锁。当一个线程尝试访问一个共享资源时,它必须首先获取互斥锁。以下是关于pthread_mutex_lock的详细信息:
cpp
int pthread_mutex_lock(pthread_mutex_t *mutex);
mutex :这是指向pthread_mutex_t类型的指针,用于指定要获取的互斥锁。
pthread_mutex_unlock是 POSIX 线程(pthread)库中的一个函数,用于释放互斥锁。当一个线程完成对共享资源的访问后,它必须释放互斥锁,以便其他线程可以获取它。以下是关于pthread_mutex_unlock的详细信息:
cpp
int pthread_mutex_unlock(pthread_mutex_t *mutex);
2.条件变量
条件变量(Condition Variable)是操作系统提供的一种同步机制,用于线程间的通信。它通常与互斥锁一起使用,以确保线程在等待某些条件满足时不会竞争共享资源。以下是关于条件变量的详细信息:
- 当线程需要等待某些条件满足时,它会释放当前持有的互斥锁,然后等待条件变量。
- 当其他线程满足这些条件时,它会通知等待的线程,然后等待的线程会重新获取互斥锁并继续执行。
pthread_cond_init是 POSIX 线程(pthread)库中的一个函数,用于初始化一个条件变量。条件变量是线程间通信的同步机制,它允许线程在等待某些条件满足时不会竞争共享资源。
cpp
int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *cond_attr);
- cond:这是指向pthread_cond_t类型的指针,用于指定要初始化的条件变量。
- cond_attr:这是一个可选参数,指向pthread_condattr_t类型的指针,用于指定条件变量的属性。如果传递NULL,则使用默认属性。
pthread_cond_destory是 POSIX 线程(pthread)库中的一个函数,用于清理条件变量。当一个条件变量不再需要时,可以使用pthread_cond_destory函数来释放它所占用的资源。
cpp
int pthread_cond_destroy(pthread_cond_t *cond);
pthread_cond_wait是 POSIX 线程(pthread)库中的一个函数,用于线程在等待条件变量时释放互斥锁。当一个线程需要等待某个条件满足时,它会释放当前持有的互斥锁,然后等待条件变量。
cpp
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
- cond:这是指向pthread_cond_t类型的指针,用于指定要等待的条件变量。
- mutex:这是指向pthread_mutex_t类型的指针,用于指定当前持有的互斥锁。
pthread_cond_signal是 POSIX 线程(pthread)库中的一个函数,用于唤醒一个等待条件变量的线程。当一个线程满足某些条件时,它可以使用pthread_cond_signa函数来通知其他等待的线程。
cpp
int pthread_cond_signal(pthread_cond_t *cond);
- cond:这是指向pthread_cond_t类型的指针,用于指定要发送信号的条件变量。
pthread_cond_broadcast是 POSIX 线程(pthread)库中的一个函数,用于唤醒所有等待特定条件变量的线程。当一个线程满足某些条件时,它可以使用pthread_cond_broadcast函数来通知所有等待的线程。
cpp
int pthread_cond_broadcast(pthread_cond_t *cond);
下面是一段测试代码:
cpp
#include<iostream>
#include<string>
#include<pthread.h>
#include<vector>
#include<unistd.h>
pthread_cond_t gcond=PTHREAD_COND_INITIALIZER;
pthread_mutex_t gmutex=PTHREAD_MUTEX_INITIALIZER;
void *SlaverCore(void *args)
{
std::string name=static_cast<const char*>(args);
while(true)
{
pthread_mutex_lock(&gmutex);
pthread_cond_wait(&gcond,&gmutex);
std::cout<<"当前被叫醒的线程是:"<<name<<std::endl;
pthread_mutex_unlock(&gmutex);
}
}
void *MasterCore(void *args)
{
sleep(3);
std::cout<<"master开始工作..."<<std::endl;
std::string name=static_cast<const char*>(args);
while(true)
{
pthread_cond_signal(&gcond);
std::cout<<"master唤醒一个线程..."<<std::endl;
sleep(1);
}
}
void StartMaster(std::vector<pthread_t> *tidsptr)
{
pthread_t tid;
int n=pthread_create(&tid,nullptr,MasterCore,(void*)"Master Thread");
if(n==0)
{
std::cout<<"create success"<<std::endl;
}
tidsptr->emplace_back(tid);
}
void StartSlaver(std::vector<pthread_t> *tidsptr,int threadnum=3)
{
for(int i=0;i<threadnum;i++)
{
char *name=new char[64];
snprintf(name,64,"slaver-%d",i+1);
pthread_t tid;
int n=pthread_create(&tid,nullptr,SlaverCore,name);
if(n==0)
{
std::cout<<"create success:"<<name<<std::endl;
tidsptr->emplace_back(tid);
}
}
}
void WaitThread(std::vector<pthread_t> &tids)
{
for(auto &tid:tids)
{
pthread_join(tid,nullptr);
}
}
int main()
{
std::vector<pthread_t> tids;
StartMaster(&tids);
StartSlaver(&tids,5);
WaitThread(tids);
return 0;
}
3.生产消费模型
生产消费模型是多线程编程中的一个经典场景,它描述了生产者线程和消费者线程之间如何共享数据缓冲区。在这个模型中,生产者线程负责创建数据并将其放入缓冲区,而消费者线程负责从缓冲区中取出数据并处理。
以下是生产消费模型的关键组成部分:
-
缓冲区:这是一个共享的资源,用于存储数据。它可以是一个固定大小的数组、链表或其他数据结构。
-
生产者线程:负责创建数据并将其放入缓冲区。生产者线程需要确保在缓冲区满时不会写入数据,以避免数据丢失。
-
消费者线程:负责从缓冲区中取出数据并处理。消费者线程需要确保在缓冲区空时不会读取数据,以避免空指针异常。
-
同步机制:为了确保生产者线程和消费者线程之间的同步,可以使用互斥锁、条件变量等同步机制。
在生产消费模型中,生产者线程和消费者线程之间通常存在以下关系:
- 生产者线程在缓冲区未满时写入数据。
- 消费者线程在缓冲区非空时读取数据。
- 生产者线程和消费者线程之间通过互斥锁和条件变量进行同步。
4.阻塞队列
阻塞队列(Blocking Queue)是一种特殊的队列,它提供了一种机制,使得生产者和消费者线程可以以不同的速度工作,而不会造成资源浪费或死锁。当队列满时,生产者线程会被阻塞,直到队列中有空闲空间;当队列空时,消费者线程会被阻塞,直到队列中有数据可消费。
以下是阻塞队列的一些关键特性:
-
生产者-消费者模型:阻塞队列支持生产者线程和消费者线程之间的数据传递。生产者线程将数据放入队列,而消费者线程从队列中取出数据。
-
线程安全:阻塞队列通常实现线程安全,可以被多个生产者和消费者线程同时访问。
-
阻塞机制:当队列满时,生产者线程会被阻塞,直到队列中有空闲空间;当队列空时,消费者线程会被阻塞,直到队列中有数据可消费。
-
无界队列和有界队列:阻塞队列可以是无界队列或有限大小的队列。无界队列没有容量限制,而有限大小的队列有最大容量限制。
cpp
#ifndef __BLOCK_QUEUE_HPP__
#define __BLOCK_QUEUE_HPP__
#include <iostream>
#include <string>
#include <queue>
#include <pthread.h>
template <class T>
class BlockQueue
{
private:
bool IsFull()
{
return _block_queue.size() == _cap;
}
bool IsEmpty()
{
return _block_queue.empty();
}
public:
BlockQueue(int cap) : _cap(cap)
{
pthread_mutex_init(&_mutex, nullptr);
pthread_cond_init(&_product_cond, nullptr);
pthread_cond_init(&_consum_cond, nullptr);
}
void Enqueue(T &in) // 生产者用的接口
{
pthread_mutex_lock(&_mutex);
if(IsFull()) //BUG??
{
// 生产线程去等待,是在临界区中休眠的!你现在还持有锁呢!!!
// 1. pthread_cond_wait调用是: a. 让调用进程等待 b. 自动释放曾经持有的_mutex锁
pthread_cond_wait(&_product_cond, &_mutex);
}
// 进行生产
// _block_queue.push(std::move(in));
std::cout << in << std::endl;
_block_queue.push(in);
// 通知消费者来消费
pthread_cond_signal(&_consum_cond);
pthread_mutex_unlock(&_mutex);
}
void Pop(T *out) // 消费者用的接口
{
pthread_mutex_lock(&_mutex);
if(IsEmpty())
{
// 消费线程去等待,是在临界区中休眠的!你现在还持有锁呢!!!
// 1. pthread_cond_wait调用是: a. 让调用进程等待 b. 自动释放曾经持有的_mutex锁
pthread_cond_wait(&_consum_cond, &_mutex);
}
// 进行消费
*out = _block_queue.front();
_block_queue.pop();
// 通知生产者来生产
pthread_cond_signal(&_product_cond);
pthread_mutex_unlock(&_mutex);
}
~BlockQueue()
{
pthread_mutex_destroy(&_mutex);
pthread_cond_destroy(&_product_cond);
pthread_cond_destroy(&_consum_cond);
}
private:
std::queue<T> _block_queue; // 阻塞队列
int _cap; // 总上限
pthread_mutex_t _mutex; // 保护_block_queue的锁
pthread_cond_t _product_cond; // 专门给生产者提供的条件变量
pthread_cond_t _consum_cond; // 专门给消费者提供的条件变量
};
#endif
5.信号量
在多线程编程中,信号量(Semaphore)是一种同步机制,用于控制对共享资源的访问。信号量可以用来实现互斥锁,也可以用来实现线程间的同步。
- 信号量的值可以增加(通常使用pthread_sem_post)或减少(通常使用 pthread_sem_wait)。
- 当信号量的值大于0时,线程可以访问共享资源;当信号量的值小于或等于0时,线程必须等待。
- 信号量的值必须始终为非负整数。
pthread_sem_init是 POSIX 线程(pthread)库中的一个函数,用于初始化一个信号量。信号量是一种同步机制,用于控制对共享资源的访问。它可以用来实现互斥锁,也可以用来实现线程间的同步。
cpp
int pthread_sem_init(pthread_sem_t *sem, const pthread_semattr_t *sem_attr, unsigned int value);
- em:这是指向pthread_sem_t类型的指针,用于指定要初始化的信号量。
- sem_attr:这是一个可选参数,指向pthread_semattr_t类型的指针,用于指定信号量的属性。如果传递NULL,则使用默认属性。
- value:这是信号量的初始值。如果传递0,则信号量会被初始化为一个空信号量。
pthread_sem_post是 POSIX 线程(pthread)库中的一个函数,用于增加信号量的值。
cpp
int pthread_sem_post(pthread_sem_t *sem);
- sem:这是指向pthread_sem_t类型的指针,用于指定要增加值的信号量。
pthread_sem_wait是 POSIX 线程(pthread)库中的一个函数,用于减少信号量的值。
cpp
int pthread_sem_wait(pthread_sem_t *sem);
- sem:这是指向pthread_sem_t类型的指针,用于指定要减少值的信号量。
6.唤醒队列
在多线程编程中,当一个线程因为某种原因(如等待条件变量、同步机制等)而被阻塞时,它会进入一个特定的等待队列中。这个等待队列被称为唤醒队列(Wakeup Queue),它包含了所有等待某些事件的线程。当这些事件发生时,这些线程会被唤醒,并有机会继续执行。
唤醒队列通常与条件变量、信号量等同步机制一起使用,以确保线程在等待事件时不会竞争共享资源。当事件发生时,其他线程可以通知等待的线程,然后等待的线程会从唤醒队列中移除,并有机会继续执行。
以下是一些常见的唤醒队列实现:
-
条件变量唤醒队列:当条件变量被信号或信号量通知时,所有等待该条件变量的线程都会被唤醒,并重新尝试获取互斥锁。
-
信号量唤醒队列:当信号量的值被增加时,所有等待该信号量的线程都会被唤醒,并有机会继续执行。
-
等待队列:操作系统内核中通常有一个全局的等待队列,用于管理所有等待事件的线程。当事件发生时,内核会遍历这个等待队列,并唤醒所有符合条件的线程。