一.线程控制
在Linux操作系统的视角,Linux下没有真正意义上的线程,而是用进程模拟的线程(LWP)。所以,Linux不会提供直接创建线程的系统调用,而是提供创建轻量级进程的接口。但是由于用户只认线程,所以pthread库会对下将Linux接口封装,对上给用户提供线程控制的接口,这种库被称为用户级线程库。 pthread 库在任何系统都要自带,因此也称原生线程库。
1.pthread_create() 创建线程
#include <pthread.h>int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg);
参数说明:
pthread_t *thread
:(线程标识符LWP)
const pthread_attr_t *attr
:
- 这个参数用于指定线程的属性。如果为
NULL
,则创建一个默认属性的线程。
void *(*start_routine)(void *)
:(新线程要执行的函数)
void *arg
:(该函数的参数)返回值:
- 如果成功,返回
0
- 如果失败,返回一个错误码
我们知道pthread库会对Linux提供创建轻量级进程的接口进行封装,pthread_create()底层就是clone()系统调用。
int (*fn)(void*) 要执行的函数
void* stack 线程独立的栈空间的指针
int flags 一个标志位参数,不同的标志控制不同的资源共享
void *arg 执行函数的参数
1.创建的新线程和main主线程谁先运行不确定。
2.给进程的时间片是固定的,进程里面的线程会对时间片进行瓜分。
3.如果多个线程执行的函数是同一个函数,该函数被重入。如果是不可重入函数就要添加保护。
4.进程定义的函数,每个线程都可以用。
5.全局变量在线程间是共享的
6.一个线程一旦异常,其它线程也会崩溃(如果给该进程内的所有线程发信号让它们退出,如何区分它们是否在一个进程里?它们会被链表维护起来,形成一个线程组)
7.线程栈不共享,堆可以共享。
栈不共享:不同线程栈中可能会有相同的变量名,线程自己在栈上定义的变量,等线程结束就会销毁。(并不是绝对访问不到,通过全局指针指向a线程栈上定义的变量,b线程也可以访问到,但不建议,有可能生命周期结束 访问到野指针)
堆共享:堆内存的分配在进程的地址空间内是全局可见的,不同线程通过同一个全局变量指针访问堆上数据。a线程申请了堆上空间,等a线程退出,堆空间也不会消失,需要delete手动释放。
8.传过去的参数arg,为什么是void类型?
便于传任何类型的参数。可以是变量 数字 结构体,如果要传多个参数就可以传一个结构体,里面包含要用到的参数。ThreadData *td=static_cast<ThreadData *>(args);对void*结构体进行转换.
2.pthread_join() 等待线程
pthread_join用于主线程等待其他线程的结束,并获取该线程的返回值。获取线程的执行结果
阻塞等待当前线程,直到指定的线程终止。
#include <pthread.h>int pthread_join(pthread_t thread, void **retval);
参数说明:
thread
:要等待的线程的线程ID。该线程必须是已经创建并且未结束的线程。retval
:一个指向指针的指针,用于接收线程的返回值。如果不关心线程的返回值,可以将该参数设为NULL
。返回值:
- 成功:返回
0
。- 失败:返回一个错误码,通常是一个负数。常见的错误包括:
ESRCH
:指定的线程不存在或无效。EINVAL
:线程已经被取消或线程是主线程。
1.返回值void **retval,用法:
先定义个void *ret=nullptr,pthread_join(th,&ret); 此时ret=返回结果。eg.返回常数return (void*)10 ,ret 类型虽然是void*指针,但里面不是地址而是常数10。想输出返回值cout<<(long long int)ret<<endl; (因为64位下指针大小8字节 用long long 8字节防止精度损失)
如果返回结构体,也可以Date *ret=nullptr; pthread_join(th,(void**)&ret); ret=返回结果,且ret类型为Date*
2.如果有一个类,里面有需要执行的函数,我想让不同线程执行不同函数,执行完成后再把结果存入类的成员变量中。最后让主线程把存入结果的所有成员变量输出。
定义一个全局的类对象,这样每个线程都可以访问到这个类对象,执行完成就把结果写入成员变量。但什么时候全部执行完呢?
pthread_join(th,nullptr)可以保证执行完成,执行结果写入成员变量。执行结果写入成员变量,可以不用接收返回值。也可以把类对象定义在主线程中,把类对象作为函数的参数,新线程中通过访问类对象执行相应函数。等到等待结束,就可以确定主线程中定义的类对象中所有成员变量都被写入结果。
3.pthread_exit() 线程退出
用于终止当前线程的函数。但对其它线程没有影响
void* retval 当前线程退出时的返回值 用pthread_join()接收
void pthread_exit(void *retval);
线程退出的方式:
1.自然退出 return2.显示退出 pthread_exit() 终止当前线程
3.异常退出 exit() 会让整进程退出,所有线程都被终止
4.pthread_cancel() 取消线程
用于请求取消另一个线程的函数。并不立即强制终止目标线程,而是发送一个取消请求,线程如何响应这个请求取决于该线程的状态和设置。
取消状态默认允许被取消,在某些合适的时机会终止。
int pthread_cancel(pthread_t thread);
thread 目标线程id
成功时返回 0。
失败时返回错误码。
取消线程必须要pthread_join(),获取的返回值为-1(PTHREAD_CANCELED)
线程会在某些“取消点”检查是否有取消请求,并作出响应。在实际应用中,使用pthread_cancel() 时需要谨慎,因为它可能会导致资源泄漏、数据不一致等问题
5.pthread_detach() 分离线程
用于将线程分离的函数。分离线程后,该线程的资源(包括线程的退出状态)会在线程结束时被自动回收,不需要其他线程通过 pthread_join() 来等待它的结束或获取退出状态。
int pthread_detach(pthread_t thread);
主线程要执行自己的代码不想等待新线程,且不关心新线程返回值,可以创建新线程和分离它,可以自动回收资源,避免僵尸状态。
对分离的线程调用 pthread_join(),会返回 EINVAL 错误。
6.pthread_self() 获取线程id
获取调用它的当前线程的线程 ID
#include <pthread.h>pthread_t pthread_self(void);
二.理解线程库
1.线程id是什么?
我们有主线程和一个新线程,让它们打印自己的线程id用0x16进制打印。
这一长串数字像什么?这其实是地址。是虚拟地址
每个线程都可以调用pthread库的方法,是因为库映射到了进程的虚拟内存。
那我们想要获取线程的属性从哪里获取呢?对处理线程的方法由库封装提供,获取线程的属性,库也要对线程的属性进行提供,并维护。
因此在库中保存着所有进程中每个线程的属性,而线程的id就是线程属性在地址空间的虚拟地址。
2.线程id和地址空间
我们现在知道所有线程的属性都存在库中,那保存线程属性的结构体是什么样的,具体分布又是如何呢?
mmap区域就是共享区,线程id指向pthread库中对应属性的地址。保存线程属性的结构体有三个字段
1.struct pthread 里面就是线程的属性 tid 状态 优先级...
2.线程栈 我们知道每个线程的栈空间都是独立的,用来保存局部变量 函数调用完成后的返回地址 切换上下文保存现场,只有主线程的栈是在地址空间的栈区 而新线程的栈是在共享区动态申请的(map出来的区域),每新建一个线程就在动态库中保存记录它的属性并申请栈空间,栈空间的大小可以设定。
3.线程的局部存储,__thread。我们知道定义的全局变量 int val=100,所有进程都可以看到,A线程修改了全局变量,B线程访问该全局变量时是修改后的全局变量。访问到的全局变量地址是同一个值。
但用__thread int val=100;对它进行修饰,A线程修改了全局变量,B线程访问该全局变量值仍不变,A B线程访问到的地址各不相同。
__thread 修饰全局变量,让原本独一份的全局变量,给每个线程各一份。相当于在每个线程中定义了一个局部变量。
有什么用呢?当我们要访问线程id时,每次都要去库里面找,效率低。__thread pthread_t tid; 将线程id缓存到里面,下次访问的时候提高效率。
__thread 只能修饰内置类型