线程相关概念
线程是比进程更小的能独立运行的基本单位,它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
一个线程中有:
- 指向当前被执行指令的指令指针
- 栈
- 寄存器值的集合,定义了一部分描述正在执行线程的处理器状态的值
- 私有的数据区
如下图,图左是进程的内存图,图右显示了是进程内部的线程(线程共享进程的虚拟空间)。
线程基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如线程ID,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源,故而线程之间的通信不需要进行共享空间、管道的方式来进行,直接通过线程之间共享的进程内存空间看来进行通信。
相比较进程之间的操作而言,它有两个优点。一是更加轻量级,二是线程间通信更加方便。
线程和进程的区别
这部分可以看博文:进程和线程的区别
多线程编程的优缺点
我们总结了多线程编程的优缺点如下:
优点
- 能适当提高程序的执行效率
- 能适当提高资源的利用率(CPU,内存)
- 线程上的任务执行完成后,线程会自动销毁
缺点
- 开启线程需要占用一定的内存空间(默认情况下,每一个线程都占 512 KB)
- 如果开启大量的线程,会占用大量的内存空间,降低程序的性能
- 线程越多,CPU 在调用线程上的开销就越大
- 程序设计更加复杂,比如线程间的通信、多线程的数据共享
查看进程的命令及文件
命令 | 含义 |
---|---|
ps -T -p | -T开启线程查看 |
top -H -p | H开启线程查看 |
文件 | 含义 |
---|---|
/proc/{PID}/task/ | 线程默认的名字和进程名相同 |
/proc/{PID}/task/{tid}/comm | 线程名 |
若进程一个线程都没开,我们也可以认为该进程作为一个主线程在跑。
多线程编程的相关函数总结
函数功能 | 函数格式 |
---|---|
查看线程标识符 | pthread_t pthread_self(void) |
创建线程 | int pthread_create(pthread_t * tidp, pthread_attr_t * attr, void *(*start_rtn)(void), void * arg) |
终止线程 | void pthread_exit(void* retval) |
合并线程 | int pthread_join(pthread_t tid, void **retval) |
分离线程 | int pthread_detach(pthread_t tid) |
发送信号 | int pthread_kill(pthread_t tid, int sig) |
查看线程标识符
查看当前线程的ID号。
pthread_t pthread_self(void)
|
- 返回值:当前线程ID
具体使用方式如下:
cout << "thread ID:" << pthread_self() << endl;
即可输出当前线程ID。
需要注意的是,某一个线程的ID号通过终端命令top
或者ps -T
查询到的与该函数查询到的线程ID不一定是相同的,不同系统的命名规则不同。pthread_self查询的线程ID是跨平台的标准下定义的,而ps命令查询得到线程ID的是linux下定义的标准命名的。
创建线程
在定义号线程pthread_t tid;
之后,我们就可以使用pthread_create
给线程tid
分配执行代码。具体如下:
int pthread_create(pthread_t * tidp, pthread_attr_t * attr, void *(*start_rtn)(void), void * arg)
参数 | 参数意义 |
---|---|
tidp | 线程ID指针 |
attr | 线程属性 |
start_rtn | 函数指针 |
arg | 传入线程的参数 |
注意:传入线程的参数的生存周期需要比线程的生存周期长,否则在线程还没有使用完毕该参数之前,该参数就会释放掉。
相关代码如下:
#include <iostream>
#include <unistd.h>
#include <pthread.h>
using namespace std;
void* func(void* arg){
for(int i=0; i<5; ++i) cout << getppid() << ":" << getpid() << ":" << pthread_self() << endl;
sleep(3);
}
int main(){
pthread_t tip;
pthread_create(&tip, NULL, func, NULL);
for(int i=0; i<5; ++i) cout << getppid() << ":" << getpid() << ":" << pthread_self() << endl;
sleep(30);
}
执行以上代码j结果如下:
我们发现除了创建线程之外,创建的线程和主线程的执行顺序是错落的,并没有先后顺序,也就是说现线程的执行是并发执行的。
除了以上的创建方式我们还可以通过lamda表达式来创建线程如下:
pthread_create(&tip, NULL, [](void*)->void*{
for(int i=0; i<5; ++i) cout << getppid() << ":" << getpid() << ":" << pthread_self() << endl;
sleep(3);
return new int(2);
}, NULL);
合并线程
合并线程的意思指的是,等待子线程退出,并由当前进程回收子线程资源。
int pthread_join(pthread_t tid, void **retval)
参数 | 参数意义 |
---|---|
tid | 等待结束的线程标识符 |
retval | 一个用户定义的指针,它用来存储被等待线程的返回值 |
该函数的关键点在于保证线程执行过程中使用的变量的生存周期的长度比线程的执行时间长。我们具体来做个实验体验一下,初始代码如下:
#include <iostream>
#include <unistd.h>
using namespace std;
void* inc(void* arg){
int* n = static_cast<int*>(arg);
for(int i=0; i<10; ++i){
++*n;
cout << *n << endl;
}
pthread_exit(new int(100));
}
void* dec(void* arg){
int* n = static_cast<int*>(arg);
for(int i=0; i<10; ++i){
--*n;
cout << *n << endl;
}
return new int(2);
}
int main(){
int n=0;
pthread_t pids[2];
pthread_create(pids,NULL,inc,&n);
pthread_create(pids+1,NULL,dec,&n);
}
执行结果如下:
我们发现执行的结果突然从32587开始,这是因为主线程在的执行会比其他两个线程的执行速度会更快一点,所以在主线程执行结束后,变量n就会被释放,这时变量n中的数将会是一个随机值,所以执行结果就会乱。
我们修改代码如下,将线程合并,最后回收线程资源,具体如下:
#include <iostream>
#include <unistd.h>
using namespace std;
void* inc(void* arg){
int* n = static_cast<int*>(arg);
for(int i=0; i<10; ++i){
++*n;
cout << *n << endl;
}
pthread_exit(new int(100));
// return new int(1);
}
void* dec(void* arg){
int* n = static_cast<int*>(arg);
for(int i=0; i<10; ++i){
--*n;
cout << *n << endl;
}
return new int(2);
}
int main(){
int n=0;
pthread_t pids[2];
pthread_create(pids,NULL,inc,&n);
pthread_create(pids+1,NULL,dec,&n);
// sleep(5);
// 等待子线程退出
for(int i=0; i<2; ++i){
int* res;
pthread_join(pids[i],reinterpret_cast<void**>(&res)); // 等待子
线程退出,并回收子线程资源
cout << "return res:" << *res << endl;
delete res;
}
}
执行结果如下:
这时两个线程并发执行,此时变量n将不会释放,两个子线程在对该变量分别操作10次加加,10次减减之后,变量n的值不会发生改变,依旧是0。
终止线程
终止线程指的就是结束该线程的运行。
void pthread_exit(void* retval)
参数 | 参数意义 |
---|---|
retcal | 函数的返回指针,只要pthread_join 中的第二个参数retval不是NULL,这个值将被传递给pthread_join 中的参数retval 。 |
由于pthread_join
和pthread_exit
的使用联系很紧密,我们将两者结合讲解,具体如下:
-
首先创建线程如下,
pthread_t pid; pthread_create(pid,NULL,inc,&n);
-
然后我们让线程
pid
执行如下函数代码:
void* inc(void* arg){
int* n = static_cast<int*>(arg);
for(int i=0; i<10; ++i){
++*n;
cout << *n << endl;
}
pthread_exit(new int(100));
// return new int(1);
}
- 那么我们在主函数中使用
pthread_join(pid,reinterpret_cast<void**>(&res));
就可以在pthread_join
中的参数res
中收到inc函数的返回值。在某种意义上pthread_exit(new int(100));
相当于return new int(1);
分离线程
分离线程的意思指的是:当前进程将当前线程传给系统其他进程管理,释放该线程的工作交给其他进程。
int pthread_detach(pthread_t tid)
实现代码如下:
#include <iostream>
#include <unistd.h>
#include <pthread.h>
using namespace std;
void* func(void* arg){
int& n = *static_cast<int*>(arg);
for(int i=0; i<10; ++i){
cout << ++n << endl;
}
return new int(2);
}
pthread_t test(){
static int n = 0;
pthread_t tid;
pthread_create(&tid, NULL, func, &n);
return tid;
}
int main(){
pthread_t tid = test();
pthread_detach(tid); // 主线程不等待,子线程由系统回收
sleep(2);
}
此时test函数中的变量n的生存周期将会比线程执行完func函数时间长。detach函数和join函数选择一个使用回收资源即可。
C++11中提供的thread类
使用thread定义一个线程对象:
thread t(函数指针);
我们接下来使用lamda表达式来定义一个thread对象,即创建一个线程,执行代码如下:
#include <iostream>
#include <thread>
using namespace std;
int main(){
thread t([](){
// 创建线程
for(int i=0; i<10; ++i){
cout << "abc" << endl;
}
});
for(int i=0; i<10; ++i){
// 主线程
cout << "123" << endl;
}
t.join();
}
执行结果如下:
注意主线程和创建的子线程的执行顺序是并发的、乱序的。