文章目录
一、 前言
在windows中进程只是一个容器,用于装载系统资源,它并不执行代码,它是系统资源分配的最小单元,而在进程中执行代码的是线程,线程是轻量级的进程,是代码执行的最小单位。
多线程的用处:
- 任务分解:
- 耗时的操作,任务分解,实时响应
- 数据分解:
- 充分利用多核CPU处理数据
- 数据流分解:
- 读写分流,解耦合设计
1.1 进程和线程
先从概念上了解一下线程和进程之间的区别:
-
进程有自己独立的地址空间, 多个线程共用同一个地址空间
- 线程更加节省系统资源, 效率不仅可以保持的, 而且能够更高
- 在一个地址空间中多个线程独享: 每个线程都有属于自己的栈区, 寄存器(内核中管理的)
- 在一个地址空间中多个线程共享: 代码段, 堆区, 全局数据区, 打开的文件(文件描述符表)都是线程共享的
-
线程是程序的最小执行单位, 进程是操作系统中最小的资源分配单位
- 每个进程对应一个虚拟地址空间,一个进程只能抢一个CPU时间片
- 一个地址空间中可以划分出多个线程, 在有效的资源基础上, 能够抢更多的CPU时间片
- 多线程之间是无序的状态,时间片是随机分配
-
CPU的调度和切换: 线程的上下文切换比进程要快的多
**上下文切换:**进程/线程分时复用CPU时间片,在切换之前会将上一个任务的状态进行保存, 下次切换回这个任务的时候, 加载这个状态继续运行,任务从保存到再次加载这个过程就是一次上下文切换。 -
线程更加廉价, 启动速度更快, 退出也快, 对系统资源的冲击小。
二、 创建线程
一个程序至少有一个线程,这个线程称为主线程(main thread),如果我们不显示地创建线程,那我们产的程序就是只有主线程的间线程程序。
2.1 线程函数pthread_self(void)
每一个线程都有一个唯一的线程ID,ID类型为pthread_t,这个ID是一个无符号长整形数,如果想要得到当前线程的线程ID,可以调用如下函数:
pthread_t pthread_self(void); // 返回当前线程的线程ID
在一个进程中调用线程创建函数,就可得到一个子线程,和进程不同,需要给每一个创建出的线程指定一个处理函数,否则这个线程无法工作。
#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, 线程库的名字叫pthread, 全名: libpthread.so libptread.a
参数:
- thread: 传出参数,是无符号长整形数,线程创建成功, 会将线程ID写入到这个指针指向的内存中
- attr: 线程的属性, 一般情况下使用默认属性即可, 写NULL
- start_routine: 函数指针,创建出的子线程的处理动作,也就是该函数在子线程中执行。
- arg: 作为实参传递到 start_routine 指针指向的函数内部
- 返回值:线程创建成功返回0,创建失败返回对应的错误号
2.2 创建线程
下面是创建线程的示例代码,在创建过程中一定要保证编写的线程函数与规定的函数指针类型一致:void *(*start_routine) (void *):
#include<iostream>
#include<string>
#include <windows.h>
#include<pthread.h>
using namespace std;
// @file:CreateThreading
// @author:IdealSanX_T
// @date:2024/6/10 10:52:50
// @brief:创建线程Test
// 子线程的处理代码
void* working(void* arg){
// 获取当前进程ID
cout << "我是子线程, 线程ID:" << GetCurrentThreadId() << endl;
for (int i = 0; i < 9; ++i){
cout << "child == i: = " << i << endl;
}
return NULL;
}
int main(){
// 1. 创建一个子线程
pthread_t tid;
pthread_create(&tid, NULL, working, NULL);
Sleep(1000);
// 2. 子线程不会执行下边的代码, 主线程执行
cout << "我是主线程, 线程ID:" << GetCurrentThreadId() << endl;
for (int i = 0; i < 3; ++i){
cout << i << endl;
}
return 0;
}
这里如果报错,可以去看博主博客【C++】网络通信-Socket中的最后一章错误汇总
三、 线程退出
3.1 线程函数pthread_exit()
在编写多线程程序的时候,如果想要让线程退出,但是不会导致虚拟地址空间的释放(针对于主线程),我们就可以调用线程库中的线程退出函数,只要调用该函数当前线程就马上退出了,并且不会影响到其他线程的正常运行,不管是在子线程或者主线程中都可以使用。
#include <pthread.h>
void pthread_exit(void *retval);
参数: 线程退出的时候携带的数据,当前子线程的主线程会得到该数据。如果不需要使用,指定为NULL
四、 线程回收
4.1 线程函数pthread_join()
线程和进程一样,子线程退出的时候其内核资源主要由主线程回收,线程库中提供的线程回收函叫做pthread_join(),这个函数是一个阻塞函数,如果还有子线程在运行,调用该函数就会阻塞,子线程退出函数解除阻塞进行资源的回收,函数被调用一次,只能回收一个子线程,如果有多个子线程则需要循环进行回收。
另外通过线程回收函数还可以获取到子线程退出时传递出来的数据,函数原型如下:
#include <pthread.h>
// 这是一个阻塞函数, 子线程在运行这个函数就阻塞
// 子线程退出, 函数解除阻塞, 回收对应的子线程资源, 类似于回收进程使用的函数 wait()
int pthread_join(pthread_t thread, void **retval);
参数:
- thread: 要被回收的子线程的线程ID
- retval: 二级指针, 指向一级指针的地址, 是一个传出参数, 这个地址中存储了pthread_exit() 传递出的数据,如果不需要这个参数,可以指定为NULL
返回值:线程回收成功返回0,回收失败返回错误号。
4.2 线程数据回收
在子线程退出的时候可以使用pthread_exit()的参数将数据传出,在回收这个子线程的时候可以通过phread_join()的第二个参数来接收子线程传递出的数据。
通过函数pthread_exit(void * retval);可以得知,子线程退出的时候,需要将数据记录到一块内存中,通过参数传出的是存储数据的内存的地址,而不是具体数据,由因为参数是void*类型,所有这个万能指针可以指向任意类型的内存地址。
注意,如果返回的是子线程的局部变量信息时:如果多个线程共用同一个虚拟地址空间,每个线程在栈区都有一块属于自己的内存,相当于栈区被这几个线程平分了,当线程退出,线程在栈区的内存也就被回收了,因此随着子线程的退出,写入到栈区的数据也就被释放了。
所以在子线程使用pthread_join()返回数据时,需要使用全局变量或者主线程变量
#include<iostream>
#include<string>
#include <windows.h>
#include<pthread.h>
using namespace std;
// @file:CreateThreading
// @author:IdealSanX_T
// @date:2024/6/10 10:52:50
// @brief:创建线程Test
// 子线程的处理代码
void* working(void* arg){
// 接收参数
int* a = (int*)arg;
// 获取当 前进程ID
cout << "我是子线程, 线程ID:" << GetCurrentThreadId() << endl;
for (int i = 0; i < 9; ++i){
cout << "child == i: " << i << endl;
}
*a = 10;
// 该函数的参数将这个地址传递给了主线程的pthread_join()
pthread_exit(a);
return NULL;
}
int main(){
int a;
// 1. 创建一个子线程,并将a这个参数地址传入子线程
pthread_t tid;
pthread_create(&tid, NULL, working, &a);
// 2. 子线程不会执行下边的代码, 主线程执行
cout << "我是主线程, 线程ID:" << GetCurrentThreadId() << endl;
// 阻塞等待子线程退出
void* ptr = NULL;
// ptr是一个传出参数, 在函数内部让这个指针指向一块有效内存
// 这个内存地址就是pthread_exit() 参数指向的内存
pthread_join(tid, &ptr);
// 做类型转换
int* pt = (int*)ptr;
cout << "子线程传出参数" << (*pt) << endl;
// 主线程调用退出函数退出, 地址空间不会被释放
pthread_exit(NULL);
return 0;
}
五、 线程分离
5.1 线程函数pthread_detach()
在某些情况下,程序中的主线程有属于自己的业务处理流程,如果让主线程负责子线程的资源回收,调用pthread_join()只要子线程不退出主线程就会一直被阻塞,主要线程的任务也就不能被执行了。
在线程库函数中为我们提供了线程分离函数pthread_detach(),调用这个函数之后指定的子线程就可以和主线程分离,当子线程退出的时候,其占用的内核资源就被系统的其他进程接管并回收了。线程分离之后在主线程中使用pthread_join()就回收不到子线程资源了。
#include <pthread.h>
// 参数就子线程的线程ID, 主线程就可以和这个子线程分离了
int pthread_detach(pthread_t thread);
六、 C++线程类
七、线程同步
互斥量(锁)(Mutexes):
- 创建:使用pthread_mutex_init()函数,指定互斥量属性(可选)。
- 锁定:使用pthread_mutex_lock()函数,阻塞当前线程直至获得互斥量所有权。
- 解锁:使用pthread_mutex_unlock()函数,释放互斥量所有权,可能唤醒等待该互斥量的其他线程。
- 销毁:使用pthread_mutex_destroy()函数,释放互斥量占用的资源。
属性设置:通过pthread_mutexattr_init()、pthread_mutexattr_settype()等函数调整互斥量的属性,如是否为递归锁、是否为错误检查锁等。
以上参数均是指向互斥量的指针,pthread_mutex_init()函数第二个参数时互斥量属性,一般为NULL
条件变量(Condition Variables):
- 原理:条件变量用于线程间的同步,允许线程在某个条件不满足时阻塞自己,当其他线程改变了该条件并通知等待的线程后,被阻塞的线程才能继续执行。
- 使用场景:生产者-消费者模型、工作队列、多线程资源池等。
相关函数:
//初始化条件变量
pthread_cond_init( pthread_cond_t *cond,const pthread_condattr_t *attr)
- pthread_cond_t *cond:指向 pthread_cond_t 类型的指针,用于接收新创建的条件变量。
- const pthread_condattr_t *attr:指向条件变量属性的指针。如果为 NULL,则使用默认属性。
//阻塞当前线程,直到收到信号或超时。
pthread_cond_wait(pthread_cond_t *cond,pthread_mutex_t *mutex)
- pthread_cond_t *cond:指向要等待的条件变量。
- pthread_mutex_t *mutex:指向与条件变量配合使用的互斥锁。调用此函数之前,线程必须已经拥有这个互斥锁。
//唤醒一个等待该条件变量的线程。
pthread_cond_signal(pthread_cond_t *cond)
- pthread_cond_t *cond:指向要发送信号的条件变量。
//唤醒所有等待该条件变量的线程。
pthread_cond_broadcast(pthread_cond_t *cond)
- pthread_cond_t *cond:指向要广播的条件变量。
//销毁条件变量。
pthread_cond_destroy(pthread_cond_t *cond)
- pthread_cond_t *cond:指向要销毁的条件变量。