Linux 线程控制

2. Linux 线程控制

首先,**内核中有没有很明确的线程的概念**,而有**轻量级进程的概念**。当我们想写多线程代码时,可以使用**POSIX线程库**,这是一个
处于应用层位置的库,几乎所有的Linux发行版都默认带这个库,使用时需要引入头文件`pthread.h`

2.1 创建线程

2.1.1 基础创建

c 复制代码
功能:创建一个新的线程
原型:
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void*), void *arg);
参数:
thread:输出型参数,返回线程ID
attr:设置线程的属性,attr为NULL表示使用默认属性
start_routine:是个函数地址,线程启动后要执行的函数
arg:传给线程启动函数的参数
返回值:成功返回0;失败返回错误码
  • 传统的一些函数是,成功返回0,失败返回-1,并且对全局变量errno赋值以指示错误
  • pthreads函数出错时不会设置全局变量errno(而大部分其他POSIX函数会这样做)。而是将错误代码通过返回值返回
  • pthreads同样也提供了线程内的errno变量,以支持其它使用errno的代码。对于pthreads函数的错误,建议通过返回值来判定,因为读取返回值要比读取线程内的errno变量的开销更小

由于是第三方库,所以编译时要加上-lpthread

makefile 复制代码
myThread : myThread.cc
	g++ -o $@ $^ -std=c++11 -lpthread

.PHONY : clean
clean:
	rm -f myThread

可以看到,这不同的线程拥有同一个进程的pid

当我们想观察线程状态时,可以使用ps -aL命令,LWP的时ligtht weight process的缩写,PID等于LWP的线程是主线程。当我们使用kill -9命令时,杀掉任意一个线程,整个进程都会被kill掉

下面这个可以持续观察线程状态

bash 复制代码
while :; do ps -aL | head -1 && ps -aL | grep 'myThread' | grep -v ps; echo "##############################################"; sleep 1; done

2.1.2 让一个函数被重入,让多个执行流同时调用

cpp 复制代码
// 可以被多个执行流执行
void Show(const string& s) 
{
    cout << s << " say: hello!" << endl;
}

void* ThreadRoutine(void* args) 
{
    while(true) {
        // cout << "new thread, pid is " << getpid() << endl;
        Show("[new thread]");
        sleep(1); 
    }
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, ThreadRoutine, nullptr);
    while(true) {
        // cout << "main thread, pid is " << getpid() << endl;
        Show("[main thread]");
        sleep(1); 
    }
    return 0;
}

虽然打印出现了错行的情况,但不影响现象的产生


2.1.3 线程之间进行通信很容易

cpp 复制代码
int g_val = 0;

void* ThreadRoutine(void* args) 
{
    while(true) {
        // cout << "new thread, pid is " << getpid() << endl;
        // Show("[new thread]");
        printf("I am new thread, g_val: %d, &g_val: %p\n", g_val, &g_val);
        sleep(1); 
    }
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, ThreadRoutine, nullptr);
    while(true) {
        // cout << "main thread, pid is " << getpid() << endl;
        // Show("[main thread]");
        g_val++;
        printf("I am main thread, g_val: %d, &g_val: %p\n", g_val, &g_val);
        sleep(1); 
    }
    return 0;
}

2.1.4 线程任意一个出现异常,进程都会退出

cpp 复制代码
void* ThreadRoutine(void* args) 
{
    while(true) {
        cout << "new thread, pid is " << getpid() << endl;
        sleep(5);
        int *p =nullptr;
        *p = 1;
    }
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, ThreadRoutine, nullptr);
    while(true) {
        cout << "main thread, pid is " << getpid() << endl;
        sleep(1); 
    }
    return 0;
}

2.1.5 观察类型是pthread_t 的 tid

定义 pthread_t 类型是 无符号长整型

c 复制代码
typedef unsigned long int pthread_t;

可以打印查看

cpp 复制代码
int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, ThreadRoutine, nullptr);
    while(true) {
        printf("I am main thread, creat new thread tid is %lu\n");
        sleep(1); 
    }
    return 0;
}

可以看到,打印出来的tid的值与LWP的值完全不一样,这是因为tid是给用户使用的的,而LWP是给OS使用的。

实际上,tid充当的是地址,在2.4中会介绍

2.1.6 给子线程传递参数

cpp 复制代码
void* ThreadRoutine(void* args) 
{
    const char* name = (const char*)args;
    while(true) {
        printf("%s is running\n", name);
        sleep(1); 
    }
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, ThreadRoutine, (void*)"new thread");
    while(true) {
        printf("main thread is running\n");
        sleep(1); 
    }
    return 0;
}

2.2 线程等待

2.2.1 为什么需要线程等待?

  • 已经退出的线程,其空间没有被释放,仍然在进程的地址空间内。
  • 创建新的线程不会复用刚才退出线程的地址空间。
  • 如果需要,可以获取子线程的返回值

