详细介绍:【Linux】线程控制
1.创建线程
创建一个新的线程我们要用到函数pthread_create,头文件#include <pthread.h>。
第一个参数一个输出型参数,获取线程的id;第二个参数是线程的属性,设为nullptr就行;第三个参数是一个返回值为void*,参数为void*的函数指针,对函数进行回调,代表新线程执行的入口函数;第四个参数是入口函数的参数。
创建成功返回0;失败返回一个错误码,第一个参数一个输出型参数就会未定义。
#include
#include
#include
#include
void* threadfunc(void* arg) //函数名随便起
{std::string name = (const char*)arg;while(true){sleep(1);std::cout << name << "运行中..." << std::endl;}return nullptr;
}
int main()
{pthread_t tid;pthread_create(&tid, nullptr, threadfunc, (void*)"线程1"); //新线程入口函数//主线程继续往后执行while(true){std::cout << "main线程运行中..." << std::endl;sleep(1);}return 0;
}
当我们创建好新线程后,新线程就会去执行threadfunc,主线程继续往后执行,此时新线程就是一组虚拟地址表示的代码和数据,主线程就是另外一组虚拟地址表示的代码和数据,各自执行各自的一部分。
当我们用g++直接编译这个代码的时候,发现会报错,而且是链接时报错。
因为我们需要指定第三方库pthread。
- -l库名:引入第三方库
//Makefile文件 test_thread:TestThread.ccg++ -o $@ $^ -lpthread
此时就可以编译通过了,我们再运行,结果如下。
想要查看库就用ldd指令。
证明只有一个进程
我们在打印时把线程的pid都打印出来。
#include
#include
#include
#include
void* threadfunc(void* arg) //函数名随便起
{std::string name = (const char*)arg;while(true){sleep(1);std::cout << name << "运行中..., pid:" << getpid() <
用下面的命令查看在运行的进程。
ps axj | head -1 && ps axj | grep test_thread

可以看到两个线程的pid是一样的,证明他们属于同一个进程。
- ps -aL:查看线程信息

可以看到查出来的两个线程PID一样,LWP不一样,LWP就是轻量级进程(light weight process)的意思,这两个执行流的LWP不同,证明进程内部存在两个线程,LWP和PID相同的就是主线程。
所以CPU在调度的时候看的是LWP,因为linux中只有轻量级进程,只有一个线程的进程在调度的时候也是看LWP,只不过此时PID和LWP是一样的。
说明
- 进程调度的时间片:等分给所有线程
void* threadfunc(void* arg) //函数名随便起
{std::string name = (const char*)arg;while(true){sleep(1);std::cout << name << "运行中..., pid:" << getpid() <
我们设置除0错误,系统会发送信号,让这个线程崩掉。
while :; do ps -aL; sleep 1; done //监控脚本

- 会发现任何一个线程崩溃会导致整个进程崩溃。其实就是因为信号是给进程发的。
细心的同学肯定发现了,代码在运行的时候,两个线程往显示器同时打印会导致打印内容混杂在一起。因为显示器的本质就是文件,两个线程都能往同一个显示器文件打印内容,此时的显示器文件就是一种共享资源,而线程在访问的时候共享资源没有锁的保护就会发生混着打印的样子。
2.线程等待
新线程创建后要被主线程等待,否则就会出现类似“僵尸进程”的问题,导致内存泄漏。
线程等待的函数为pthread_join,阻塞等待,成功返回0,失败错误码被设置。

第一个参数就是线程的id,第二个参数先设为nullptr,后面细说。
#include
#include
#include
#include
void* threadfunc(void* arg) //函数名随便起
{std::string name = static_cast(arg);int cnt = 5;while(cnt){sleep(1);std::cout << name << "运行中..., pid:" << getpid() <
上面的代码5秒之后就会退出。
线程ID和重入
我们把线程的ID打印出来,线程的ID是前面说过的LWP吗?
void ShowId(pthread_t &p)
{printf("tid:%ld\n", p);
}
int main()
{pthread_t tid;pthread_create(&tid, nullptr, threadfunc, (void*)"线程1"); //新线程入口函数ShowId(tid);pthread_join(tid, nullptr);//主线程等待return 0;
}

可以看到LWP并不是线程的ID,是一个非常大的数,我们打印的时候可以用16进制打印。
void ShowId(pthread_t &p)
{//printf("tid:%ld\n", p);printf("tid:0x%lx\n", p);
}
怎么证明这个ID就是这个线程的ID呢?可以用另一个函数接口,叫pthread_self,作用就是打印调用这个函数的线程的ID。

void* threadfunc(void* arg) //函数名随便起
{std::string name = static_cast(arg);pthread_t tid = pthread_self(); //谁调用就获取谁的printf("新线程调用self获得的tid:0x%lx\n", tid);int cnt = 5;while(cnt){sleep(1);std::cout << name << "运行中..., pid:" << getpid() <

主线程也是线程,也会有自己的线程ID,同样打印出来看看。
void ShowId(pthread_t &p)
{//printf("tid:%ld\n", p);printf("tid:0x%lx\n", p);
}
void* threadfunc(void* arg)
{std::string name = static_cast(arg);pthread_t tid = pthread_self(); //谁调用就获取谁的printf("新线程调用self获得的");ShowId(tid);while(cnt){sleep(1);std::cout << name << "运行中..., pid:" << getpid() <

我们可以发现,ShowId这个函数被两个线程都能调用,此时就叫这个函数被重入了,这种函数也叫可重入函数。
再增加一个全局变量做实验,让新线程做修改,主线程不修改只打印。
int g_val = 100;
void* threadfunc(void* arg)
{while(true){sleep(1);g_val--; //修改g_val}return nullptr;
}
int main()
{pthread_t tid;pthread_create(&tid, nullptr, threadfunc, (void*)"新线程1");while(true){//sleep(1);std::cout << "main线程打印g_val:" << g_val << std::endl;sleep(1);}return 0;
}

这也能说明线程是共享地址空间的。

返回值和参数
新线程是主线程创建的,主线程在join等待的时候,要获取新线程的退出信息,退出信息通过pthread_join函数的第二个参数获得,这个参数是一个输出型参数。

因为新线程的入口函数要求返回值和参数类型都是void*的,pthread_join函数的第二个参数要是一个二级指针,才能获得新线程的退出信息。
就好比下面这个例子,我们想从过func函数改变a的值,我们就要通过指针去修改。
void func(int a) //错误写法
{a = 20;
}
void func(int *a) //正确写法
{*a = 20;
}
main()
{a = 0;func(a); //错误写法func(&a); //正确写法
}
前面代码的a就相当于新线程的入口函数void*,我们要拿到这个函数里面的变化就要用二级指针。
比如我们现在让threadfunc函数随便返回一个值,在外面用join函数就能获取到。
void* threadfunc(void *arg)
{int n = 3;while(n--){sleep(1);std::cout << (char*)arg << "运行中..." << std::endl;}return (void*)123;
}
int main()
{pthread_t tid;pthread_create(&tid, nullptr, threadfunc, (void*)"新线程1");void* ret;pthread_join(tid, &ret); //退出信息在ret里std::cout << "新线程退出码:" << (long long int)ret << std:: endl;
}

思考一下,为什么线程退出没有异常相关的字段呢?

因为如果等待的线程出异常了,整个进程就退出了,包括main线程,所以join异常无意义,也看不到;join是基于线程健康状态的情况,不需要处理异常信号,异常信号是进程需要处理的。
新线程的入口函数返回值和参数可以传任意类型,包括对象,比如说现在有下面两个类。
class Add
{
public:Add(int a, int b):_a(a), _b(b){}int Execute(){return _a + _b;}
private:int _a;int _b;
};
class Result
{
public:Result(int ret) : _ret(ret){}int GetRet(){return _ret;}
private:int _ret;
};
然后给threadfunc函数传参传这个Add结构体,返回值返回Result结构体。
void* threadfunc(void *arg)
{Add *t = static_cast(arg);sleep(1);Result *r = new Result(t->Execute());sleep(1);return r;
}
int main()
{pthread_t tid;Add *pa = new Add(11, 22);pthread_create(&tid, nullptr, threadfunc, (void*)pa);Result* ret;pthread_join(tid, (void**)&ret);int n = ret->GetRet();std::cout << "新线程运行结果为:" << n << std:: endl;delete pa;return 0;
}

线程分离
因为pthread_join等待方式就是阻塞式等待,如果主线程不想等,想让新线程结束时自己释放,可以设置线程为分离状态。
- 线程分离可以是主线程分离新线程,也可以是新线程自己分离自己。
- 新线程被设置为分离状态后,线程依旧在进程的地址空间中,进程的资源还是可以和线程共享,但是主线程不再等待新线程。
- 分离之后不需要被join,join会失败。
分离线程用pthread_detach,参数就是要分离的线程ID。
void* threadfunc(void *arg)
{std::string name = static_cast(arg);int cnt = 3;while(cnt--){std::cout << name << "运行中..." << std::endl;sleep(1);}return nullptr;
}
int main()
{pthread_t tid;pthread_create(&tid, nullptr, threadfunc, (void*)"新线程1");pthread_detach(tid); //主线程分离新线程std::cout << "线程被分离" << std::endl;int cnt = 3;while(cnt--){sleep(1);std::cout << "main线程运行中..." << std::endl;}int ret = pthread_join(tid, nullptr); //分离的线程会join失败if(ret != 0){std::cout << "pthread_join error: " << strerror(ret) << std::endl;}return 0;
}

可以看到join失败了,证明新线程确实被主线程分离了。
还可以自己分离自己。
void* threadfunc(void *arg)
{pthread_detach(pthread_self()); //线程分离自己std::cout << "线程被分离" << std::endl;std::string name = static_cast(arg);int cnt = 3;while(cnt--){std::cout << name << "运行中..." << std::endl;sleep(1);}return nullptr;
}
int main()
{pthread_t tid;pthread_create(&tid, nullptr, threadfunc, (void*)"新线程1");int cnt = 3;while(cnt--){sleep(1);std::cout << "main线程运行中..." << std::endl;}int ret = pthread_join(tid, nullptr); //分离的线程会join失败if(ret != 0){std::cout << "pthread_join error: " << strerror(ret) << std::endl;}return 0;
}
3.线程终止
- 线程的入口函数return就是线程的终止。
- 线程不可以调用exit终止,因为exit是终止进程的。
- 可以调用pthread_exit终止线程,谁调用就终止谁,这个函数的参数就等于return后面跟的东西,传什么就返回什么。

- 还可以调用pthread_cancel,取消一个线程,参数就是要取消的线程的ID,一般是主线程取消新线程。

我们可以看一下线程被pthread_cancel后的退出结果。
void* threadfunc(void *arg)
{std::string name = static_cast(arg);while(true){sleep(1);std::cout << name << "运行中..." << std::endl;}return nullptr;
}
int main()
{pthread_t tid;pthread_create(&tid, nullptr, threadfunc, (void*)"新线程1");sleep(3);pthread_cancel(tid);//新线程运行3秒之后被主线程取消void* ret;pthread_join(tid, &ret);std::cout << "新线程运行结果为:" << (long long)ret << std::endl;return 0;
}

所以线程如果被取消,退出结果就是-1,-1是PTHREAD_CANCEL。
本次分享就到这里了,我们下篇见~
