Linux 线程控制

1.线程库

linux中有pthread动态库,几乎所有的linux平台都自带这个动态库,所以在makefil编译的时候需要链接这个动态库。

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

.PHONY:clean
clean:
	rm -f mythead

2.pthread_create创建线程

pthread_t:一个长整型类型。

参数thread:用来返回线程的tid,也就类似于进程的pid。

参数attr:是一个const pthread_attr_t *类型的,作用是为线程设置属性,这里我们不需要给线程设置属性,所以默认我们不关心第二个参数attr,即传入nullptr即可

参数start_routine :即一个函数指针类型void *(*) (void *)类型的,用于传入我们要让线程执行的函数,也就意味着这个函数的类型需要是**void*(void*),**因为我们创建线程的目的就是让线程执行函数任务

参数arg:是arg类型是void*,用于给线程执行的函数传参,这里我们暂时不关心设置为nullptr即可

pthread_create的返回值,如果创建新线程成功,那么返回0,并且新线程的tid被定义,如果创建新线程失败,那么返回错误码,并且新线程的tid没有被定义,这里有一个值得思考的点,为什么创建新线程失败,并没有设置错误码errno呢?

因为一个进程中可能有多个线程,而错误码errno是一个全局变量只有一个,所以并不能支持所有的新线程设置错误码,所以干脆新线程如果出错了都不设置错误码了,而是直接返回错误码即可,这样就保证了每一个线程都可以有自己的错误码。

代码1示例:创建一个线程去一直打印进程的pid,主线程也一直打印进程pid。

cpp 复制代码
#include <iostream>
#include <pthread.h>
#include <unistd.h>

using namespace std;

void* threadRoutine(void* args)
{
    while(true)
    {
        cout << "I am a thread, pid: " << getpid() << endl;
        sleep(1);
    }
}

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

    return 0;
}

运行结果:

1.可以看到打印的pid是一样的,说明处于同一个进程中。

2.同时可以看出来,打印出来的结果并不是主线程打印一行,线程打印一行,而是有时候打印一行有时候打印两行,说明两个线程在争取显示器资源,后续可以通过加锁来实现互斥。

代码2示例:在代码1的基础上面,通过第四个参数传入参数给执行的函数

cpp 复制代码
  1 #include <iostream>
  2 #include <pthread.h>
  3 #include <unistd.h>
  4 
  5 using namespace std;
  6 //pthrtead_create
  7 void* threadRoutine(void* args)
  8 {
  9     const char*str=static_cast<const char*>(args);
 10     while(true)
 11     {
 12         cout<<str << ", I am a thread, pid: " << getpid() << endl;
 13         sleep(1);
 14     }
 15 }
 16 
 17 int main()
 18 {
 19     pthread_t tid;
 20     pthread_create(&tid, nullptr, threadRoutine, (void*)"hello world");
 21 
 22     while(true)
 23     {
 24         cout << "main thread, pid: " << getpid() << endl;
 25         sleep(1);
 26     }
 27 
 28     return 0;
 29 }

运行结果:

因为第四个参数是void*类型,所以在传入之前是需要强转为void*类型,在函数里面接收后,需要强转为原来的类型。

**代码3示例:**第四个参数不仅仅可以传入内置类型,也可以传入类对象。

cpp 复制代码
 31 struct ThreadData
 32 {
 33 public:
 34     ThreadData(string name)
 35     :threadname(name)
 36     {}
 37     string threadname;
 38 };  
 39 
 40 void* threadRoutine(void* args)
 41 {   
 42     ThreadData*str=static_cast<ThreadData*>(args);
 43     while(true)
 44     {   
 45         cout<<str->threadname << ", I am a thread, pid: " << getpid() << endl;
 46         sleep(1); 
 47     }   
 48 }   
 49 
 50 int main()
 51 {   
 52     pthread_t tid;
 53     ThreadData td("hello worla");
 54     pthread_create(&tid, nullptr, threadRoutine, (void*)&td);
 55     
 56     while(true)
 57     {   
 58         cout << "main thread, pid: " << getpid() << endl;
 59         sleep(1);
 60     }
 61     
 62     return 0;
 63 }