2.2.2 基础使用

c 复制代码
功能:等待线程结束
原型
int pthread_join(pthread_t thread, void **value_ptr);
参数
thread:线程ID
value_ptr:它指向一个指针,后者指向线程的返回值
返回值:成功返回0;失败返回错误码
  1. 如果thread线程通过return返回,value_ ptr所指向的单元里存放的是thread线程函数的返回值。
  2. 如果thread线程被别的线程调用pthread_ cancel异常终掉,value_ ptr所指向的单元里存放的是常数PTHREAD_ CANCELED,其值为-1。
  3. 如果thread线程是自己调用pthread_exit终止的,value_ptr所指向的单元存放的是传给pthread_exit的参数。
  4. 如果对thread线程的终止状态不感兴趣,可以传NULL给value_ ptr参数。
cpp 复制代码
void* ThreadRoutine(void* args) 
{
    const char* name = (const char*)args;
    int cnt = 5;
    while(cnt--) {
        printf("%s is running\n", name);
        sleep(1); 
    }
    return nullptr;     // 线程走到这里后,会默认退出
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, ThreadRoutine, (void*)"new thread");
    pthread_join(tid, nullptr);     // 主线程等待的时候,默认是阻塞等待
    cout << "main thread quit" << endl;
    return 0;
}

2.2.3 获取子线程的返回值

cpp 复制代码
void* ThreadRoutine(void* args) 
{
    const char* name = (const char*)args;
    int cnt = 5;
    while(cnt--) {
        printf("%s is running\n", name);
        sleep(1); 
    }
    return (void*)1;     // 线程走到这里后,会默认退出
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, ThreadRoutine, (void*)"new thread");
    void* ret;
    pthread_join(tid, &ret);     // 主线程等待的时候,默认是阻塞等待
    printf("main thread quit, ret: %d\n", (long long)ret);  // 由于void* 是8字节,所以这里要用8字节的long long 来进行强转
    return 0;
}

如果想要多返回些信息,可以使用类

cpp 复制代码
struct Request
{
    Request(int s, int e, string n) 
    : _start(s)
    , _end(e)
    , _name(n) {}

    // 累加 [start, end]
    int Run()
    {
        int tmp = 0;
        for(int i = _start; i<=_end; ++i) {
            tmp += i;
        }
        return tmp;
    }

    int _start;
    int _end;
    string _name;
};

struct Response
{
    Response(int r, int e) 
    : _res(r)
    , _exitCode(e) {}

    int _res;
    int _exitCode;
};


void* GetSum(void* args)
{
    Request* rq = static_cast<Request*>(args);
    Response* rs = new Response(0, 0);
    // 计算
    rs->_res = rq->Run();
    delete rq;
    return rs;
}
 
int main()
{
    pthread_t tid;
    Request* rq = new Request(1, 100, "new Thread");
    pthread_create(&tid, nullptr, GetSum, rq);

    void* ret;
    pthread_join(tid, &ret);
    Response* rs = static_cast<Response*>(ret);
    printf("res: %d, exitCode: %d\n", rs->_res, rs->_exitCode);
    delete rs;
    return 0;
}

以这个为例子,如果一个计算任务很大,比如1-100000,就可以拆分,让不同的线程执行不同的范围,最后主线程再将子线程的结果进行汇总

2.3 线程终止

2.3.1 直接使用return

上面的所有例子都是使用这个方法,当线程走到return后,会默认退出。注意:exit()是进程退出函数,不是线程,当线程函数使用它时会使整个进程退出

2.3.2 pthread_exit

c 复制代码
功能:线程终止
原型
void pthread_exit(void *value_ptr);
参数
value_ptr:value_ptr不要指向一个局部变量。
返回值:无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)
cpp 复制代码
void* ThreadRoutine(void* args) 
{
    const char* name = (const char*)args;
    int cnt = 5;
    while(cnt--) {
        printf("%s is running\n", name);
        sleep(1); 
    }
    pthread_exit((void*) 123);
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, ThreadRoutine, (void*)"new thread");
    void* ret;
    pthread_join(tid, &ret);     // 主线程等待的时候,默认是阻塞等待
    printf("main thread quit, ret: %d\n", (long long)ret);  // 由于void* 是8字节,所以这里要用8字节的long long 来进行强转
    return 0;
}

2.3.3 pthread_cancel

