Linux系统编程——线程控制

目录

一,关于线程控制

二,线程创建

2.1 pthread_create函数

2.2 ps命令查看线程信息

三,线程等待

3.1 pthread_join函数

 3.2 创建多个线程

3.3 pthread_join第二个参数 

四,线程终止

4.1 关于线程终止

4.2 pthread_exit线程退出

4.3 pthread_cancel线程取消

五,线程分离

七,线程ID和进程地址空间布局

7.1 线程ID与LWP

7.2 地址空间共享区中的线程栈

7.3 代码验证几个问题

八,pthread_create第四个参数传类的对象


一,关于线程控制

  1. 在线程概念章节,我们已经说过:Linux内核中没有明确的线程的概念,只有轻量级进程的概念,所以Linux没有给我们提供线程的系统调用,只有轻量级进程的系统调用
  2. 但又因为没有系统接口,用户用起来就比较犯难,所以他也必须给我们提供一套完整的线程接口。于是Linux设计者在用户层实现了一套线程方案,以动静态库的方式提供给用户进行使用 --> pthread线程库(也叫POSIX线程库或者原生线程库)
  3. 原生指的是大部分的Linux系统默认内置该线程库,使用这些库函数需要包含头文件"#include<pthread.h>",同时在编译链接时需要加上"-lpthread"选项
  4. 并且由于原生线程库是第三方库,不属于系统接口,那么pthread函数出错时不会设置errno全局变量,而是直接用返回值来告诉用户执行结果

 下面是常规情况下使用原生线程库的makefile内容:

mythread:mythread.cc
	g++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:
	rm -f mythread

二,线程创建

2.1 pthread_create函数

pthread_create就是的创建线程的函数了,在man手册中函数声明如下:

 解释下四个参数:

  1. 第一个参数是输出型参数,负责返回成功创建的新线程的id,pthread_t也是一个unsigned long int
  2. attr表示设置创建线程的属性,我们目前只要传入nullptr即可,表示不添加其它属性
  3. 第三个参数很明显是一个函数指针,该指针指向一个参数为void*,返回值为void*的函数
  4. 第四个参数arg表示要传入第三个函数指针参数的值,这个arg不仅仅可以传内置类型参数,也可以传C++类的对象,后面有代码

下面是用pthread_create函数创建线程,然后打印pid的代码:

#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;

void show(const string &name)
{
    cout << name << ", pid: " << getpid() << "\n" << endl; // 在一个进程里面获取pid
}

void *threadRun(void *args)
{
    const string name = (char *)args;
    while (true)
    {
        show(name);
        sleep(1);
    }
}

int main()
{
    pthread_t tid;
    char name[64];
    snprintf(name, sizeof name, "new thread");              // 以特定格式化,把内容搞到字符串里去,然后创建进程时把该字符串传过去
    pthread_create(&tid, nullptr, threadRun, (void *)name); // 新线程就跳转过去执行这个函数,主线程就继续向下运行
    while (true)
    {
        cout << "I am main thread" << ", pid: " << getpid() << endl;
        sleep(1);
    }
}

 打印结果如下:

 

可以发现,两个执行流打印的进程pid一样,可以证明确实创建了线程 

2.2 ps命令查看线程信息

先让上面的进程挂着,可以另起一个窗口,先查看进程的

ps -ajx | head -1 && ps ajx | grep mythread

发现只有一条,正常,因为我们创建的是线程不是进程,要查看线程信息需要使用ps -aL 

ps -aL | head -1 && ps -aL | grep mythread

 其中PID就是进程的PID,LWP(Light Weight Process)就是线程的ID了,其中最小的就是主线程,主线程的LWP值和PID一样,LWP也存在task_struct里。

所以CPU调度时本质是看的LWP,由于我们前面说的是看的PID,那其实是一个进程只有一个线程的这种特殊情况,所以在CPU看来,LWP和PID其实是一样滴

三,线程等待

3.1 pthread_join函数

线程和进程一样,线程也是需要进行等待的,如果主线程不等待新线程,就会引起类似僵尸进程得问题,导致内存泄漏。线程等待得函数为pthread_join,下面是函数声明

 参数解释:

  1. 第一个参数thread表示要等待得线程得ID
  2. 第二个参数retval表示线程退出时得退出码信息

 3.2 创建多个线程

和进程一样,我么也可以创建多个线程并进行等待,如下代码:

#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <string>
#include <cstdio>
using namespace std;

void show(const string &name)
{
    cout << name << ", pid: " << getpid() << endl; // 在一个进程里面获取pid
}