运行结果:

2.1 ps -aL指令

使用ps -aL可以查看系统中的全部的轻量级进程,其中-aL选项中的a是all全部的意思,L是轻量级进程的意思,所以当左侧进程运行起来之后,我们使用ps -aL查看系统中全部的轻量级进程。

图中PID代表的是进程的PID,LWP表示的light weight process轻量级进程的意思,这个LWP是给内核看的,主线程的LWP等于进程的PID,CPU可以通过比较LWP和PID来区分是否为主线程,如果主线程的时间片到了,就需要进程切换。

3.pthread_wait线程等待

创建线程起始就是创建一个PCB对象,所以一个线程也需要被等待,不然也会造成内存泄漏。

(1)回收新线程,防止内存泄漏(2)如果需要的话,获取新线程执行任务的返回值,确认新线程是否执行完任务。

参数thread:等待的线程tid

参数retval:二级指针,存储的是pthread_create第3个参数的返回值的地址,如果不想获取设为nullptr即可。

**代码示例1:**不关心函数的返回值,创建一个线程,线程5秒后退出

cpp 复制代码
#include <iostream>
#include <pthread.h>
#include <unistd.h>

using namespace std;

void* threadRoutine(void* args)
{
    const char* name = static_cast<const char*>(args);
    int cnt = 5;
    while(true)
    {
        cout << name << " create, pid: " << getpid() << ", cnt: " << cnt << endl;
        sleep(1);

        cnt--;
        if(cnt == 0)
            break;
    }
}

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

    pthread_join(tid, nullptr);
    cout << "main thread quit..." << endl;

    return 0;
}

运行结果:

可以看出来,pthread_join的等待方式是非阻塞等待方式,主线程结束了,不会退出,会阻塞的等待线程的退出。

**代码示例2:**使用pthread的第二个参数去获取函数的返回值

cpp 复制代码
 67 void* threadRoutine(void* args)
 68 {
 69     const char* name = static_cast<const char*>(args);
 70     int cnt = 5;
 71     while(true)
 72     {
 73         cout << name << " create, pid: " << getpid() << ", cnt: " << cnt << endl;
 74         sleep(1);
 75 
 76         cnt--;
 77         if(cnt == 0)
 78             break;
 79     }
 80     return (void*)1;
 81 }
 82 
 83 int main()
 84 {   
 85     pthread_t tid;
 86     pthread_create(&tid, nullptr, threadRoutine, (void*)"Thread 1");
 87     void*retval;
 88     pthread_join(tid,&retval);
 89     cout << "main thread quit..." <<(int) retval<<endl;
 90     
 91     return 0;
 92 }

运行结果:

会发现编译不过去,因为在linux中,是64位的,指针的字节是8位,但是int是固定的4字节,就会触发指针转窄整数的报错,可以将int换位(long long int)是固定的8字节数。

修改后代码:

cpp 复制代码
 67 void* threadRoutine(void* args)
 68 {
 69     const char* name = static_cast<const char*>(args);
 70     int cnt = 5;
 71     while(true)
 72     {
 73         cout << name << " create, pid: " << getpid() << ", cnt: " << cnt << endl;
 74         sleep(1);
 75 
 76         cnt--;
 77         if(cnt == 0)
 78             break;
 79     }
 80     return (void*)1;
 81 }
 82 
 83 int main()
 84 {   
 85     pthread_t tid;
 86     pthread_create(&tid, nullptr, threadRoutine, (void*)"Thread 1");
 87     void*retval;
 88     pthread_join(tid,&retval);
 89     cout << "main thread quit..." <<(long long int) retval<<endl;
 90 
 91     return 0;
 92 } 

修改后编译结果:

所以这样子以后,主线程就可以获取线程的pthread_create的返回值(退出码),来知道线程的返回情况。那么对于退出码已经可以获取了,那么对于异常呢?