c 复制代码
功能:取消一个执行中的线程
原型
int pthread_cancel(pthread_t thread);
参数
thread:线程ID
返回值:成功返回0;失败返回错误码
cpp 复制代码
void* ThreadRoutine(void* args) 
{
    const char* name = (const char*)args;
    while(true) {
        printf("%s is running\n", name);
        sleep(1); 
    } 
	return nullptr;
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, ThreadRoutine, (void*)"new thread");
    // 主进程取消掉子进程
    sleep(1);
    pthread_cancel(tid);
    void* ret;
    pthread_join(tid, &ret);     // 主线程等待的时候,默认是阻塞等待
    printf("main thread quit, ret: %d\n", (long long)ret);  // 由于void* 是8字节,所以这里要用8字节的long long 来进行强转
    return 0;
}

可以看到ret为-1,那是因为PTHREAD_ CANCELED,其值为-1。见2.2.2

2.4 线程ID及进程地址空间布局

  • pthread_ create函数会产生一个线程ID,存放在第一个参数指向的地址中。该线程ID和前面说的线程ID不是一回事,(pid和LWP)
  • 前面讲的线程ID属于进程调度的范畴。因为线程是轻量级进程,是操作系统调度器的最小单位,所以需要一个数值来唯一表示该线程
  • pthread_ create函数第一个参数指向一个虚拟内存单元,该内存单元的地址即为新创建线程的线程ID,属于NPTL线程库的范畴。线程库的后续操作,就是根据该线程ID来操作线程的
  • 线程库NPTL提供了pthread_ self函数,可以获得线程自身的ID:
c 复制代码
功能:
       pthread_self - 获取调用线程的ID

原型:
       pthread_t pthread_self(void);
描述:
       pthread_self()函数返回调用线程的ID。
	与在创建this的pthread_create(3)调用中的*thread中返回的值
	线程是一样的。
返回值:
       这个函数总是成功,返回调用线程的ID。
cpp 复制代码
// 将tid以地址的格式打出来,即16进制
string ToHex(pthread_t tid)
{
    char hex[64];
    snprintf(hex, sizeof(hex), "%p", tid);
    return hex;
}

void* ThreadRoutine(void* args)
{
    while(true) {
        // 打印出进程id,将它转成16进程
        // cout << "thread id: " << ToHex(pthread_self()) << endl;
        printf("thread id: %s\n", ToHex(pthread_self()).c_str());
        sleep(1);
    }
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, ThreadRoutine, nullptr);
    while(true) {
        printf("child tid: %s\n", ToHex(tid).c_str());
        sleep(1);
    }
    pthread_join(tid, nullptr);
    return 0;
}

pthread_t到底是什么类型呢?取决于实现。对于Linux目前实现的NPTL实现而言,pthread_t类型的线程ID,本质就是一个进程地址空间上的一个地址

可以看到,除了主线程,所有其他线程的独立栈,都在共享区,具体来讲是在pthread库中,tid指向的用户tcb中,这样,线程在调度运行的时候就不会互相干扰了。

2.5 其它问题

2.5.1 创建多个线程

cpp 复制代码
#include <iostream>
#include <vector>
#include <string>
#include <unistd.h>
#include <pthread.h>
#define NUM 3
using namespace std;

struct ThreadData
{   
    ThreadData(string name) : _threadName(name) {}
    string _threadName;
};

void* ThreadRoutine(void* args)
{
    ThreadData* td = static_cast<ThreadData*>(args);
    int cnt = 5;
    while(cnt--) {
        printf("%s, tid: %p, pid: %d\n", td->_threadName.c_str(), pthread_self(), getpid());
        sleep(1);
    }
    delete td;
    return nullptr;
}

int main()
{
    // 创建多个线程
    vector<pthread_t> tids;
    for (size_t i = 0; i < NUM; i++) {
        pthread_t tid;
        ThreadData* td = new ThreadData("Thread-" + to_string(i+1));
        // 给子线程传递数据,堆空间是共享的
        pthread_create(&tid, nullptr, ThreadRoutine, td);
        tids.push_back(tid);
    }
    
    // 等待线程
    for(const auto& t : tids) {
        pthread_join(t, nullptr);
    }
    
    return 0;
}

2.5.2 每一个线程自己独立的栈结构

修改ThreadRoutine()函数

cpp 复制代码
void* ThreadRoutine(void* args)
{
    ThreadData* td = static_cast<ThreadData*>(args);
    int cnt = 5, x = 0;
    while(cnt--) {
        printf("%s, tid: %p, pid: %d, x: %d, &x: %p\n", td->_threadName.c_str(), pthread_self(), getpid(), x, &x);
        x++;
        sleep(1);
    }
    delete td;
    return nullptr;
}

可以看到,每一个线程都独享一个x,即独立的栈空间。注意这里是独立,并不是私有,其它线程想访问还是可以的,比如主线程想访问Thread-2的x值

cpp 复制代码
int *p = nullptr;

void* ThreadRoutine(void* args)
{
    ThreadData* td = static_cast<ThreadData*>(args);
    int cnt = 5, x = 0;
    while(cnt--) {
        printf("%s, tid: %p, pid: %d, x: %d, &x: %p\n", td->_threadName.c_str(), pthread_self(), getpid(), x, &x);
        x++;
        // 获取该线程的x
        if(td->_threadName == "Thread-2")   p = &x;
        sleep(1);
    }
    delete td;
    return nullptr;
}

