目录
前言
一、线程创建
1.创建线程
2.线程传递结构体
3.创建多线程
4.收到信号的线程
二、线程终止
三、线程等待
四、线程分离
五、取消线程
六、线程库管理的原理
七、站在语言角度理解pthread库
八、线程的局部存储
前言
前面我们学习了线程概念和线程创建,今天我们学习线程控制,如何操控一个线程完成任务,同时能取消线程、等待线程,分离线程。
一、线程创建
1.创建线程
功能:创建一个新的线程
- 参数 thread:返回线程ID
- attr:设置线程的属性,attr为NULL表示使用默认属性
- start_routine:是个函数地址,线程启动后要执行的函数
- arg:传给线程启动函数的参数
- 返回值:成功返回0;失败返回错误码
#include<iostream>
#include<unistd.h>
#include<pthread.h>
using namespace std;void* TreadToutine(void *arg)
{const char* threadname = (const char*) arg;while(1){cout<<"我是一个新线程"<<threadname<<endl;sleep(1);}
}int main()
{pthread_t tid;pthread_create(&tid,NULL,TreadToutine, (void*)"thread 1");//主线程while(1){cout<<"我是主线程"<<endl;sleep(1);}return 0;
}
linux没有真正的线程概念,他的线程是复用的进程代码,只是做了一些区分。线程客观的可以叫做轻量级进程。因此Linux只会提供轻量级进程创建的函数调用,不会直接提供线程创建的接口。因此我们使用pthread原生线程库,编译时需要手动链接库文件(-lpthread)。
这样编译后就可以运行了。
从上面代码可以看出,给线程传递的信息可以是char*,由于pthread_create函数的最后一个参数为void*,同时线程去运行的函数参数也是void*,因此我们任意类型都可以传递过去,进行一下强转即可。
2.线程传递结构体
比如现在我想传递很多内容过去,叫线程帮我们处理
如下,我们传递了结构体
线程成功收到结构体,并做出了处理。
3.创建多线程
创建多线程也很简单,只需要循环创建即可。
4.收到信号的线程
如果进程创建的线程有一个发生了异常,收到了信号,会导致整个进程都被终止,因为线程是进程创建出来的,发送信号是发给了进程,进程如果退出,那么该进程所有的资源也都得被回收。而线程本身就是进程资源的一部分。
二、线程终止
我们知道线程去执行的函数返回类型为void*,当线程执行结束,return时,线程就自动终止了。
如果我们返回时调用exit()函数 ,那么整个进程都会被终止。
同时,pthread.h库还给我们提供了 pthread_exit() 接口,我们使用该接口也可以终止线程。
pthread_exit()
作用:终止一个运行的线程
参数retval:返回void*的全局变量
注意,pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了。
代码如下,两种方法都可以退出
运行结果如下,线程被退出,不再打印消息。
三、线程等待
线程退出默认要被等待,如果不等待,就会发生类似于僵尸进程的问题。因此我们需要用pthread_join()函数进行等待
pthread_join()
功能:等待线程结束
- 参数1::thread:线程ID
- 参数2:value_ptr:它指向一个指针,后者指向线程的返回值(void**指向的线程返回值void*)
- 返回值:成功返回0;失败返回错误码
代码如下,让子线程程循环5次后退出并传参常量字符串,主线程去join等待,并将等待的结果输出。
#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <cstdio>
using namespace std;class Add
{
public:Add(string name, int a, int b): _name(name), _a(a), _b(b){}public:string _name;int _a;int _b;
};void *TreadToutine(void *arg)
{Add *a1 = (Add *)arg;int cnt = 5;while (cnt--){cout << "我是一个新线程: " << a1->_name << ",计算结果为" << a1->_a + a1->_b << endl;sleep(1);}//return nullptr;pthread_exit((void*)"pthread-1 退出"); //常量区
}int main()
{pthread_t tid;Add *td = new Add("thread-1", 10, 20);pthread_create(&tid, NULL, TreadToutine, td);// 主线程cout << "我是主线程,子线程的tid: "<< tid << endl;void* msg = nullptr;pthread_join(tid,&msg);cout<<"等待成功,子线程退出信息: "<<(char*)msg <<endl;sleep(1);return 0;
}
等待成功,同时输出了消息。注意等待是阻塞式等待,子线程退出后才会执行后续代码。
四、线程分离
我们知道,线程是需要被等待的,不然会发生类似于僵尸进程的现象,那么如果我想让线程一直去运行,比如说一直帮我播放音乐,那么主线程就会一直等待,不可能执行后面的代码。
在这种情况下,我们可以让线程分离,也就是主线程不再关心创建的子线程的死活,他要运行就运行,不运行了操作系统会回收。不过一般建议主线程最后再退出。
可以使用pthread_detach()函数进行线程的分离。
pthread_detach()
作用:分离线程
分离线程很简单,直接调用pthread_detach()就可以,我们不过多展示,下面代码是先分离线程,再等待线程看看会发生什么。
发现等待线程的返回值为22,不是0证明等待失败,22的意思是该线程不需要等待。
这是我们是在主线程进行分离的,子线程也可以被分离, 由于子线程默认看不到自己的tid,因此可以调用pthread_self()函数获取自己的tid。
pthread_self
作用:让线程获取自己的tid
如下是子线程选择分离。
小总结:
- 默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。
- 如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。
五、取消线程
主线程可以取消线程,也就是让子线程退出,可以调用pthread_cancel()函数进行终止线程。
pthread_cancel()
功能:向线程发送取消请求
- 参数1:thread,线程ID
- 返回值:成功返回0;失败返回错误码
代码如下,先取消进程,再等待线程,同时查看线程退出码
#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <cstdio>
using namespace std;void *TreadToutine(void *arg)
{while(1){cout << "我是一个新线程" << endl;sleep(1);}return nullptr;
}int main()
{pthread_t tid;pthread_create(&tid, NULL, TreadToutine, (void *)"pthread-1");sleep(3);// 取消线程int n = pthread_cancel(tid);cout << "线程取消成功,n: " << n << endl;// 等待线程void *ret = nullptr;n = pthread_join(tid, &ret);cout << "等待线程返回值n: " << n << ",线程返回值: " << (int64_t)ret << endl;return 0;
}
运行看到,线程返回值为0,取消成功,等待返回值为0,等待成功。我们看到线程没有阻塞在等待函数这里,而是直接往后运行,同时进程返回为-1。
这是因为如果thread线程被别的线程调用pthread_ cancel异常终掉,value_ ptr所指向的单元里存放的是常数 PTHREAD_ CANCELED
而如果线程先被脱离,再取消,结果怎么样呢?
发现也是能被取消的,但是线程等待是22(等待失败)。因为系统直接回收了。
小总结:
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参数。
六、线程库管理的原理
我们对线程的操作一直要使用tid,那么tid里面的内容到底是什么呢?
其实他是一个地址,我们转成16进程来看一下。
确实是很像是地址,但这跟LWP(Light Weight Process)也不一样啊。该如何理解呢?
- 首先,我们知道pthread. h不是操作系统的接口,而是原生线程库。那么用户创建的线程,操作系统无法管理,则需要线程库来进行管理。他从系统中获取轻量级进程相关属性,从用户中也获取一些属性,这样就先描述起来了,再通过数据结构将线程组织起来,就将线程管理好了。
- 我们也知道,线程要有独立属性,独立的主要有硬件上下文和栈空间,其中硬件上下文跟操作系统有关,而栈空间则是要从用户中来。栈不是只有一个吗?为什么每一个线程都有自己的栈空间呢?这其实是操作系统帮我们处理了的,操作系统会在堆区创建空间,来充当线程独立的栈。pthread库会获取到栈空间,并将他管理维护好,而默认地址空间中的栈,由主线程使用。
那么线程库如何管理呢,在哪管理呢?
- 在进程地址空间中,mmap(共享区)加载了动态库,其中我们使用的pthread库就在该区域,他会管理好每一份线程,每一份线程都在其中有自己的属性集。
- struct pthread里存在很多线程属性,线程局部存储,还有线程栈,这个栈指向的是堆空间的区域,每当有新线程被创建,都会在后面继续创建这种数据结构。就这样将多个线程统一的描述组织起来了,可以进行管理了。因此我们调用pthread相关函数,相当于对该空间进行访问、处理。
那么现在,我们也可以理解 pthread_t tid 是什么了,他不就是每一个线程在进程地址空间的起始地址嘛,我们pthread_create 对tid进行写入,因为需要创建对应的数据结构,找到起始地址,然后返回,后续用户要继续对线程进行控制,等待啊,终止啊,分离啊,取消啊。都需要传入tid,也就是能找到在进程地址空间的位置后,才可以处理。
七、站在语言角度理解pthread库
我们之前学的pthread库,是Linux提供的原生线程库,在语言层面,比如C++/JAVA\PYTHON,他们也会提供给我们线程库。
我们写了一份代码,使用的是C++提供的线程库 thread
#include<iostream>#include<unistd.h>#include<thread>using namespace std;void myrun()
{while(1){cout<<"我是一个新线程"<<endl;sleep(1);}
}int main(){thread t(myrun);t.join();}
编译后运行,发现说多线程操作被禁止了,这是因为我们没有链接pthread库。
c++提供的线程库封装了pthread.h。因此我们编译时仍然需要链接 pthread库。
到现在,我们可以知道,语言上也许线程库的使用不一定相同,但是他们底层都是用的linux原生线程库。
在Linux下做了封装,那么这段代码我们可以在Linux中运行。
如果thread头文件在Windows下,封装了Windows线程的操作,那么也可以在Windows下运行。这大大提高了文件的可移植性。
八、线程的局部存储
我们定义一个全局变量,创建线程,让新线程对全局变量做++,观察新线程和主线程全局变量是否发生变化。
#include <iostream>
#include <unistd.h>
#include <pthread.h>
using namespace std;int g_val = 100;void *TreadToutine(void *arg)
{while (1){cout << "我是一个新线程,g_val: " << g_val << ",&g_val: " << &g_val << endl;g_val++;sleep(1);}return nullptr;
}int main()
{pthread_t tid;pthread_create(&tid, NULL, TreadToutine, (void *)"Thread 1");while (1){cout << "我是一个主线程,g_val: " << g_val << ",&g_val: " << &g_val << endl;sleep(1);}pthread_join(tid,nullptr);
}
我们可以看到,全局变量值一样,地址也一样,我们现在知道全局变量是被所有进程共享的。
如果我们给全局变量前添加上__thread,GCC/G++编译器提供的一个扩展,用于声明线程局部存储变量。
现在运行,主线程和新线程g_val不一样,地址也不一样。
因为我们添加的__thread 会在G++编译时,给每个线程的局部存储空间里将变量拷贝进程,私有一份,于是每个线程自己管理自己的那一份资源。不再与外部共享。
只是__thread只能修饰内置类型,如string这种自定义类型无法处理。