目录
线程操作
引入 -- pthread库
- 我们前面说过,linux中只有轻量化进程,而没有真正的线程
- 所以linux只能提供操作轻量化进程的接口
- 但是用户并不了解linux的特性,他们只知道应该有单独操作线程的接口
- 所以为了满足用户需要,就在用户层封装出了多线程操作的函数库 -- pthread库,也被叫做原生线程库
- 我们在使用时,需要带上头文件<pthread.h>
-lpthread
- 因为不是所有的程序都需要用到多线程操作的库
- 所以编译器默认情况下不会自动包含pthread库
- 如果我们需要用的话,需要手动让编译器知道我们使用了这个库的函数
- 由于这是linux提供的库,所以链接器肯定可以找到这个库
pthread_create() -- 创建线程
函数原型
thread
是一个输出型参数,用于保存线程id
其中,pthread_t类型是线程id的类型
attr
用于指定新线程的属性的指针
我们不用管这个,传入nullptr使用默认属性即可
start_routine
一个函数指针,该函数作为该线程要执行的任务
参数和返回值都是void*
- 参数 -- 系统自动将arg参数传入进去
- 返回值 -- 返回给主线程想要返回的信息(由于类型是void*,所以想传什么类型的信息都可以)
arg
作为start_routine的参数,传入进去
也就是说,我们创建出的新线程,可以使用arg这个变量
返回值
- 如果创建成功,返回0
- 失败,返回错误码,并且线程id是未定义的 (pthreads函数出错时不会设置全局变量errno,而是直接返回错误码)
示例
#include <iostream> #include <string> #include <pthread.h> #include <unistd.h> #include <sys/types.h> using namespace std; void *func(void *args) { string arr = (char *)args; while (true) { cout << "im " << arr << " : " << getpid() << endl; sleep(1); } } void test1() { pthread_t tid; char arr[64]; for(int i=0;i<2;++i){ snprintf(arr,sizeof(arr),"%s %d","thread",i+1); pthread_create(&tid, nullptr, func, (void *)arr); sleep(1); } while (true) { cout << "im main thread : " << getpid() << endl; sleep(3); } }
我们创建了两个线程,都往显示器中打印自己的pid
- 会发现,他们的pid都是一样的
- 并且存在打印格式乱了的情况,说明他们在互相争抢资源(也就是说,并不存在访问控制,具体谁先执行由调度器决定)
查看线程
如果使用平时的ps -axj来显示进程,会发现只出现了一个,但我们应该有三个线程的:
ps -aL
所以,我们需要用新的选项来查看:
- 他们的pid都是一样的,但lwp有所不同(所以实际上,cpu调度时看的是lwp编号)
- 其中,有一个线程的pid和lwp是一样的,它是我们的主线程
- (对于之前学习的进程概念来说,它只有一个线程,是主线程
- 所以[进程操作的接口使用pid]和今天学习的新内容并不矛盾,因为那个线程的pid=lwp)
LWP
- LWP(Lightweight Process,轻量级进程)
- 也就是说,显示的那个LWP其实就代表了linux中轻量级进程的编号
线程等待
引入
- 和父子进程类似,线程也需要被主线程等待,不然也会导致内存泄漏
- 其中,父子进程的内存泄漏问题,我们是可以观察到的 -- 子进程的状态变成Z状态
- 而线程导致的问题,我们无法查看,因为线程只是作为进程的分支,我们只能看到整个进程的状态
- 所以,内存泄漏问题我们只要知道它存在就行
pthread_join
只能阻塞式等待
函数原型
thread
要等待线程的id
retval
- 输出型参数
- 用于获取线程结束时的返回值 (对,没错,就是线程任务函数的返回值)
- 返回值是void*类型,所以获取它就要用void**类型
- 使用该变量,强转类型即可
返回值
- 失败时返回错误码
- 可以通过strerror() / perror()来获取错误信息
示例
当副线程先行退出时:
void *func2(void *args) // 5s后退出 { int count = 0; while (true) { cout << "im new thread " << endl; sleep(1); ++count; if (count == 4) { break; } } cout << "new thread quit" << endl; char *arr = new char[20]; strcpy(arr, "im dead"); // 注意,返回的信息最好是全局变量/动态申请的资源,否则在其他线程使用该变量时,该线程已经退出了,属于野指针 return (void *)arr; } void test2() { pthread_t tid; pthread_create(&tid, nullptr, func2, nullptr); int count = 0; while (true) { cout << "im main thread : " << getpid() << endl; sleep(2); count++; if (count == 3) { break; } } void *ret = nullptr; pthread_join(tid, &ret); cout << "join success , the message : " << (char *)ret << endl; delete[] (char*)ret; cout << "main quit " << endl; }
可以看到,我们成功等待,且获取到线程的返回值:
验证阻塞等待
如果主线程进行等待时,副线程还未退出:
void test2() { pthread_t tid; pthread_create(&tid, nullptr, func2, nullptr); int count = 0; void *retval = nullptr; //验证阻塞等待 int ret = pthread_join(tid, &retval); cout << "join " << strerror(ret) << ", the message : " << (char *)retval << endl; while (true) { cout << "im main thread : " << getpid() << endl; sleep(2); count++; if (count == 3) { break; } } delete[] (char*)retval; cout << "main quit " << endl; }
会看到,主线程并未执行join后面的打印语句,而是阻塞等待副线程的退出:
线程退出
return
线程执行函数中,return语句就代表了该线程的结束
exit()
如果在线程中使用exit():
void *func2(void *args) // 5s后退出 { int count = 0; while (true) { cout << "im new thread " << endl; sleep(1); ++count; if (count == 4) { break; } } cout << "new thread quit" << endl; exit(1); return (void *)"im dead"; }
无论主线程在干什么,都会直接被终止:
因为exit终止的是整个进程,所以进程中的所有线程都会被终止
终止线程 与 终止进程的区别
当我们终止进程时,所有线程都会退出
- 其实我们终止的是主线程,我们看似使用的是进程pid,实际上使用的是主线程的lwp
- 因为线程需要使用进程(也就是主线程)的资源,主线程都退了,资源哪里来呢?
- 线程们自然也就退出了
发生异常
如果线程中发生了异常,无论是哪个线程,都会让整个进程直接退出:
void *func2(void *args) // 5s后退出 { int count = 0; int a = 1; a /= 0; char *arr = new char[20]; strcpy(arr, "im dead"); // 注意,返回的信息最好是全局变量/动态申请的资源,否则在其他线程使用该变量时,该线程已经退出了,属于野指针 while (true) { cout << "im new thread " << endl; sleep(1); ++count; if (count == 4) { break; } } cout << "new thread quit" << endl; // exit(1); return (void *)arr; }
可以看到,整个进程都因为异常退出了:
- 说明,无论是哪个线程引起的,信号是发给整个进程的,都会让整个进程退出
- 这也就是为什么线程的健壮性比较差,因为线程的异常会影响到整个进程
pthread_exit()
这是库提供的线程退出接口
函数原型
reval
- 用于保存退出信息 (和线程执行的函数的返回值有同样的作用)
- 可以被主线程中pthread_join函数的输出型参数获取
示例
void *func2(void *args) // 5s后退出 { int count = 0; char *arr = new char[20]; strcpy(arr, "im dead"); // 注意,返回的信息最好是全局变量/动态申请的资源,否则在其他线程使用该变量时,该线程已经退出了,属于野指针 pthread_exit((void *)arr); //终止线程,并返回错误信息 while (true) { cout << "im new thread " << endl; sleep(1); ++count; if (count == 4) { break; } } cout << "new thread quit" << endl; return (void *)arr; } void test2() { pthread_t tid; pthread_create(&tid, nullptr, func2, nullptr); int count = 0; void *retval = nullptr; // 验证阻塞等待 int ret = pthread_join(tid, &retval); cout << "join " << strerror(ret) << ", the message : " << (const char *)retval << endl; while (true) { cout << "im main thread : " << getpid() << endl; sleep(2); count++; if (count == 3) { break; } } cout << "main quit " << endl; }
可以看到,我们同样可以通过retval获取返回信息:
pthread_cancel()
- pthread_exit()用于结束自身的执行
- 而cancel用于在外部控制下结束线程,例如在主线程中请求取消某个工作线程
函数原型
只有一个参数,用于指定要取消的线程
示例
void *func3(void *args) { while (true) { cout << "im new thread " << endl; sleep(1); } cout << "new thread quit" << endl; return nullptr; } void test3() { pthread_t tid; pthread_create(&tid, nullptr, func3, nullptr); int count = 0; sleep(2); pthread_cancel(tid); cout << "cancel success " << endl; while (true) { cout << "im main thread : " << getpid() << endl; sleep(1); count++; if (count == 3) { break; } } cout << "main quit " << endl; }
可以看到,我们成功取消了创建的线程:
示例 -- 等待一个已经取消的进程
void *func3(void *args) // 验证取消 { char *arr = new char[20]; strcpy(arr, "im dead"); while (true) { cout << "im new thread " << endl; sleep(1); } cout << "new thread quit" << endl; return (void *)arr; } void test3() { pthread_t tid; pthread_create(&tid, nullptr, func3, nullptr); int count = 0; while (true) { cout << "im main thread : " << getpid() << endl; sleep(2); count++; if (count == 3) { break; } } pthread_cancel(tid); cout << "cancel success " << endl; void *retval = nullptr; // 验证阻塞等待 int ret = pthread_join(tid, &retval); cout << "join " << strerror(ret) << ", the message : " << (char *)retval << endl; cout << "main quit " << endl; }
会发现,我们的进程因为越界访问退出了:
为什么呢?
- 我们成功取消了,说明问题出现在join之后
- 那应该就是我们打印出了问题
原因
实际上,我们如果等待一个已经取消的线程,join函数会立即返回,并且返回PTHREAD_ CANCELED
- 也就是说,join函数并不能获取返回信息,它的retval是固定的
- 自然也就不能被我们强转为char*后使用了
PTHREAD_ CANCELED
- 是个宏,从-1强转得到
pthread_detach -- 分离线程
引入
- 已经退出的线程,其空间并没有被释放,仍然在占用进程的资源
- 那么当我们创建新的线程时,就无法复用退出线程的地址空间
- 所以我们需要等待线程,以清理资源
- 但我们有时候并不想等待怎么办,毕竟线程只能阻塞式等待
- 所以我们就引入了分离线程
介绍
- 将指定的一个线程标记为"可分离的"
- 也就是说,线程退出时会自动释放资源,而不需要等待其他线程调用pthread_join函数
函数原型
返回值
线程id
引入
- 前面一直在说线程id,但我们并没有特意看过它的值
- 所以,我们接下来就来介绍线程id
pthread_t类型
引入
线程id的类型(是一个抽象的线程标识符类型)
- 为什么这么说呢?原因将在后面介绍
- 它实际上只是普通类型进行包装了下:
pthread_self()
函数原型
用于获取当前线程的线程id(Thread ID)
线程id是一个唯一标识符,用于区分不同的线程
示例
void *func4(void *args) { sleep(1); pthread_t tid2 = pthread_self(); cout << "my tid : " << tid2 << endl; sleep(5); return nullptr; } void test4() { pthread_t tid; pthread_create(&tid, nullptr, func4, nullptr); cout << "tid : " << tid << endl; pthread_join(tid, nullptr); }
会发现pthread_self() 和 pthread_create接口为我们提供的线程id是一样的,且都是pthread_t类型:
- 都是很大的数字
- 但是,我们之前见到的编号都挺小的(文件fd,进程id),他们完全达不到这样的量级
为什么会这样呢?
- 其实这个并不是传统意义上的编号,而是从地址转换来的数字
- 转换为地址后:
- 那为什么会是地址呢?这就要说到线程在地址空间的分布了
pthread_t类型的线程id本质
引入
前面我们说了,线程有自己的独立栈,那这个独立栈结构是如何实现的呢?
- 如果直接使用进程的栈:
- 因为会进行上下文切换,所以线程之间不会互相影响
- 似乎可以
- 但要是计算机是多核的,多个线程的压栈操作就会混乱起来了
- 如果将进程的栈拆分成多个栈区域:
- 那就会让os察觉到有线程的存在(这和我们在用户层封装出线程矛盾了)
- 所以不行
- 既然线程是用户层的概念,那栈结构也就可以在用户层实现:
- 也就是下面的分布图了
地址空间分布图
- 首先我们明确,我们使用pthread库,就需要将它映射到进程的地址空间中
- 而我们一般使用的是动态链接
- 也就是说,pthread库被映射到我们的共享区(mmap(memory map)区域)
除此之外,这个动态库不仅要给我们提供接口,还会给我们提供一些资源
- 比如上面说要在用户层实现的线程独立栈结构
- 以及其他的资源
- 如图:
本质
- 所以,其实我们是将地址空间中,该线程所在空间的起始地址作为线程id
- 也就是说,pthread_ create函数第一个参数和pthread_ self()获取到的都是这个进程id
- 线程库的后续操作,就是根据该线程id来操作线程的
pid_t类型
gettid()
函数原型
注意,这里返回的id是pid_t类型嗷
介绍
它也可以用来获取线程id,但这个id是真实id(也就是轻量级进程的id)
- (是不是和getpid()很像,它用来获取进程id)
- 该函数并不是标准 POSIX 函数,而是 Linux 特有的(因为轻量级进程是linux特有)
- 但是我们似乎不能直接调用它:
- 似乎意思是这个系统调用并不包括在gilbc(GNU C Library)库中(?也许吧)
如何调用gettid
- 所以我们可以通过其他方式调用它:
pthread_t tid = syscall(SYS_gettid);
- 通过syscall这个系统调用接口,直接通过宏定义,来调用这个接口
syscall()
- 用于在 Linux 和一些其他类 Unix 操作系统上执行底层的系统调用
SYS_gettid
- 追根溯源后,这个宏其实是个整数,应该是内核中使用的某种编号
示例
void *func4(void *args) { sleep(1); pthread_t tid2 = pthread_self(); pthread_t tid1 = syscall(SYS_gettid); cout << "my tid : " << tid1 << endl; sleep(5); return nullptr; } void test4() { pthread_t tid; pthread_create(&tid, nullptr, func4, nullptr); pthread_join(tid, nullptr); }
可以看到,我们用gettid拿到的id和lwp编号是一样的:
__thread -- 局部存储
引入
我们的线程某些资源是共享的,比如全局区
但如果有时候,我们想要让某个全局变量只是在线程内作为全局变量呢?
- 这样就要用到局部存储的概念了:
- (这是地址空间中的内容,pthread库会为每个线程保存其局部数据)
示例
int g_val = 0; void* func6(void* args){ while (true) { sleep(1); ++g_val; cout << "im new : "<< g_val << " " << &g_val << endl; } } void test6() { pthread_t tid; pthread_create(&tid, nullptr, func6, nullptr); while (true) { sleep(2); ++g_val; cout << "im main : "<< g_val << " " << &g_val << endl; } }
这样,我们任何线程对其进行修改,都会影响其他线程:
如果我们不想这样,就可以使用__thread修饰该全局变量:
会看到,现在这两个进程就各自拥有自己的g_val了
介绍
- 用于声明线程局部变量
- 线程局部变量是每个线程独有的变量,每个线程都有自己的一份,互不干扰
- 在多线程编程中,使用线程局部变量可以避免对全局变量的竞争条件,提高程序的并发性
- 线程局部变量的生命周期与线程的生命周期相同