void *threadRun(void *args)
{
    const string name = (char *)args;
    for (int i = 0; i < 5; i++)
    {
        show(name);
        sleep(1);
    }
    return nullptr;
}

int main()
{
    pthread_t tid[5]; // 创建5个线程,这个pthread_t类型也是一个unsigned long int
    char name[64];
    for (int i = 1; i <= 5; i++)
    {
        snprintf(name, sizeof name, "%s-%d", "thread", i);         // 以特定格式化,把内容搞到字符串里去,然后创建进程时把该字符串传过去
        pthread_create(tid + i, nullptr, threadRun, (void *)name); // 新线程就跳转过去执行这个函数,主线程就继续向下运行
    }
    for (int i = 1; i <= 5; i++)
    {
        int a = pthread_join(tid[i], nullptr);
        if (a == 0)
        {
            cout << "thread-" << i << " quit" << endl;
        }
        else
        {
            cout << "thread-" << i << " quit error: " << a << endl;
        }
    }
}

上面代码的逻辑大致是:一次性创建5个线程,每个线程各自打印部分内容,主线程等待,5秒过后,5个线程全部退出,和进程等待一样 

3.3 pthread_join第二个参数 

前面提到过:①原生线程库函数是通过返回值来告诉用户退出结果的    ②pthread_join的第二个参数可以获取线程退出码

但其实,第二个参数说明白点其实是获取线程退出信息,“获取退出码”其实是代表退出信号恰好是数字,所以我们可以用第二个参数获取其它信息,如下代码:

#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;
// 1,线程谁先运行与调度器有关
// 2,线程一旦异常, 就可能导致整个进程崩溃:所以,所有线程共用一个寄存器的标记位
// 3,线程的输入值和返回值问题
// 4,线程异常退出的理解

void *threadRoutine(void *args)
{
    int *data = new int[10];
    for (int i = 0; i < 10; i++)
    {
        cout << "新线程:" << (char *)args << " running " << i << endl;
        sleep(1);
        data[i] = i;
        // int a = 100;
        // a /= 0;
        // 线程出现除0异常,进程也随之终止
    }
    // exit(10); exit是进程退出,所以不要在线程里轻易调用exit
    //  return (void *)10; // 这个返回值一般返回给主线程,返回给pthread_join的第二个参数,直接保存在ret里
    return (void *)data; // data保存i的值,返回给主线程
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRoutine, (void *)"thread");

    int *ret = nullptr;               // 这里的ret属于指针变量。可以存值
    pthread_join(tid, (void **)&ret); // 默认是阻塞等待新线程退出,不关心线程异常退出,所以线程的健壮性会差一些
    // cout << "main thread wait done, main quit, " << (long long)ret << endl;    //64位下指针是8字节,指针是四字节,所以用long long
    for (int i = 0; i < 10; i++) // 打印线程返回给主线程的执行结果
    {
        cout << ret[i] << " ";
    }
    cout << endl;
    return 0;
}

上面代码大致逻辑是: 创建一个线程,然后在线程里new一个数组,然后循环打印,并且在每次循环就往数组里写一个数组,最后线程执行完后,把数组的指针返回给主线的pthread_join的第二个参数,然后打印这个数组,结果如下:

所以我们可以感觉到这些传来传去的值,其实*度很高,而这都是void*这个特殊类型的优势所在,后面会专门讲下void*相比其它类型指针的优势

四,线程终止

4.1 关于线程终止

  1. 线程执行进程代码的一部分,换到代码上就是执行一个函数,而一个函数常见的退出方式有三种:return,exit,信号退出。
  2. return是线程的正常退出,对其它线程没有影响,但是如果是exit,那么就是进程退出,会把所有的线程全部给退出掉,而且发生异常例如除0错误,是信号退出,信号退出也是进程退出
  3. 所以主线的pthread_join等待线程函数不关心线程的异常退出信息,因为线程如果出异常,那么全没了,再等待也没有意义了;而且主线程return时,其它线程也会退出,因为主线程一旦退出,其它线程赖以生存的地址空间资源也就全释放了,所以也会退出
  4. 所以要想线程退出并且不影响其它线程,有三种方法:return,pthread_exit和pthread_cancel,就是我们接下来要讲的

4.2 pthread_exit线程退出

使用起来很简单,功能就是终止当前,但是由于线程结时无法返回退出信息给它的调用者(因为是干掉自身,所以返回值无意义),所以pthread_exit和return返回的指针必须是全局的或者是malloc从堆上申请的,不能返回线程的独立栈上的数据,因为线程退出也会销毁栈

如下代码:

#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;

void *threadRun(void *args)
{
    cout << "I am new thread, my quit number is: 666" << endl;
    pthread_exit((void *)666);
}