int main()
{
    // 创建多个线程
    vector<pthread_t> tids;
    for (size_t i = 0; i < NUM; i++) {
        pthread_t tid;
        ThreadData* td = new ThreadData("Thread-" + to_string(i+1));
        // 给子线程传递数据,堆空间是共享的
        pthread_create(&tid, nullptr, ThreadRoutine, td);
        tids.push_back(tid);
    }
    sleep(2);
    printf("Main Thread get x: %d, &x: %p\n", *p, p);
    // ...
}

2.5.3 全局变量

默认情况下,全局变量被所有线程共享

cpp 复制代码
// 共享资源
int gVal = 100;

void* ThreadRoutine(void* args)
{
    ThreadData* td = static_cast<ThreadData*>(args);
    int cnt = 5;
    while(cnt--) {
        gVal++;
        printf("%s, tid: %p, pid: %d, gVal: %d, &gVal: %p\n", td->_threadName.c_str(), pthread_self(), getpid(), gVal, &gVal);
        sleep(1);
    }
    delete td;
    return nullptr;
}

如果想要让每个进程独享该变量,可以在变量前加上__thread,让编译器把该变量放到tcb的线程局部存储单元中,见2.4。注意这里只能初始化内置类型,自定义类型是不可以的

cpp 复制代码
__thread int gVal = 100;

介绍一个__thread的用法:减少系统调用

cpp 复制代码
__thread unsigned long int self = 0;
__thread int tPid = 0;
void* ThreadRoutine(void* args)
{
    ThreadData* td = static_cast<ThreadData*>(args);
    self = pthread_self();
    tPid = getpid();
    int cnt = 5;
    while(cnt--) {
        printf("%s, tid: %p, pid: %d\n", td->_threadName.c_str(), self, tPid);
        sleep(1);
    }
    delete td;
    return nullptr;
}

像上面这样,就减少了系统调用的次数。可以将这样的变量理解为线程级别的全局变量,不同线程之间变量互不干扰

2.6 分离线程

  • 默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。
  • 如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。
c 复制代码
int pthread_detach(pthread_t thread);

可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离:

c 复制代码
pthread_detach(pthread_self());

joinable和分离是冲突的,一个线程不能既是joinable又是分离的

cpp 复制代码
__thread unsigned long int self = 0;
__thread int tPid = 0;
void* ThreadRoutine(void* args)
{
    ThreadData* td = static_cast<ThreadData*>(args);
    self = pthread_self();
    tPid = getpid();
    // 自己分离
    pthread_detach(self);
    int cnt = 5;
    while(cnt--) {
        printf("%s, tid: %p, pid: %d\n", td->_threadName.c_str(), self, tPid);
        sleep(1);
    }
    delete td;
    return nullptr;
}

int main()
{
    // 创建多个线程
    vector<pthread_t> tids;
    for (size_t i = 0; i < NUM; i++) {
        pthread_t tid;
        ThreadData* td = new ThreadData("Thread-" + to_string(i+1));
        // 给子线程传递数据,堆空间是共享的
        pthread_create(&tid, nullptr, ThreadRoutine, td);
        tids.push_back(tid);
    }
    sleep(2);
    // 等待线程
    for(const auto& t : tids) {
        int ret = pthread_join(t, nullptr);
        printf("ret: %d, who: %p, why: %s\n", ret, t, strerror(ret));
    }
    
    return 0;
}
相关推荐
饮啦冰美式2 分钟前
22.04Ubuntu---ROS2使用rclcpp编写节点
linux·运维·ubuntu
wowocpp2 分钟前
ubuntu 22.04 server 安装 和 初始化 LTS
linux·运维·ubuntu
Huaqiwill4 分钟前
Ubuntun搭建并行计算环境
linux·云计算
九圣残炎4 分钟前
【从零开始的LeetCode-算法】1456. 定长子串中元音的最大数目
java·算法·leetcode
wclass-zhengge6 分钟前
Netty篇(入门编程)
java·linux·服务器
Lign173148 分钟前
ubuntu unrar解压 中文文件名异常问题解决
linux·运维·ubuntu
lulu_gh_yu10 分钟前
数据结构之排序补充
c语言·开发语言·数据结构·c++·学习·算法·排序算法
丫头,冲鸭!!!29 分钟前
B树(B-Tree)和B+树(B+ Tree)
笔记·算法
Re.不晚34 分钟前
Java入门15——抽象类
java·开发语言·学习·算法·intellij-idea
ULTRA??43 分钟前
C加加中的结构化绑定(解包,折叠展开)
开发语言·c++