目录
线程的创建
tid
pthread_self()
线程的退出
pthread_join
传参问题和返回值问题
pthread_exit
线程取消
线程分离
我们来学习线程的控制与线程操作
线程的创建
我们之前在线程的概念中就讲过了,我们可以通过pthread_create来创建一个或者多个子线程,此时main函数里面就是主线程。
#include<iostream>
#include<pthread.h>
#include<string>
#include<unistd.h>
#include<cstring>
using namespace std;void* routine(void* args)
{string name = static_cast<const char*>(args);while (true){cout << "我是新线程, 我的名字是:" << name << endl;sleep(1);}return 0;
}
int main()
{pthread_t tid;int n = pthread_create(&tid, nullptr, routine, (void*)"thread-1");if (n != 0){cout << "creat thread error: " << strerror(n) << endl;return 1;}while (true){cout << "我是main线程..." << endl;sleep(1);}return 0;
}
我们之前说过一般要调用线程创建函数,以pthread_开头的这些跟线程有关的函数都要在编译时指定线程库的,pthread_create这些函数不是系统调用是C语言封装的库函数,在Linux内核中没有线程的概念,只有LWP(轻量级进程的概念),线程是使用LWP进行模拟的,进一步来说Linux不会给我们提供线程的接口,只会提供轻量级进程的接口,轻量级进程和进程都是进程,怎么分呢,系统调用vfork巧妙的分出了进程。
vfork
是一个用于创建新进程的系统调用,类似于 fork
,但有一些关键区别。它主要用于在某些 Unix 系统中优化进程创建,尤其是在子进程立即调用 exec
系列函数时。
而这个函数底层调用的是clone系统调用。
这个clone从传参有一个flag标志位,通过这个flag的不同可以知道要创建的是线程(轻量级进程)还是进程,但是我们不会去用这个clone去创建线程,太复杂了,所以clone就被封装起来,在用户层和Linux内核中间添加一层软件层(线程库),然后在软件层和Linux内核中间创建一个线程创建的接口,用户调用软件层中的pthread_create实际上是让这个函数通过系统提供的线程创建的接口间接调用clone。pthread_create创建的线程我们认为是用户级线程。C++支持多线程,本质封装了pthread库。
tid
pthread_create函数的第一个参数我们说是输出型参数,那个tid就是线程的id值,用来标识唯一线程的,我们用ps -eLf指令查看到的线程的lwp那一列的值就是每个线程的下标,在 Linux 系统中,TID
(Thread ID)和 LWP
(Lightweight Process ID)通常是相同的值,因为它们都指向同一个内核调度实体(task_struct
)。™显示不同的原因肯定是某些工具(如 ps
或 top
)可能会以不同的方式显示 TID
和 LWP
,导致误解,我们已tid的值为准。
int main()
{pthread_t tid;int n = pthread_create(&tid, nullptr, routine, (void*)"thread-1");cout << "new thread id: " << tid << endl;while (true){cout << "我是main线程..." << endl;sleep(1);}return 0;
}
pthread_self()
可以看到tid特别大。除了直接打印tid,C语言还提供了pthread_self函数,pthread_self()
是 POSIX 线程库(pthread
)中的一个函数,用于获取当前线程的线程 ID。
int main()
{pthread_t tid;int n = pthread_create(&tid, nullptr, routine, (void*)"thread-1");cout << "new thread id: " << pthread_self() << endl;while (true){cout << "我是main线程..." << endl;sleep(1);}return 0;
}
这个id和进程一样是实时在变的,位数一样就可以了。
通过上面这个图或许你会认为在多线程中每个线程运行的顺序是固定的,但是其实新线程和main线程谁先运行是不确定的,有可能会插来插去的,所以我们才需要加锁保护,一个进程一次能运行的时间片是被内部的线程瓜分的,每个线程只能拿到进程时间片的五分之一,创建越多的线程,每个线程运行的时间越少,总量就这么多。
void* routine(void* args)
{string name = static_cast<const char*>(args);while (true){cout << "我是新线程, 我的名字是:" << name << endl;sleep(1);}return 0;
}
int main()
{pthread_t tid1;int n = pthread_create(&tid1, nullptr, routine, (void*)"thread-1");pthread_t tid2;int n = pthread_create(&tid2, nullptr, routine, (void*)"thread-1");pthread_t tid3;int n = pthread_create(&tid3, nullptr, routine, (void*)"thread-1");pthread_t tid4;int n = pthread_create(&tid4, nullptr, routine, (void*)"thread-1");cout << "new thread id: " << tid1 << endl;cout << "new thread id: " << tid2 << endl;cout << "new thread id: " << tid3 << endl;cout << "new thread id: " << tid4 << endl;while (true){cout << "我是main线程..." << endl;sleep(1);}return 0;
}
创建多个线程,这4个线程可以都调用同一个执行函数,这时如果这4个线程的2个即以上都同时进入了routine函数,那这个函数不就被重入了吗,这种重入可能会导致错乱,所以要加锁保护,他们可以同时进入函数,进程内的函数是共享的,如果这个函数里面有文件操作,那文件不就是在线程中共享的了吗,不加保护的情况下,显示器文件(打印)就是共享的。
#include<iostream>
#include<pthread.h>
#include<string>
#include<unistd.h>
#include<cstring>
using namespace std;
int gval = 100;void* routine(void* args)
{string name = static_cast<const char*>(args);while (true){cout << "我是新线程, 我的名字是:" << name << ",my tid is: " << pthread_self() << ", gval: " << gval << endl;gval++;sleep(1);}return 0;
}
int main()
{pthread_t tid1;int n = pthread_create(&tid1, nullptr, routine, (void*)"thread-1");pthread_t tid2;int n2 = pthread_create(&tid2, nullptr, routine, (void*)"thread-1");pthread_t tid3;int n3 = pthread_create(&tid3, nullptr, routine, (void*)"thread-1");pthread_t tid4;int n4 = pthread_create(&tid4, nullptr, routine, (void*)"thread-1");cout << "new thread id: " << tid1 << endl;cout << "new thread id: " << tid2 << endl;cout << "new thread id: " << tid3 << endl;cout << "new thread id: " << tid4 << endl;while (true){cout << "我是main线程..." << endl;sleep(1);}return 0;
}
可以看到4个线程分支每次打印的gval的值都分别不同,所以static/全局变量也是共享的,不然gval增加的速度就不会这么快。全局变量存储在内存的 数据段(Data Segment) 中,所以进程里面的数据段以及代码段对于线程来说也是共享的
结论1:新线程和main主线程谁先运行是不确定的
结论2:线程创建出来,要堆进程的时间片进行瓜分
结论3:位于共享区的资源也是共享的
为什么呢,C/C++的标准库是位于共享区的,库是共享的吧,谁都能访问,所以共享区的资源是共享的
不同线程也可以执行不同的函数,让不同线程看到同一个资源是很容易的事。
线程的退出
在多线程中一旦单个线程出现异常,可能会导致其他线程一起崩溃包括主线程。
#include<iostream>
#include<pthread.h>
#include<string>
#include<unistd.h>
#include<cstring>
using namespace std;
int gval = 100;void* routine1(void* args)
{string name = static_cast<const char*>(args);while (true){cout << "我是新线程, 我的名字是:" << name << ",my tid is: " << pthread_self() << ", gval: " << gval << endl;gval++;sleep(1);}return 0;
}void* routine2(void* args)
{string name = static_cast<const char*>(args);while (true){cout << "我是新线程, 我的名字是:" << name << ",my tid is: " << pthread_self() << ", gval: " << gval << endl;gval++;sleep(1);int* p = nullptr;*p = 1;}return 0;
}int main()
{pthread_t tid1;int n = pthread_create(&tid1, nullptr, routine1, (void*)"thread-1");pthread_t tid2;int n2 = pthread_create(&tid2, nullptr, routine2, (void*)"thread-2");cout << "new thread id: " << tid1 << endl;cout << "new thread id: " << tid2 << endl;while (true){cout << "我是main线程..." << endl;sleep(1);}return 0;
}
调用routine2的线程出现了段错误,原本还运行得好好的3个线程,顷刻间全部都没了,并且爆出了同样的退出方式(段错误异常退出),所以线程间的异常退出方式都是共享的。进程间存在“组”的关系,同一个pid就被分为了一组,所以那些执行分支的pid都一样所以很好找到并同时杀死异常线程。遍历进程的时间复杂度仍然是 O(n),而每次访问一个进程的时间是 O(1)。
线程如果退出没有被主线程回收会呈现僵尸状态,所以pthread_join用于线程的回收。
pthread_join
pthread_join
是 POSIX 线程库(pthread
)中的一个函数,用于等待指定的线程终止,并获取该线程的返回值。它是多线程编程中常用的线程同步机制之一。用于主线程回收其创建的线程。
pthread_join是默认阻塞式等待,就是创建的目标线程(ID)如果没有返回或者终止或者死亡,主线程就会卡住等待子线程执行完,然后回收,所以这可以保证主线程承担起回收子线程的工作并一定是最后退出的。
retval是输出型参数,是一个二级指针,指向保存返回值的那个空间(返回一级指针),如果不关心子线程的返回值可以使用nullptr。相当于返回值传给*retval
int main()
{pthread_t tid1;int n = pthread_create(&tid1, nullptr, routine1, (void*)"thread-1");pthread_t tid2;int n2 = pthread_create(&tid2, nullptr, routine2, (void*)"thread-2");cout << "new thread id: " << tid1 << endl;cout << "new thread id: " << tid2 << endl;int p = pthread_join(tid1, nullptr);if (p != 0){cerr << "join error: " << p << ", " << strerror(p) << endl;return 1;}cout << "join success" << endl;while (true){cout << "我是main线程..." << endl;sleep(1);}return 0;
}
我们让主线程等待id为tid1的线程,由于这个线程是死循环的退不出,所以主线程运行到pthread_join那里会卡住。
可以看到确实主进程卡住了。
接着给两个子线程都加上break,子线程退出后就可以看到回收成功的信息了。
如果捕获的是一个错误的id,就会返回错误码,进而我们自己打印错误码对应的错误信息,线程创建之后也是要被等待和回收的,是为了防止僵尸线程的问题,并且知道新线程的执行结果和返回值,如果一个线程被成功回收,没有报错,那这个线程一定是已经执行完的了。
int main()
{pthread_t tid1;int n = pthread_create(&tid1, nullptr, routine1, (void*)"thread-1");pthread_t tid2;int n2 = pthread_create(&tid2, nullptr, routine2, (void*)"thread-2");cout << "new thread id: " << tid1 << endl;cout << "new thread id: " << tid2 << endl;int p = pthread_join(pthread_self(), nullptr);if (p != 0){cerr << "join error: " << p << ", " << strerror(p) << endl;return 1;}cout << "join success" << endl;while (true){cout << "我是main线程..." << endl;sleep(1);}return 0;
}
主线程是不能自己回收自己的,所以遇到了异常回收就报错了,此时n不等于0
传参问题和返回值问题
可以注意到创建线程函数的最后一个参数就是执行函数run的参数,是线程调用这个函数的时候自己传进去的,因为是void*类型,所以就意味着run的参数可以传递任何类型的指针变量,也就是可以传递变量,数字,类指针对象。
所以我今天传入一个类指针,让其在run函数里面执行类方法并输出结果,实现a+b的任务。
class ThreadData
{
public:ThreadData(const string& name, int a, int b):_name(name),_a(a),_b(b){}int excute(){return _a + _b;}string Name(){return _name;}~ThreadData(){}
private:string _name;int _a;int _b;
};void* routine1(void* args)
{ThreadData* td = static_cast<ThreadData*>(args);while (true){cout << "我是新线程, 我的名字是:" << td->Name() << ",my tid is: " << pthread_self() << endl;cout << "task result is : " << td->excute() << endl;sleep(1);break;}return (void*)10;
}// void* routine2(void* args)
// {
// string name = static_cast<const char*>(args);
// while (true)
// {
// cout << "我是新线程, 我的名字是:" << name << ",my tid is: " << pthread_self() << ", gval: " << gval << endl;
// gval++;
// sleep(1);
// break;
// }
// return 0;
// }int main()
{pthread_t tid1;ThreadData* td = new ThreadData("thread-1", 10, 20);int n = pthread_create(&tid1, nullptr, routine1, td);cout << "new thread id: " << tid1 << endl;int p = pthread_join(tid1, nullptr);if (p != 0){cerr << "join error: " << p << ", " << strerror(p) << endl;return 1;}cout << "join success" << endl;while (true){cout << "我是main线程..." << endl;sleep(1);}return 0;
}
可以看到当输出完a+b的值后线程return退出,所以线程的退出方式1:线程出口函数return,表示线程退出。pthread_join的第二个参数就肯定能拿到这个返回值10。
&ret是存放返回值的地址,此时ret里面装的就是10,由于10转成void*后是8字节的,所以需要强转成长整型才放得下。
int main()
{pthread_t tid1;ThreadData* td = new ThreadData("thread-1", 10, 20);int n = pthread_create(&tid1, nullptr, routine1, td);cout << "new thread id: " << tid1 << endl;void* ret = nullptr;int p = pthread_join(tid1, &ret);if (p != 0){cerr << "join error: " << p << ", " << strerror(p) << endl;return 1;}cout << "join success!, ret: " << (long long int)ret << endl;while (true){cout << "我是main线程..." << endl;sleep(1);}return 0;
}
在子线程中的返回值(void*)10是装在该线程地址空间的寄存器里的,所以上面的ret(第二个参数)是直接向寄存器拿值的。
理论上不同线程之间的堆区和栈区都是共享的,但是之前不是说线程有自己独立的栈吗,这些独立的栈和堆是谁拿到栈/堆空间的入口地址,谁就可以访问该栈/堆区。
void* routine1(void* args)
{ThreadData* td = static_cast<ThreadData*>(args);while (true){cout << "我是新线程, 我的名字是:" << td->Name() << ",my tid is: " << pthread_self() << endl;cout << "task result is : " << td->excute() << endl;sleep(1);break;}int* p = new int(10);return (void*)p;
}
可以看到主线程可以拿到此时创建在子线程堆区的返回值p,所以理论上不同线程之间的堆区和栈区都是共享的。返回值返回参数可以是变量,数字,对象。
我们还可以创建多线程,然后让每个线程都执行各自的的任务,然后汇总。
#include<iostream>
#include<pthread.h>
using namespace std;
#include<cstring>
#include<unistd.h>class ThreadData
{
public:void Init(const string& name, int a, int b){_name = name;_a = a;_b = b;}void excute(){_result = _a + _b;}string Name(){return _name;}void setid(pthread_t tid){_tid = tid;}int result(){return _result;}pthread_t id(){return _tid;}int A(){return _a;}int B(){return _b;}~ThreadData(){}
private:string _name;int _a;int _b;int _result;pthread_t _tid;
};//执行调用方法
void* routine(void* args)
{ThreadData* td = static_cast<ThreadData*>(args);td->excute();int* p = new int(10);return (void*)p;
}int main()
{ThreadData td[10];//初始化for (int i = 0; i < 10; i++){char id[64];snprintf(id, sizeof(id), "thread-%d", i);td[i].Init(id, i*10, i*20);}//创建多线程for (int i = 0; i < 10; i++){pthread_t id;pthread_create(&id, nullptr, routine, &td[i]);td[i].setid(id);}//等待多个线程for (int i = 0; i < 10; i++){pthread_join(td[i].id(), nullptr);}//汇总处理结果for (int i = 0; i < 10; i++){printf("td[%d]: %d+%d=%d[%ld]\n", i, td[i].A(), td[i].B(), td[i].result(), td[i].id());}return 0;
}
这也证明了不同的线程可以访问被人的栈空间,只要在同一个地址空间下。
线程可以使用return退出,但是不可以使用exit进行退出,因为exit退出的单位是整个进程,使用exit()
会导致整个 进程 退出,整个进程都退出了,其内部的执行分支线程也就跟着退出了。
//执行调用方法
void* routine(void* args)
{ThreadData* td = static_cast<ThreadData*>(args);td->excute();//int* p = new int(10);exit(1);
}
但是我们可以使用pthread_exit函数进行取代exit的效果。
pthread_exit
pthread_exit()
用于 终止当前线程,但不会影响其他线程或整个进程的运行。它是 exit()
在 多线程 环境下的 安全替代方案。任何地方调用exit表示进程退出,我们第二种线程退出的方式找到了:调用pthread_exit。
pthread_exit()
只终止 调用它的线程,不会影响 其他线程 或 整个进程,允许 线程返回值,该返回值可以被 pthread_join()
取出,如果主线程调用 pthread_exit()
,主线程会退出,但进程 不会终止,除非所有线程都结束。
传递的是一个指向返回值的指针。
//执行调用方法
void* routine(void* args)
{ThreadData* td = static_cast<ThreadData*>(args);td->excute();int* p = new int(10);pthread_exit(p);
}int main()
{ThreadData td[10];//初始化for (int i = 0; i < 10; i++){char id[64];snprintf(id, sizeof(id), "thread-%d", i);td[i].Init(id, i*10, i*20);}//创建多线程for (int i = 0; i < 10; i++){pthread_t id;pthread_create(&id, nullptr, routine, &td[i]);td[i].setid(id);}//等待多个线程for (int i = 0; i < 10; i++){void* cur = nullptr;pthread_join(td[i].id(), &cur);printf("%d\n", *(int*)cur);}
我们进一步发现pthread_join的第二个参数实际上是让返回值给其的解引用。当主线程执行return的时候表示进程结束。
线程取消
pthread_cancel()
用于 请求 取消某个线程的执行,但它不会立即终止线程,而是依赖于线程的取消点(cancellation point)。
向一个进程id发送取消指令,让这个进程不取消不执行run函数,pthread_cancel()
只是发送一个取消请求,必须在这个线程被创建之后才可以取消。
#include<iostream>
#include<pthread.h>
#include<string>
#include<unistd.h>
#include<cstring>
using namespace std;void* routine1(void* args)
{while (true){printf("hahahahahahahaha\n");sleep(1);}return (void*)20;
}int main()
{pthread_t tid1;int n = pthread_create(&tid1, nullptr, routine1, (void*)"thread-1");cout << "new thread id: " << tid1 << endl;sleep(5);//线程取消pthread_cancel(tid1);cout << "取消线程" << tid1 << endl;sleep(5);void* ret = nullptr;int p = pthread_join(tid1, &ret);if (p != 0){cerr << "join error: " << p << ", " << strerror(p) << endl;return 1;}cout << "join success!, ret: " << (long long int)ret << endl;while (true){cout << "我是main线程..." << endl;sleep(1);}return 0;
}
可以看到执行取消后就直接自动被终止了,然后子线程无法正常返回,pthread_join自然拿不到子线程的返回值,这时join并没有失效,主线程通过 pthread_join
获取返回值时,如果线程是被取消的,返回值是 PTHREAD_CANCELED,可以看到这个值就是值为-1的
特殊的指针值。
我们不建议直接取消正在运行的线程,因为我们无法判断当线程取消到来时这个线程的状态是什么,贸然取消很容易导致多线程混乱。
线程分离
pthread_detach
是 POSIX 线程库中的一个函数,用于将线程设置为“分离状态”(detached state)。分离状态的线程在终止时会自动释放其资源,主线程不需要调用 pthread_join
来等待它结束。当你发现主线程回收压力很大的时候,就可以使用线程分离。
线程再怎么分离也不可能分离出进程的地址空间的,单个线程可以自己进行分离也可以让主线程进行分离。
#include<iostream>
#include<pthread.h>
#include<string>
#include<unistd.h>
#include<cstring>
using namespace std;void* routine1(void* args)
{pthread_detach(pthread_self());while (true){printf("我是执行routine1的线程\n");printf("我是执行routine1的线程\n");printf("我是执行routine1的线程\n");break;}return (void*)20;
}int main()
{pthread_t tid1;int n = pthread_create(&tid1, nullptr, routine1, (void*)"thread-1");cout << "new thread id: " << tid1 << endl;sleep(5);void* ret = nullptr;int p = pthread_join(tid1, &ret);if (p != 0){cerr << "join error: " << p << ", " << strerror(p) << endl;return 1;}cout << "join success!, ret: " << (long long int)ret << endl;while (true){cout << "我是main线程..." << endl;sleep(1);}return 0;
}
#include<iostream>
#include<pthread.h>
#include<string>
#include<unistd.h>
#include<cstring>
using namespace std;void* routine1(void* args)
{while (true){printf("我是执行routine1的线程\n");sleep(1);}return (void*)20;
}int main()
{pthread_t tid1;int n = pthread_create(&tid1, nullptr, routine1, (void*)"thread-1");cout << "new thread id: " << tid1 << endl;pthread_detach(tid1);void* ret = nullptr;int p = pthread_join(tid1, &ret);if (p != 0){cerr << "join error: " << p << ", " << strerror(p) << endl;return 1;}cout << "join success!, ret: " << (long long int)ret << endl;while (true){cout << "我是main线程..." << endl;sleep(1);}return 0;
}
由于上面是在线程内部自己分离自己和主线程进行分离,此时如果仍让主线程回收的话,该线程 ID 会无效,表示线程已经是分离状态而致使报错产生,主线程已经找不到子线程的id了,线程分离了就不能join回收了,调用 pthread_detach
后,线程的资源会在其终止时自动释放。换句话来说主线程不会等待被分离的进程,不会管的。使用 pthread_detach
可以避免资源泄漏,简化线程管理。