代码示例:线程发送/0错误,收到信号,主线程的退出情况。

cpp 复制代码
#include <iostream>
#include <pthread.h>
#include <unistd.h>

using namespace std;

void* threadRoutine(void* args)
{
    const char* name = static_cast<const char*>(args);

    int cnt = 3;
    while(true)
    {
        cout << name << ", new thread, pid: " << getpid() << ", cnt: " << cnt << endl;
        sleep(1);

        cnt--;
        if(cnt == 0)
            break;
    }

    int a = 10;
    a /= 0;

    return (void*)2;
}

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

    void* retval;
    pthread_join(tid, &retval);
    cout << "main thread quit..., ret: " << (long long int)retval << endl;

    return 0;
}

运行结果:

信号其实是相对于进程发送的,所以当线程发送了异常,进程也就会得到了信号,直接退出了,也就打印不出来主线程的结束语句了。

小疑问:为什么函数的返回值要使用二级指针来获取,用一级指针不是就可以了吗?

因为pthread线程库是线程的管理者,线程属性中有线程函数,那么线程函数的返回值pthread线程库也会知道,所以线程函数的返回值被存储到了pthread线程库的一个区域中,此时这个区域中存储的是一个void*的一级指针,那么pthread_join接口想要获取这个区域的一级指针只能采用二级指针来进行获取。

4.线程进一步理解

4.1 全局变量

进程地址空间里面的大部分数据都是被共享的,那么创建一个全局变量,是不是会被多个线程共享呢?

代码示例1:主线程每隔2秒打印一次g_val的值,并对g_val的值++,线程每隔一秒打印一次g_val的值。

cpp 复制代码
128 int g_val = 100;
129 
130 void* threadRoutine(void* args)
131 {
132     const char* name = static_cast<const char*>(args);
133     
134     while(true)
135     {
136         printf("new thread, g_val: %d, &g_val: %p\n", g_val, &g_val);
137         sleep(1);
138     }   
139     
140     return (void*)0;
141 }   
142 
143 int main()
144 {
145     pthread_t tid;
146     pthread_create(&tid, nullptr, threadRoutine, (void*)"Thread 1");
147     
148     while(true)
149     {
150         printf("main thread, g_val: %d, &g_val: %p\n", g_val, &g_val);
151         g_val++;
152         sleep(2);
153     }   
154     
155     void* retval;
156     pthread_join(tid, &retval);
157     cout<<g_val<<endl;
158     cout << "main thread quit..., ret: " << (long long int)retval << endl;
159     
160     return 0;
161 }   

运行结果:

可以看到每次主线程对g_val的值++,线程访问到的g_val的值也随之改变,并且每次访问的地址都相同。

**代码示例2:**使用__thread来修饰变量,会使每一个线程中都有一个独立的g_val变量,一个线程的修改不会影响其他线程。

cpp 复制代码
128 __thread int g_val = 100;
129 
130 void* threadRoutine(void* args)
131 {
132     const char* name = static_cast<const char*>(args);
133     
134     while(true)
135     {
136         printf("new thread, g_val: %d, &g_val: %p\n", g_val, &g_val);
137         sleep(1);
138     }   
139     
140     return (void*)0;
141 }   
142 
143 int main()
144 {
145     pthread_t tid;
146     pthread_create(&tid, nullptr, threadRoutine, (void*)"Thread 1");
147     
148     while(true)
149     {
150         printf("main thread, g_val: %d, &g_val: %p\n", g_val, &g_val);
151         g_val++;
152         sleep(2);
153     }   
154     
155     void* retval;
156     pthread_join(tid, &retval);
157     cout<<g_val<<endl;
158     cout << "main thread quit..., ret: " << (long long int)retval << endl;
159     
160     return 0;
161 }   

运行结果:

4.2 可重入函数

可重入函数指的是能够被多个执行流(如进程、线程、中断服务程序)安全地并发调用,且不会因为共享资源或自身状态导致结果错误的函数。

