目录
1、创建线程
- thread:输出型参数,返回线程 ID。
- attr:设置线程的属性,attr 为 NULL 表示使用默认属性。
- start_routine:想让线程执行的任务,它是一个返回值 void*,参数 void* 的一个函数指针。
- arg:回调函数的参数,若线程创建成功,在执行 start_routine 时,会把 arg 传入start_routine
返回值:成功返回0,失败返回非0,数字是几,代表什么原因出错。
- 传统的一些函数是,成功返回 0,失败返回 -1,并且对全局变量 errno 赋值以指示错误。
- pthreads 函数出错时不会设置全局变量 errno(而大部分其他 POSIX 函数会这样做),而是将错误代码通过返回值返回。
- pthreads 同样也提供了线程内的 errno 变量,以支持其它使用 errno 的代码。对于pthreads 函数的错误,建议通过返回值业判定,因为读取返回值要比读取线程内的 errno 变量的开销更小
代码如下:
#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;
void* threadrun(void* args)
{
while(1)
{
cout << "pthread is running,pid is:" << getpid() << endl;
sleep(1);
}
return nullptr;
}
int main()
{
pthread_t rid;
pthread_create(&rid,nullptr,threadrun,nullptr);
while(1)
{
cout << "main thread is running,pid is" <<getpid() << endl;
sleep(1);
}
return 0;
}
这里让新线程执行除 0 操作,我们发现它会影响整个进程。线程是进程的一个执行分支,除 0 错误操作会导致线程退出的同时,也意味着进程触发了该错误,进而导致进程退出。这也就是线程会使用代码健壮性降低的一个表现。
我们能看到这个线程崩了,整个进程会一起被干掉。
我们看这段代码
运行结果如下:
两个执行流都进入了show函数,就如我们上一篇写的,我们把show函数称作为可重入函数。
我们再来看一段代码
运行结果:
我们可以看到,主线程和新线程都可以看到这个变量被修改了。说明两个线程共享这个变量。
所以两个线程想要进行通信实在是太容易了
这里我们注意,如果我们设置gal为int的时候,会出现这样的报错
这是因为int是4个字节,我们传入第四个参数的时候,是void* 64机器下为8个字节,强转的类型大小不一样可能会报错,所以我们用long,long类型在32位下是4个字节,64位下是8个字节。
2、线程等待
那么这两个线程谁先进行退出呢?一般来说是新线程先退出的,然后主线程才能退出的,因为是主线程创建的它,它要对这个新线程进行管理。
如果我们主线程是一个死循环,而新线程一直不退出,那么也会造成类似于进程中的僵尸进程的问题(当然线程里没有这个说法)。所以新线程被创建出来以后,一般也要被等待,如果不等待,可能会造成类似于僵尸进程的问题。当然这个问题我们是无法验证出来的,因为新线程一退,我们查也就查不到了。但是确确实实会存在这个问题。
更重要的是,我们将新线程创建出来,就是让他就办事的,我们得知道它办的怎么样,结果数据是什么?
所以我们线程等待的两个目的:
- 防止内存泄漏
- 如果需要,我们也可以获取一下子进程的退出结果
下面是线程等待的函数
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
//Compile and link with -pthread.
如果成功返回0,失败返回错误码。注意:线程里面所有的函数都不用errno错误码,而是直接返回一个错误码。这就保证了所有的线程都可以有一个返回的错误码,不需要去抢占全局的那个变量
关于参数:
第一个参数是线程的tid
第二个参数是该线程结束时的返回值。注意*retval才是void*类型,也就是*retval才是函数的返回值。
如下图所示,当void*通过pthread_join的方式传递的时候,会产生一个临时变量。比如说,我们调用函数的时候传递&x,那么&x其实会被拷贝一份,我们这里暂且记作retavl。然后在pthread_join内部执行,*retval = z这一步。最终就成功的为x赋值了。即x就相当于一个输入型参数。
下面我们用代码演示一下
我们让新创建的线程退出,让主线程等待。主线程等待5秒,主线程也退出
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <string>
using namespace std;
void show(const string& name)
{
cout << name << "is running" << endl;
}
void* threadrun(void* args)
{
long gal = (long)args;
int cnt = 8;
while(cnt--)
{
show("[new pthread]");
// cout << "pthread is running,pid is:" << getpid() << "gal:" << gal++ <<" "<< "&gal:" << &gal << endl;
sleep(1);
}
// int a = 10;
// a /= 0;
return nullptr;
}
int main()
{
long gal = 0;
pthread_t rid;
pthread_create(&rid,nullptr,threadrun,(void*)gal);
void* retval;
pthread_join(rid,&retval);
sleep(5);
cout<< "main pthread quit!" << endl;
// while(1)
// {
// show("[main pthread]");
// // cout << "main thread is running,pid is" <<getpid() << "gal:" << gal++ <<" "<< "&gal:" << &gal << endl;
// sleep(1);
// }
return 0;
}
运行结果如下:
我们现在利用一下这个*retavl
运行结果如下:
这里我们让新线程退出为什么不能用exit接口呢?
我们先来看看运行结果是什么样的
我们看到主线程都没等待就全部退出了。
其实exit是用来终止进程,不能用来终止线程。
接下来我们来看终止线程的接口
3、终止线程
#include <pthread.h>
void pthread_exit(void *retval);
//Compile and link with -pthread.
它的作用是终止调用这个函数的线程,谁调用它就终止谁。参数是void*,和这个函数的返回值的含义是一样的。
运行结果如下
上面是新线程去调用pthread_exit接口,那么只有这个线程会退出,如果主线程去调用这个接口退出的话,那么整个进程都会终止
运行结果人如下:进程直接退出了,没有进行等待。
4、线程等待
#include <pthread.h>
int pthread_cancel(pthread_t thread);
//Compile and link with -pthread.
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <string>
using namespace std;
void* runpthread(void*args)
{
string name = (char*)args;
int cnt = 5;
while(cnt--)
{
cout << name << "pid:" << getpid() << endl;
sleep(1);
}
}
int main()
{
pthread_t rid;
pthread_create(&rid,nullptr,runpthread,(void*)"pthread-");
sleep(1);
pthread_cancel(rid);
void* retval;
pthread_join(rid, &retval); //main thread等待的时候,默认是阻塞等待的
cout << "main thread quit..., ret: " << (long)retval << endl;
return 0;
}
我们可以注意到,此时这个线程等待以后的返回值为-1
其实是因为一个线程如果被取消的话,会有这样一个宏
#define PTHREAD_CANCELED ((void *) -1)
换句话说,如果线程是被取消的,那么它退出时的返回码就是-1,即上面的宏
5、传递对象
其实线程的参数和返回值,不仅仅可以用来传递一般参数,也可以传递对象
我们可以用下面的代码来看
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <string>
using namespace std;
class Request
{
public:
Request(int start,int end,string name):start_(start),endl_(end),pthread_name(name)
{}
public:
int start_;
int endl_;
string pthread_name;
};
class Response
{
public:
Response(int result,int exitcode):result_(result),exitcode_(exitcode)
{}
public:
int result_;
int exitcode_;
};
void* sumcount(void* args)
{
Request* rq = (Request*)args;
Response* rep = new Response(0,0);
for(int i = rq->start_; i <=rq->endl_; i++)
{
cout << rq->pthread_name << " is running calling "<< i<< endl;
rep->result_ += i;
}
delete rq;
return (void*)rep;
}
int main()
{
pthread_t tid;
Request* rq = new Request(1,100,"pthread-1");
pthread_create(&tid,nullptr,sumcount,rq);
void* ret;
pthread_join(tid,&ret);
Response* rep = (Response*)ret;
cout << "rep->result_:" << rep->result_ << ",exitcode:" << rep->exitcode_ <<endl;
delete rep;
return 0;
}
运行结果如下:
所以它就可以用来求出和。让每一个线程只执行其中的一部分计算,然后我们自己在将这些结果合并起来。
并且我们发现,我们的这些对象都是在堆区创建的。并且我们是交叉使用的,说明堆空间的也是被线程共享使用的
6、C++11中的线程
目前,我们使用的是原生线程库(pthread库)
其实C++11 语言本身也已经支持多线程了,它与我们的原生线程库有什么关系呢?
C++11的线程需要用下面的库
#include<thread>
代码如下:
#include <iostream>
#include <pthread.h>
#include <string>
#include <cstdlib>
#include <unistd.h>
#include <thread>
using namespace std;
void threadrun()
{
while(true)
{
cout << "I am a new thread for C++" << endl;
sleep(1);
}
}
int main()
{
thread t1(threadrun);
int cnt = 5;
while(cnt--)
{
cout << "main pthread is running " <<endl;
sleep(1);
}
t1.join();
return 0;
}
运行结果:
我们需要注意的是,C++11中的线程库其实底层还是封装了linux提供的系统调用接口,所以我们编译的时候还是需要使用-lpthread选项的。
而C++11其实是有跨平台性的。因为它在不同平台下已经写好了不同版本的库。所以对我们而言,不同的平台写代码是没有感觉的。
我们最好使用C++的多线程。因为具有跨平台性
7、线程ID与线程地址空间布局
我们先来看一段代码
#include <iostream>
#include <pthread.h>
#include <string>
#include <cstdlib>
#include <unistd.h>
#include <thread>
using namespace std;
void* runpthread(void* args)
{
string name = (char*)args;
printf("%s, tid:%p\n", name.c_str(), pthread_self());
return nullptr;
}
int main()
{
pthread_t tid;
pthread_create(&tid,nullptr,runpthread,(void*)"pthread-1");
printf("main create a new pthread id is:%p\n", pthread_self());
pthread_join(tid,nullptr);
return 0;
}
运行结果如下:
我们能看到这个tid地址很大,那它具体存放在哪呢?
我们知道的是,内核中并没有明确的线程的概念,只有轻量级进程的概念
而轻量级进程接口是这样的
这个接口我们一般是不用的,包括fork的底层其实用的也是这个接口
这个的第一个参数是一个函数指针,第二个参数是自定义的一个栈…
这个接口是被pthread线程库封装了。
所以我们采用的是pthread_create,pthread_join这些接口。
如下图所示,这个clone这个接口它需要提供一个回调函数,独立栈结构等,用它去维护线程。而这些都是线程库在做的事情,也就是线程的概念是库给我们维护的,我们用的原生线程库,也要加载到内存中,因为都是基于内存的。线程库是一个动态库,经过页表映射后,也要到共享区的。这些栈都是在共享区创建的。我们的线程库只需要维护线程的概念即可,不用维护线程的执行流,不过线程库注定了要维护多个线程属性集合,线程也要管理这些线程,先描述在组织。而这个线程控制块它就要可以找到这些回调函数,独立栈,以及在内部的LWP。这个线程控制块就是用户级线程
所以我们就将这个下面的这个叫做线程的tcb。而每一个tcb的起始地址,叫做线程的tid
所以拿着这个tid,就可以找到库里面的属性了。
而我们前面打印出来的这个地址,我们也可以看到,它是比较大的,其实它就是介于堆栈之间的共享区
每一个线程都必须要有自己的独立栈结构,因为它有独立的调用链,要进行压栈等操作。其中主线程用的就是地址空间中的这个栈。剩下的轻量级进程在我们创建的时候会先创建一个tcb,它里面的起始地址作为线程tid,它的里面有一个默认大小的空间,叫做线程栈,然后内核中调用clone创建好执行流。在clone中形成的临时数据都会压入到这个线程库中的栈结构中。
所以除了主线程,所有其他线程的独立栈,都在共享区,具体来讲是在pthread库中,tid指向的用户tcb中,这个tid是这个线程所在动态库的起始地址,tid是虚拟地址。
总结:
- Linux OS 没有真正意义上的线程,而是用进程 PCB 模拟的,这就叫作轻量级进程。其本身没有提供类似线程创建、终止、等待、分离等相关 System Call 接口,但是会提供轻量级进程的接口,如 clone。所以为了更好的适配,系统基于轻量级进程的接口,模拟封装了一个用户层的原生线程库 pthread。这样,系统通过 PCB 来进行管理,用户层也得知道线程 ID、状态、优先级等其它属性用来进行用户级线程管理。
- pthread_create 函数会产生一个线程 ID,存放在第一个参数指向的地址中,该线程 ID 和前面说的线程 ID LWP 不是一回事。LWP 属于进程调度的范畴,因为线程是轻量级进程,是 OS 调度器的最小单位,所以需要一个数值来唯一表示该线程。pthread_create 函数的第一个参数指向一个虚拟内存单元,该内存单元的地址即为新创建线程的线程 ID,属于 NPTL 线程库的范畴,线程库的后续操作,就是根据该线程 ID 来操作线程。
- 原生线程库是一个库,它在磁盘上就是一个 libpthread.so 文件,运行时加载到内存,然后将这个库映射到共享区,此时这个库就可以被所有线程执行流看到了。此时有两个 ID 概念,一个是在命令行上看到的 LWP,一个是在用户层上看到的 tid。前者是在系统层面上供 OS 调度的,后者是 pthread_create 获得的线程 ID,它是一个用户层概念,本质是一个地址,就是 pthread 库中某一个起始位置,也就是对应到共享区中的某一个位置。所以线程数据的维护全都是在 pthread 线程库中去维护的,上图所示,其中会包含每个线程的局部数据,struct pthread 就是描述线程的 TCB,线程局部存储可以理解是不会在线程栈上保存的数据,我们在上面说过线程会产生各种各样的中间数据,如上下文数据,此时就需要独立的栈去保存,它就是线程栈。而下图中拿到的 tid 就是线程在共享区中线程库内的相关属性的起始地址,所以只要拿到了用户层的 tid,就可以在库中找到线程相关的属性数据,很明显 tid 和 LWP 是 1 : 1 的,而主线程不使用库中的栈结构,直接使用地址空间中的栈区,称为主线程线。
8、验证结论
(1)各个线程有独立的栈结果
#include <iostream>
#include <pthread.h>
#include <string>
#include <cstdlib>
#include <unistd.h>
#include <thread>
#include <vector>
using namespace std;
void* threadrun(void* args)
{
long gal = (long)args;
while(true)
{
cout << "pthread is running,tid is:" << pthread_self() << "gal:" << gal++ <<" "<< "&gal:" << &gal << endl;
sleep(1);
}
}
int main()
{
vector<pthread_t> tids;
long flag = 0;
for(int i = 0; i < 5; i++)
{
pthread_t tid;
pthread_create(&tid,nullptr,threadrun,(void*)flag);
tids.push_back(tid);
}
for(auto& e:tids)
{
pthread_join(e,nullptr);
}
return 0;
}
运行结果如下:
我们能看到传入到每个线程的falg地址不一样,但它们地址又很相近。
说明了每一个线程都有自己的栈,该栈存放该执行流创建的变量等,并且他们都在共享区的动态库中。
(2)主线程能拿到其中一个线程的变量
#include <iostream>
#include <pthread.h>
#include <string>
#include <cstdlib>
#include <unistd.h>
#include <thread>
#include <vector>
using namespace std;
int *p = NULL;
struct pthreadname
{
string threadname;
};
void InitThreadData(pthreadname *td, int number)
{
td->threadname = "pthread-" + to_string(number); // thread-0
}
void* threadrun(void* args)
{
int test_i = 0;
pthreadname* name = (pthreadname*)args;
if(name->threadname == "pthread-2")
{
p = &test_i;
}
int cnt = 3;
while(cnt--)
{
cout << name << "tid is:" << pthread_self() << " test_i:" << test_i << " &test_i" << &test_i <<endl;
// cout << "pthread is running,tid is:" << pthread_self() << "gal:" << gal++ <<" "<< "&gal:" << &gal << endl;
sleep(1);
}
}
int main()
{
vector<pthread_t> tids;
for(int i = 0; i < 5; i++)
{
pthreadname* name = new pthreadname;
pthread_t tid;
InitThreadData(name,i);
pthread_create(&tid,nullptr,threadrun,name);
tids.push_back(tid);
}
sleep(5);
cout << "p:" << p << endl;
for(auto& e:tids)
{
pthread_join(e,nullptr);
}
return 0;
}
运行结果如下:
9、__thread关键字
上面我们验证了主线程能拿到其中一个线程的变量,说明进程与进程之间没有秘密。
我们可以用__thread关键字让一个线程有自己的私有全局变量。
__thread int g_iThreadCount = 0;
这实际是线程的局部存储。
让我们写段代码验证一下
#include <iostream>
#include <pthread.h>
using namespace std;
//一个用__thread关键字修饰的全局变量
__thread int g_iThreadCount = 0;
void *pthreadFunc1(void *pArg)
{
g_iThreadCount += 1;
cout << "pthreadFunc1::g_iThreadCount = " << g_iThreadCount << endl;
pthread_exit((void *)1);
}
void *pthreadFunc2(void *pArg)
{
g_iThreadCount += 2;
cout << "pthreadFunc2::g_iThreadCount = " << g_iThreadCount << endl;
pthread_exit((void *)2);
}
int main(void)
{
int iRet;
pthread_t pthreadId1;
pthread_t pthreadId2;
pthread_create(&pthreadId1, NULL, pthreadFunc1, NULL);
pthread_create(&pthreadId2, NULL, pthreadFunc2, NULL);
pthread_join(pthreadId1, NULL);
pthread_join(pthreadId2, NULL);
return 0;
}
运行结果如下:
注意:__thread只能够定义内置类型,不能定义自定义类型。
10、分离线程
- 默认情况下,新创建的线程是 joinable 的,线程退出后,需要对其进行 pthread_join 操作,否则无法释放资源,从而造成内存泄漏。
- 如果不关心线程的返回值,join 则是一种负担,这个时候,可以使用分离,此时就告诉系统,当线程退出时,自动释放线程资源,这就是线程分离的本质。
- joinable 和 pthread_detach 是冲突的,也就是说默认情况下,新创建的线程是不用 pthread_detach。
- 就算线程被分离了,也还是会和其它线程影响的,因为它们共享同一块地址空间。
可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离:
joinable 和分离是冲突的,一个线程不能既是 joinable 又是分离的。
注意:没有线程替换这种操作,但可以在线程中执行进程替换系列函数。这是因为新线程内部执行进程替换函数,这看起来像是把新线程中的代码替换了,但实际会把主线程中的代码也替换了,因为主线程和新线程共享地址空间,所以新线程内部进程替换后,所有的线程包括主线程都会被影响。所以轻易不要在多线程中执行进程替换函数。
我们来看一段代码
#include <iostream>
#include <pthread.h>
#include <string>
#include <cstdlib>
#include <unistd.h>
#include <thread>
#include <vector>
#include <cstdio>
#include <cerrno>
#include <cstring>
using namespace std;
int flag = 0;
void* runthread(void*args)
{
pthread_detach(pthread_self());
while(true)
{
cout << (char*)args << ": " << flag++ << "&" <<&flag<<endl;
sleep(1);
break;
}
pthread_exit((void*)11);
}
int main()
{
pthread_t tid;
pthread_create(&tid,nullptr,runthread,(void*)"pthread-1");
while(true)
{
cout<<"main thread:"<<flag<<" &" <<&flag<<endl;
sleep(1);
break;
}
int n =pthread_join(tid,nullptr);
cout << "n:" << n << "errstring: " << strerror(n) <<endl;
return 0;
}
结果如下:
pthread_join 返回的是 22,说明等待失败了,然后返回,进程终止。其实一个线程被设置为分离状态,则该线程不应该被等待,如果被等待了,结果是未定义的,至少一定会等待出错。