int main()
{
    pthread_t tid;
    void *ret = nullptr;
    pthread_create(&tid, nullptr, threadRun, nullptr);
    pthread_join(tid, &ret);
    cout << (long long)ret << endl;
}

代码的逻辑很简单,线程函数直接调用pthread_exit退出,然后将666作为返回值返回,然后主线程的pthread_join捕捉到,就拿到了线程的返回值“666”

4.3 pthread_cancel线程取消

上面的pthread_exit是线程自己退出自己,其实使用起来比较别扭,而且容易出错,所以我们可以用pthread_cancel线程取消函数,这个函数可以指定线程ID取消,相比上面线程退出,线程取消灵活性更高,我们可以直接在主线程像类似“远程操控”一样控制线程退出

如下代码:

#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;

void *threadRoutine(void *args)
{
    int i = 0;
    // int *data = new int[10];
    cout << "新线程:" << (char *)args << " running " << endl;
    while (true)
    {
        sleep(1);
    }
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRoutine, (void *)"thread 1");
    for (int i = 5; i > 0; i--)
    {
        cout << "线程取消倒计时: " << i << endl;
        sleep(1);
    }
    pthread_cancel(tid); // 直接取消线程
    int *ret = nullptr;
    pthread_join(tid, (void **)&ret); // 线程被取消,join的时候,退出码是-1 --> #define PTHREAD_CANCELED ((void *)-1)
    cout << "新线程退出,退出码为:  " << (long long)ret << endl;
}

五,线程分离

  1. 其实线程分离的概念和进程分离的概念是高度重合的,都是“如果不关心线程退出情况”,就不再需要pthread_join等待了,因为在不关心线程退出的情况下,join反而是一种负担,会降低整体效率,毕竟是阻塞式等待的
  2. 分离线程后,该线程依旧要使用进程的资源,崩溃时也要退出,只是该线程不再需要join等待了,线程分离后也不会造成“僵尸线程”,系统会自动回收该线程的PAB资源
  3. 分离和join是冲突的,只能存在一个

 

 下面是线程分离的样例代码:

#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;

void *threadRoutine(void *args)
{
    pthread_detach(pthread_self()); // pthread_self()返回自己线程的id
    cout << "新线程:" << (char *)args << " running " << endl;
    sleep(3);
    cout << "新线程退出" << endl;
    pthread_exit((void *)666);
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRoutine, (void *)"thread 1");
    sleep(5);
    cout << "主线程退出" << endl;
}

七,线程ID和进程地址空间布局

7.1 线程ID与LWP

先阐明一个事实:线程ID和LWP没关系

下面是创建一个线程,然后获取线程ID,再以16进制打印出来,如下代码:

#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;

std::string toHex(pthread_t tid) // 把tid转16进制
{
    char hex[64];
    snprintf(hex, sizeof(hex), "%p", tid);
    return hex;
}

void *threadRoutine(void *args)
{
    while (true)
    {
        cout << "thread id: " << toHex(pthread_self()) << endl;
        sleep(1);
    }
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRoutine, (void *)"thread 1");
    pthread_join(tid, nullptr);
    return 0;
}

 

  • pthread_create函数会产生一个线程ID,存放在第一个参数指向的空间中
  • 这个ID与LWP是两回事,LWP属于进程调度的范畴,因为线程是操作系统调度的最基本单位,所以需要一个数值来唯一表示,类似线程PID
  • pthread_create函数的第一个参数获取的数值与pthread_self()函数返回的数值是一样的

7.2 地址空间共享区中的线程栈

谈论这个要结合上面提到的线程ID

  1. 因为我们用的不是Linux自带的创建线程的接口,我们用的是pthread库中的接口,所以线程的概念是库给我们维护的,而且线程库不用维护线程的执行流,我们用的原生线程库,也是要加载到内存里的,加载到地址空间里
  2. 库的加载,默认是动态链接的,先把磁盘上的pthread库加载到内存里,然后通过页表映射到共享区,当我想调用库,就直接从代码区跳转到共享区,(如果想访问系统接口,就直接跳转到内核区然后通过内核级页表找到内核代码)
  3. 由于OS没有具体的关于线程的概念,所以关于线程的各种属性要由库来管理,而要管理,就要“先描述,再组织”,所以库就在共享区中为每一个线程都创建了对应的用户层线程的数据集合,这个集合里就包含了线程栈,而由于地址空间是线性的,所以为了让每个线程都能快速找到自己的属性集合,就把每个描述线程的结构体的的起始地址充当线程id了
  4. 主线程就用的是内核级栈结构,新线程用的就是共享区内部提供的私有栈结构
  5. 所以,本质上我们说的进程ID,其实就是一个虚拟地址,每一个进程的虚拟地址都是不同的,因此可以用它来区分每一个线程