代码示例:

cpp 复制代码
165 void show(const string& name)
166 {
167     cout << name << "say# " << "hello thread" << endl;
168 }   
169 
170 void* threadRoutine(void* args)
171 {
172     while(true)
173     {
174         show("new thread");
175         
176         sleep(1);
177     }   
178     
179     return (void*)0;
180 }   
181 
182 int main()
183 {
184     pthread_t tid;
185     pthread_create(&tid, nullptr, threadRoutine, nullptr);
186     
187     while(true)
188     {
189         show("main thread");
190         
191         sleep(1);
192     }   
193     
194     void* retval;
195     pthread_join(tid, &retval);
196     cout << "main thread quit..., ret: " << (long long int)retval << endl;
197     
198     return 0;
199 }

运行结果:

5. pthread_excl和pthread_cancel线程终止

线程的终止方法有3个

1.return返回

2.pthread_exit返回

3.pthread_cancel返回

5.1 pthread_excl

参数retval:线程的返回值。

cpp 复制代码
#include <iostream>
#include <pthread.h>
#include <unistd.h>

using namespace std;

void* threadRoutine(void* args)
{
    const char* name = static_cast<const char*>(args);

    int cnt = 3;
    while(true)
    {
        cout << name << ", new thread, pid: " << getpid() << ", cnt: " << cnt << endl;
        sleep(1);

        cnt--;
        if(cnt == 0)
            break;
    }

    pthread_exit((void*)7);
}

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

    void* retval;
    pthread_join(tid, &retval);
    cout << "main thread quit..., ret: " << (long long int)retval << endl;

    return 0;
}

运行结果:

ps:这里要和exit区分开,如果线程里面调用的是exit,是把整个进程都退出了。

5.2 pthread_cancel

参数thread:需要传入线程的tid。

pthread_cancel同样也是一种终止单个线程的方法,但是pthread_cancel并不常用,pthread_cancel的参数是传入要取消的线程的tid即可去掉指定线程。

**代码示例:**在主线程创建出新线程后,新线程执行死循环打印的线程函数,在主线程中,当新线程运行2秒后,在主线程中使用pthread_cancel取消新线程,观察新线程能否被取消,并且在主线程中观察新线程的退出码

cpp 复制代码
233 void* threadRoutine(void* args)
234 {
235     const char* name = static_cast<const char*>(args);
236     
237     while(true)
238     {
239         cout << name << ", new thread, pid: " << getpid() << endl;
240         sleep(1);
241     }   
242 }   
243 
244 int main()
245 {
246     pthread_t tid;
247     pthread_create(&tid, nullptr, threadRoutine, (void*)"Thread 1");
248     
249     sleep(3);
250     
251     pthread_cancel(tid);
252     
253     void* retval;
254     pthread_join(tid, &retval);
255     cout << "main thread quit..., ret: " << (long long int)retval << endl;
256     
257     return 0;
258 }  

运行结果:

新线程虽然执行的是死循环的线程函数,但是2秒过后,只要主线程中调用的pthread_cancel对新线程进行了取消,那么新线程无论在做什么都会被立即去掉,进而终止新线程,一旦新线程被终止,主线程自热而然就可以等待成功新线程,获取新线程的退出码为-1

并且这个-1实际上是一个专门用于线程被pthread_cancel取消后返回的宏定义也就是PTHREAD_CANCELED,即(void*)-1

也就是相对于pthread_excl是线程自己来退出,pthread_cancel而是主线程来决定线程的退出。

6.pthread_self获取进程的tid

首先要注意LWP和tid是两个不同的东西,LWP是操作系统用来管理进程的。

代码示例:主线程和线程都打印线程的tid。

cpp 复制代码
#include <iostream>
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <string>
#include <stdlib.h>

using namespace std;

string ToHex(pthread_t tid)
{
    char hex[64];
    snprintf(hex, sizeof(hex), "%p", tid);
    
    return hex;
}

