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库中申请。

相关推荐
镜中人★10 小时前
408操作系统考纲知识点
linux·运维·服务器
liulilittle10 小时前
rinetd 端口转发工具技术原理
linux·服务器·网络·c++·端口·通信·转发
fy zs10 小时前
应用层自定义协议和序列化
linux·网络·c++
lytao12310 小时前
MySQL高可用集群部署与运维完整手册
运维·数据库·mysql·database
末日汐10 小时前
库的制作与原理
linux·后端·restful
tmacfrank10 小时前
Binder 预备知识
linux·运维·binder
cnstartech10 小时前
esxi-vmware 虚拟机互相打开
linux·运维·服务器
mcdx10 小时前
bootm的镜像加载地址与uImage镜像的加载地址、入口地址之间的关系
linux
不知疲倦的仄仄10 小时前
第四天:Netty 核心原理深度解析&EventLoop、Future/Promise 与 Pipeline
linux·服务器·网络