Linux 线程控制

发布于:2025-03-20 ⋅ 阅读:(20) ⋅ 点赞:(0)

目录

1、创建线程

2、线程等待

3、终止线程

4、线程等待

5、传递对象

6、C++11中的线程

7、线程ID与线程地址空间布局

8、验证结论

(1)各个线程有独立的栈结果

(2)主线程能拿到其中一个线程的变量

9、__thread关键字

10、分离线程


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、线程等待

那么这两个线程谁先进行退出呢?一般来说是新线程先退出的,然后主线程才能退出的,因为是主线程创建的它,它要对这个新线程进行管理。

如果我们主线程是一个死循环,而新线程一直不退出,那么也会造成类似于进程中的僵尸进程的问题(当然线程里没有这个说法)。所以新线程被创建出来以后,一般也要被等待,如果不等待,可能会造成类似于僵尸进程的问题。当然这个问题我们是无法验证出来的,因为新线程一退,我们查也就查不到了。但是确确实实会存在这个问题。

更重要的是,我们将新线程创建出来,就是让他就办事的,我们得知道它办的怎么样,结果数据是什么?

所以我们线程等待的两个目的:

  1. 防止内存泄漏
  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,说明等待失败了,然后返回,进程终止。其实一个线程被设置为分离状态,则该线程不应该被等待,如果被等待了,结果是未定义的,至少一定会等待出错。