void* threadRoutine(void* args)
{
    cout << "new thread running, tid: " << ToHex(pthread_self()) << endl;

    return (void*)0;
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRoutine, nullptr);

    cout << "main thread create thread done, new thread tid: " << ToHex(tid) << endl;

    pthread_join(tid, nullptr);

    return 0;
}

运行结果:

可以看出主线程打印的tid和线程打印的是一样的。

6.1 细说tid

上面对代码中,把tid转化位了16进程任何打印了出来,为什么呢?

首先要知道在内核中是没有线程的概念的,只有轻量化进程的概念,所以内核提供的系统接口是

这个接口实在是太复杂了,所以linux开发者编写了一个pthread库专门用于对于线程的操作,在底层是封装了clone函数,在linux中,编写多线程的代码,都需要使用gcc,g++编译链接时需要在后面加上-pthread来使用第三方库。

但是内核的角度是没有形成的概念的,只有轻量化进程概念,我们使用的

cpp 复制代码
ps -aL

会显示所有的轻量化进程,查询到的LWP,操作系统也只认识这个东西,内核会记录每个LWP的运行状态(如运行、就绪、阻塞)、优先级、时间片等,以便决定下一个运行哪个LWP。

但是线程也是有很多属性的,像pthread_create创建线程,pthread_self()获取线程的tid,这些接口是用户态来调用的,这些数据都是需要被维护起来,给用户来获取的,所以线程库就需要去维护这些数据。

线程库是一个动态库,是一定会被加载到内存里面的,tid就指向存储这些数据属性的起始地址,所以我们在pthread_create创建线程时,就会返回tid这个地址给用户,来方便用户获取数据。

所以对于一个线程来说,PCB的数据会被存储于内核空间中,比如线程的时间片,LWP线程唯一标识符,还有一些线程的属性是存在于pthread库中的。

ps:**一定注意!!!**pthread_ create函数会产生一个线程ID(tid),存放在第一个参数指向的地址中。该线程ID和前面说的线程ID(LWP)不是一回事。前面讲的线程ID属于进程调度的范畴。因为线程是轻量级进程,是操作系统调度器的最小单位,所以需要一个数值来唯一表示该线程。

每一个线程都有自己的独立栈

因为每一个线程都有自己的独立链,主线程需要去调用函数去创建线程,等待线程,那么其他线程里面也有自己的独立链,可能去调用其他函数,就需要存储一些临时变量和函数执行过程中产生的临时数据,记录函数的调用关系,像返回的地址,参数,这个时候需要记录这些数据就需要栈空间来存储,这个栈空间在哪里呢?pthread库中

在线程中,因为主线程是在程序运行的时候就分配了栈区,所以主线程的栈是在地址空间的栈上,由于新线程的独立栈都是在程序运行期间被分配的,申请的栈空间都会存在于pthread库中申请。

相关推荐
大树882 小时前
金刚石散热越强,管路越先见顶
大数据·运维·服务器·人工智能·ai
摇滚侠3 小时前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
霸道流氓气质3 小时前
领域驱动设计(DDD)在 Spring Boot 微服务中的实践指南
运维·spring boot·微服务
bush43 小时前
嵌入式linux学习记录十四、术语
linux·嵌入式
载数而行5203 小时前
Linux 11 动态监控指令top
linux
小宇宙Zz3 小时前
Maven依赖冲突
java·服务器·maven
Inhand陈工4 小时前
基于台达PLC与映翰通IG502的智慧水产养殖精准投喂与远程运维解决方案
运维·人工智能·物联网·阿里云·信息与通信
酣大智4 小时前
ARP代理--工作原理
运维·网络·arp·arp代理
不会C语言的男孩4 小时前
Linux 系统编程 · 第 8 章:进程基础
linux·c语言
shushangyun_5 小时前
2026年快消品B2B系统推荐:支持终端门店订货、促销政策自动化的工具?
java·运维·网络·数据库·人工智能·spring·自动化