一、线程概念
- 线程是比进程更加轻量化的一种执行流 / 线程是在进程内部执行的一种执行流
- 线程是CPU调度的基本单位,进程是承担系统资源的基本实体
在说线程之前我们来回顾一下进程的创建过程,如下图
那么以进程为参考,我们该如何去设计创建一个线程呢?
线程不止一个,就注定它也需要被管理,即先描述在组织,即线程需要有一个TCB(和PCB类似),同时线程也是执行流,也需要被调度运行,被管理,所以我们还需要给它设计一系列的算法,然后还得让它和进程联系起来,这样很麻烦
那有没有更简单的做法呢?
由于线程同进程一样也是一个执行流,那么管理进程用的一些信息,线程也应该需要,只不过线程更加起轻量化而已(这个后面会有具体说明),所以进程的结构体和管理用的内核数据结构对于线程也应该同样适用,所以我们可以选择复用进程的代码和数据结构,轻松的完成任务,并且也不用考虑进程和线程的耦合问题,因为它们处在同一个体系框架下
(上面介绍的是Linux的做法,当然也有OS是将进程和线程分开来设计的)
具体如下
但是现在又出现了一个问题,如何看待进程???根据上面这张图,我们会发现进程和线程似乎一样了,都有数据结构和各自的代码和数据。下面我们来进一步理解进程
感性的理解进程和线程
我们可以将进程想象成一个家庭,线程则是家庭中的人,进程的任务就是将这个家变得越来越好,所以家中的每个人都要分工合作干好各自的事,每个家庭都具有独立性(进程独立性),即你生活的好不好跟你的邻居没太大关系,但是你们可能会有交集(进程间通信),之前我们讲的进程可以看成是家中只有一个人的情况。
下面写一段代码(不用管创建线程的函数,后面会讲,主要是观察现象)
#include <iostream>
#include <pthread.h>
#include <unistd.h>
//新线程
void *Threadroutine(void *args)
{char *p = (char *)args;while (1){std::cout << "I am a new thread : " << p << ", pid : " << getpid() << std::endl;sleep(1);}return nullptr;
}int main()
{//已经有进程了pthread_t tid;pthread_create(&tid, nullptr, Threadroutine, (void *)"thread1");//主线程while (1){std::cout << "I am a main thread, pid : " << getpid() << std::endl;sleep(1);}return 0;
}
首先确实有两个执行流在循环打印语句,并且它们的进程pid相同,但是它们的LWP(light weight process)不同,即它们是线程(在Linux中线程的底层是轻量级进程),所以OS在调度时看的是LWP,当然我们会发现有一个线程的LWP和PID是一样的,它就是主线程。(如果你看到乱序的打印也是正常的,因为OS如何调度两个线程是未知的)
而由于线程共享同一个进程地址空间,所以线程间的共享资源很多,也更容易通信(不保证安全)
#include <iostream>
#include <pthread.h>
#include <unistd.h>int cnt = 0;
void *Threadroutine(void *args)
{char *p = (char *)args;while (1){std::cout << "I am a new thread : " << p << ", pid : " << getpid() << " cnt:" << cnt << std::endl;cnt++;sleep(1);}return nullptr;
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, Threadroutine, (void *)"thread1");sleep(1);pthread_t tid1;pthread_create(&tid1, nullptr, Threadroutine, (void *)"thread2");sleep(1);pthread_t tid2;pthread_create(&tid2, nullptr, Threadroutine, (void *)"thread3");sleep(1);while (1){std::cout << "I am a main thread, pid : " << getpid() << " cnt:" << cnt << std::endl;sleep(1);}return 0;
}
注意:打印乱序是正常现象,因为Threadroutine是不可重入函数,但是我们重入了。但是这不妨碍我们能看到cnt在不断增加的,并且每个线程都能看到,也就是说cnt这个全局变量对于线程来说是共享的。
如何理解线程更加轻量化???
1、线程的创建更加简单
2、线程的切换更加高效
- 要修改的寄存器少了---因为线程中有很多数据是一样的,比如存放页表地址的寄存器就不用修改,因为它们共用一个页表
- 不需要重新更新cache(缓存)---根据局部性原理,在执行某条语句之后,更有可能执行它的上下文中的代码,所以我们会提前将它附近的代码和数据加载到缓存(称为热数据),来提高CPU效率,对于线程来说这样的热数据大概率是有效的,而对于进程来说,则是基本无效的,需要重新加载
(注意:这里谈论的线程切换是指在同一个进程中的线程的切换,不同进程的线程切换还是属于进程切换)
二、进一步理解进程地址空间
三、线程的优缺点+资源+异常
1、优点
- 创建一个新线程的代价要比创建一个新进程小得多
- 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
- 线程占用的资源要比进程少很多
- 能充分利用多处理器的可并行数量
- 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
- 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
- I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作
2、缺点
- 性能损失:一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。
- 健壮性降低:编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。
- 缺乏访问控制:进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。
- 编程难度提高:编写与调试一个多线程程序比单线程程序困难得多(要更加细节)
3、资源
线程共享进程数据,但也拥有自己的一部分数据:线程ID、硬件上下文、栈、errno、信号屏蔽字、调度优先级
进程的多个线程共享同一地址空间,因此Text Segment、Data Segment都是共享的,如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境:文件描述符表、每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)、当前工作目录、用户id和组id
(注意:信号的pending位图、block位图都是各自私有的)
4、线程异常
- 单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃
- 线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出
四、线程控制---相关函数接口介绍
1、线程创建
功能:创建一个新的线程
参数:
- thread:返回线程ID
- attr:设置线程的属性,attr为NULL表示使用默认属性
- start_routine:是个函数地址,线程启动后要执行的函数
- arg:传给线程启动函数的参数
返回值:成功返回0;失败返回错误码
#include <iostream>
#include <pthread.h>
#include <unistd.h>void* ThreadRoutine(void * args)
{while(1){std::cout << "I am a new thread" << std::endl;sleep(1);}
}
int main()
{pthread_t tid;pthread_create(&tid,nullptr,ThreadRoutine,nullptr);while(1){std::cout << "I am a main thread" << std::endl;sleep(1);}return 0;
}
1)给线程传参的问题
#include <iostream>
#include <functional>
#include <string>
#include <vector>
#include <pthread.h>
#include <unistd.h>
#include <time.h>using func_t = std::function<void()>;
class ThreadDate
{
public:std::string _name;u_int64_t _createtime;func_t _f;public:ThreadDate(std::string name, u_int64_t createtime, func_t f): _name(name), _createtime(createtime), _f(f){}
};void Print()
{std::cout << "我正在执行某个任务" << std::endl;
}//函数的参数为void*,即可以传任意类型的指针,我们可以把结构体对象当参数传进去
void *ThreadRoutine(void *args)
{ThreadDate *ptd = static_cast<ThreadDate *>(args);while (1){std::cout << "new thread name: " << ptd->_name << " , create time: " << ptd->_createtime << std::endl;ptd->_f();sleep(1);}
}int main()
{std::vector<pthread_t> v;for (int i = 0; i < 5; i++){char name[64] = {0};sprintf(name, "%s-%d", "thread", i);ThreadDate *p = new ThreadDate(name, (u_int64_t)time(nullptr), Print);pthread_t tid;pthread_create(&tid, nullptr, ThreadRoutine, (void *)p);v.push_back(tid);}sleep(3);std::cout << "thread id: ";for (auto x : v){std::cout << x << " ";}std::cout << std::endl;while (1){std::cout << "I am a main thread" << std::endl;sleep(1);}return 0;
}
我们确实能通过函数的参数将我们想要的数据(放在结构体对象中)传给线程,并且也可以传函数,同理该线程的返回值也是一样(这个后面会演示)
打印出来的tid显然和LWP不相同,它具体是什么呢?(后面会说,这里只是抛出问题)。
也可以通过上面这个函数获取线程自身的tid,注意不是LWP!!!
2)线程异常问题
显然在第四个线程出现异常,收到信号后,整个进程都退出了,也说明线程的健壮性比较差
2、线程退出
原型:void pthread_exit(void *value_ptr)
功能:线程终止
参数:
- value_ptr:value_ptr不要指向一个局部变量。
返回值:无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)
#include <iostream>
#include <pthread.h>
#include <unistd.h>void* ThreadRoutine(void* args)
{int cnt = 5;while(cnt--){std::cout << " I am a new thread " << std::endl;sleep(1);}pthread_exit(nullptr); // 终止线程// return nullptr; // 也可以终止线程// exit(1); // 注意:该函数是用来结束进程的!!!
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, ThreadRoutine, nullptr);//...return 0;
}
我们也可以直接return,这样也是能终止线程的。
3、线程等待
线程退出默认也是需要被等待的(就像进程一样)
- 线程退出,没有等待,会导致类似进程的僵尸问题
- 线程退出,主线程也需要获取新线程的返回值
原型:int pthread_join(pthread_t thread, void **value_ptr)
功能:等待线程结束
参数
- thread:线程ID
- value_ptr(输出型参数):它指向一个指针,该指针指向线程的返回值,可以接收任意类型的指针
返回值:成功返回0,失败返回错误码
#include <iostream>
#include <pthread.h>
#include <unistd.h>void* ThreadRoutine(void* args)
{int cnt = 5;while(cnt--){std::cout << " I am a new thread " << std::endl;sleep(1);}pthread_exit((void*)"thread end"); //结束线程// return (void*)"thread end";
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, ThreadRoutine, nullptr);void*ret = nullptr;int n = pthread_join(tid,&ret);std::cout << "main thread, n: " << n << std::endl;std::cout << "new thread return val: " << (char*)ret << std::endl;return 0;
}
和线程创建时传给线程的参数一样,这里的返回值可以是任意类型的指针(可以指向结构体,该结构体中可以存放任何你想通过线程得到的数据,这个就不演示了,类比线程创建即可)
这里简单说明一下:为什么进程退出时既关心是否正常退出,又关心异常问题,但是线程出现异常我们并不关心?因为线程一旦异常,会导致进程整个挂掉,所以线程的异常就没必要关心了
4、线程分离
当主线程不关心线程的的返回结果时,我们可以将线程设置为分离状态,这样该线程结束后就会自动被OS回收,不需要主线程在等待了
int pthread_detach(pthread_t thread)
功能:分离线程
参数:
- thread:线程ID
返回值:成功返回0,失败返回错误码
#include <iostream>
#include <pthread.h>
#include <unistd.h>void* ThreadRoutine(void* args)
{// pthread_detach(pthread_self()); // 可以在线程中进行线程分离int cnt = 5;while(cnt--){std::cout << " I am a new thread " << std::endl;sleep(1);}return nullptr;
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, ThreadRoutine, nullptr);pthread_detach(tid);sleep(1);int n = pthread_join(tid,nullptr);std::cout << "main thread, n: " << n << std::endl;return 0;
}
显然线程等待失败,这里的线程分离,也可以在创建的线程中使用 (注意:线程分离后,出现异常还是会导致整个进程挂掉)
5、线程取消
原型:int pthread_cancel(pthread_t thread)
功能:取消一个执行中的线程
参数:
- thread:线程ID
返回值:成功返回0,失败返回错误码
#include <iostream>
#include <pthread.h>
#include <unistd.h>void* ThreadRoutine(void* args)
{while(true){std::cout << " I am a new thread " << std::endl;sleep(1);}return nullptr;
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, ThreadRoutine, nullptr);// pthread_detach(tid);sleep(5);int n = pthread_cancel(tid);std::cout << "main thread, cancel return: " << n << std::endl;void*ret = nullptr;n = pthread_join(tid, &ret);std::cout << "main thread, join return: " << n << " ret: " << (long long)ret << std::endl;return 0;
}
(注意:如果线程在pthread_cancel之前终止,那么该函数调用失败,返回错误码)
thread线程以不同的方法终止,通过pthread_join得到的终止状态是不同的,总结如下
1. 如果thread线程通过return返回,value_ ptr所指向的单元里存放的是thread线程函数的返回值。
2. 如果thread线程被别的线程调用pthread_ cancel异常终掉,value_ ptr所指向的单元里存放的是常数PTHREAD_ CANCELED [#define PTHREAD_CANCELED ((void *) -1)]
3. 如果thread线程是自己调用pthread_exit终止的,value_ptr所指向的单元里存放的是传给pthread_exit的参数
4. 如果对thread线程的终止状态不感兴趣,可以传NULL给value_ ptr参数
- 默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏
- 如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源,即线程分离
- 一个线程要么是joinable的,要么是分离的
五、理解线程库
在语言的角度:其实C++11中的线程库就是对原生线程库的封装,其他语句同理
注意:__thread只能修饰内置类型,自定义类型不行,在C++11中可以用thread_local对自定义类型进行修饰
六、模拟实现C++线程库(简易版)
#pragma once
#include <iostream>
#include <functional>
#include <string>
#include <pthread.h>using func_t = std::function<void()>; // 该函数类型可以按照需求改变
class thread
{
public:thread(std::string name, func_t f): _tid(0), _name(name), _isrunning(false), _fun(f){}// 注意,如果是非静态成员,则会多一个this作为参数(c++语法)static void *ThreadRoutine(void *args){thread *t = static_cast<thread *>(args);t->_fun(); // 要想访问类成员,要传类对象return nullptr;}bool Start(){int n = pthread_create(&_tid, nullptr, ThreadRoutine, this);if (n == 0){_isrunning = true;return true;}return false;}bool Join(){if (!_isrunning)return true;int n = pthread_join(_tid, nullptr);if (n == 0){_isrunning = false;return true;}return false;}std::string getname(){return _name;}bool IsRunning(){return _isrunning;}~thread(){}private:pthread_t _tid;std::string _name;bool _isrunning;func_t _fun;
};//进阶---用模板
template <class T>
using func_t = std::function<void(T)>;template <class T>
class thread
{
public:thread(std::string name, func_t<T> f, T data): _tid(0), _name(name), _isrunning(false), _fun(f),_data(data){}// 注意,如果是非静态成员,则会多一个this作为参数(c++语法)static void *ThreadRoutine(void *args){thread *t = static_cast<thread *>(args);t->_fun(t->_data); // 要想访问对象,要传递对象return nullptr;}bool Start(){int n = pthread_create(&_tid, nullptr, ThreadRoutine, this);if (n == 0){_isrunning = true;return true;}return false;}bool Join(){if (!_isrunning)return true;int n = pthread_join(_tid, nullptr);if (n == 0){_isrunning = false;return true;}return false;}std::string getname(){return _name;}bool IsRunning(){return _isrunning;}~thread(){}private:pthread_t _tid;std::string _name;bool _isrunning;func_t<T> _fun;T _data;// 如果需要也可以加一个成员变量存储线程的结果
};