问题:如何保证栈区是每一个线程独占的呢?
解答:用户要线程,但是OS只提供轻量级进程,所以在中间加了个线程库作为中间软件层,一样的,线程也要被管理起来,所以这个管理OS承担一部分,库承担一部分,OS承担的主要是对轻量级进程的调度和它的内核数据结构的管理,库主要是要为用户提供线程相关的属性字段。(线程也需要又自己私有的一部分属性,而这部分属性有可能无法和进程一样完整地表示线程,所以这部分工作就由库来完成。)

7.3 代码验证几个问题

通过下面的代码可以验证两个线程的周边结论:

①验证每个线程都有独立的栈空间

②主线程能访问每一个线程栈里的数据

#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <vector>
using namespace std;

#define NUM 3
int *p = NULL;

struct threadData
{
    string threadname;
};

string toHex(pthread_t tid)
{
    char buffer[128];
    snprintf(buffer, sizeof(buffer), "0x%x", tid);
    return buffer;
}

void InitThreadData(threadData *td, int number)
{
    td->threadname = "thread-" + to_string(number);
}

void *threadRoutine(void *args)
{

    int test_i = 0;
    threadData *td = static_cast<threadData *>(args);
    if (td->threadname == "thread-2")
        p = &test_i; // 验证主线程能访问其它线程的栈数据
    int i = 0;
    while (i < 10)
    {
        cout << "pid: " << getpid() << ", tid: " << toHex(pthread_self()) << ", threadname: " << td->threadname
             << ", test_t: " << test_i << ", &test_i: " << &test_i << endl;
        sleep(1);
        i++;
        test_i++;
    }

    delete td;
    return nullptr;
}

int main()
{
    vector<pthread_t> tids;
    for (int i = 0; i < NUM; i++)
    {
        pthread_t tid;
        threadData *td = new threadData;
        InitThreadData(td, i);

        pthread_create(&tid, nullptr, threadRoutine, td);
        tids.push_back(tid);
    }
    sleep(1);
    cout << "main thread get a thread local value, val: " << *p << ", &p: " << p << endl;
    for (int i = 0; i < tids.size(); i++)
    {
        pthread_join(tids[i], nullptr);
    }

    return 0;
}

 打印结果乱序很正常,比较线程本来就是并发执行的,从上面的信息我们看到下面几点:

  1. 每个线程打印的pid是一样的,每个线程的tid也就是地址不同,表示每个线程都有独立的栈结构
  2. 主线程定义全局指针p,p能访问2号线程的test_i值,说明主线程能访问每个线程的栈数据
  3. 每个线程都对test_i++,打印的值一样,再次证明线程有独立的栈结构
  4. 在线程当中,线程有独立的栈结构,但是没有私有的栈结构,其它线程仍然能访问,但是以后编写代码时,非常不建议这样搞

八,pthread_create第四个参数传类的对象

pthread_create第四个参数不仅可以传整数或字符串,也可以传类的对象,这就是void*的好处,如下代码:

#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;

class Request
{
public:
    Request(int start, int end, const string &threadname)
        : _start(start), _end(end), _threadname(threadname)
    {
    }

public:
    int _start;
    int _end;
    string _threadname;
};

class Response
{
public:
    Response(int result, int exitcode)
        : _result(result), _exitcode(exitcode)
    {
    }

public:
    int _result;
    int _exitcode;
};

void *sumCount(void *args)
{
    Request *rq = static_cast<Request *>(args); // 类似于Request *eq = (Request*)args
    int sum = 0;
    for (int i = rq->_start; i <= rq->_end; i++) // 求start到end的数的和
    {
        cout << rq->_threadname << " is runing... " << i << endl;
        sum += i;
        usleep(60000);
    }
    Response *rsq = new Response(sum, 0);
    delete rq;
    return rsq;
}

int main()
{
    pthread_t tid;
    Request *rq = new Request(1, 100, "thread 1:");
    pthread_create(&tid, nullptr, sumCount, rq); // 这个参数不仅仅只能传整数或字符串,我还可以传类的对象

    void *ret;
    pthread_join(tid, &ret);
    Response *rsp = static_cast<Response *>(ret);
    cout << "rsp->result: " << rsp->_result << endl;
    delete rsp;
    return 0;
}


上一篇:MyBatis中复杂查询(一对多和多对一)


下一篇:【续集】Java之父的退休之旅:从软件殿堂到多彩人生的探索