线程等待:
等待一个指定线程的退出,获取这个退出线程的返回值,并且允许系统回收这个线程占用的资源。
但是,并不是所有的线程都需要被等待,因此线程有一个属性,默认为joinable。处于这个属性的线程,退出后不会自动回收资源。需要其他线程进行等待处理。
如何等待:
int pthread_join(pthread_t tid, void **retval);
tid:用于指定要等待的线程
retval:输出性参数,返回线程的退出返回值
retval为什么是一个二级指针:
int a=10; test(int *b){*b=1;} test(&a)
a=? (10)
线程的返回值是一个一级指针,(b就是一个一级指针)因此对一级指针取地址应该用二级指针。
int *a{这个变量拥有一块空间,可以存放另一款空间的地址}
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>
void *thr_start(void *arg)
{
pthread_detach(pthread_self());//分离自己
printf("Normal thread will exit in 3 seconds\n");
sleep(3);
char *buf = "nihao\n"; //返回的是常量的地址
//char buf[] = "nihao\n"; //返回的是局部空间的地址
return buf;
}
int main()
{
pthread_t tid;
int ret = pthread_create(&tid, NULL, thr_start, NULL);
if (ret != 0) {
printf("thread create failed!!\n");
return -1;
}
printf("Main thread waiting for normal thread to exit\n");
char *retval = NULL;
//pthread_join(tid, (void**)&retval);//等待一个线程的退出,若线程没有退出,则会一直等待
//pthread_detach(tid);
while(1) {
printf("retval:%s\n", retval);
sleep(1);
}
return 0;
}
在函数中定义 char * buf=“nihao” 和 char buf[]=”nihao”有什么区别
“nihao”这个字符串是一个常量,存放在正文段。
char *buf=常量的地址 将常量的地址赋给buf,buf这个指针就指向了常量的空间
char buf[]=常量 给buf开辟空间,将常量的数据赋值给了这块空间
为什么要进行线程等待:
一个线程退出的时候有一个返回值,需要保存起来。因此这个线程虽然已经退出了,但是在虚拟地址空间中所占的资源没有被回收。其他线程通过线程等待的方式获取这个线程的返回值,然后回收这个线程所占的资源。
但有时候并不关心线程的返回值。此时如果还必须等待的话,会浪费资源。
因此设置了两种属性:joinable/detach。
默认属性为joinabl,这种线程退出后不会自动释放资源,因此需要被等待进行处理。
线程分离:
等待线程会退出,原因是joinable属性的线程资源无法自动被回收。过程等待线程退出,获取返回值,释放资源。
不想获取线程的返回值,直接释放线程资源,不需要再去等待:线程分离的实现
线程分离是在干什么:设置属性。设置线程属性,从joinable设置为detach,处于detach属性的线程退出后,会自动释放资源(这种线程不需要被等待)
pthread_join
是线程等待,是阻塞函数(如果线程没退出,就一直等待)
如何分类一个线程:
int pthread_detach(pthread_t tid);
分离一个指定的线程,设置这个指定线程的属性为detach
分离一个线程,可以在任意位置,任意线程中完成
线程安全:
多个执行流对临界资源进行争抢访问,而不会造成数据二义性或者逻辑混乱。称这段争抢访问的过程是线程安全的。
线程安全的实现:如何保证多个执行流对临界资源进行争抢访问而不会造成数据二义性
- 同步:通过条件判断,实现对临界资源访问的时序合理性
- 互斥:通过唯一访问,实现对临界资源访问的安全性
这里用一个例子说明:比如我们平时上厕所的时候。
互斥:进去上厕所的时候把门锁上,只要我不开门就没有别的人能进来。
同步:上完厕所,刚出来,本来应该让别人上厕所了,但是你此时又进去,你反复的进去出来,导致别人都上不了厕所。
互斥的实现技术:
线程互斥是指对于共享的操作系统资源,在各线程访问时的排他性。当有若干戈线程都要使用某一共享资源时,任何时刻最多只允许一个线程去使用,其他要使用该资源必须等到,直到占用资源者释放该资源。
互斥锁/信号量。
实现互斥的原理:只要保证同一时间只有一个执行流能够访问资源就是互斥。
1.对临界资源进行状态标记:
- 每人访问的时候标记为1,表示可访问。
- 有人正在访问的时候,就标记为0,表示不可访问。
2.再对临界资源进程访问之前先进行状态的判定,决定是否能够访问,不能访问则使其休眠。
能够实现互斥的技术中:互斥锁。
互斥锁:
其实就是一个计数器。只有0/1的计数器,用于标记资源当前的访问状态。 1:可访问(没有一个进程进入临界区) 0:不可访问
互斥锁想要实现互斥,每个线程在访问临界资源之前都优先访问同一个互斥锁(加锁),在进行状态判断是否可以访问。意味着互斥锁本身就是一个临界资源,涉及到计数器的修改,修改过程必须保证安全。
互斥锁的计数器操作如何实现原子性:
- 1.将cpu寄存器上的值修改为0,然后与内存中计数器进行数据交换。(意味着这时候计数器变成了0,谁来访问,都是不可访问状态,别人都进不去。这时候,寄存器就可以慢慢判断是否可以访问了。)
- 2.若寄存器交换后数据为0,则表示当前不可访问。则将pcb状态置为阻塞状态,线程将被挂起等待。
若寄存器交换后数据为1,则表示当前可以访问。则加锁操作直接返回,表示加锁成功,可以继续访问资源。 - 3.访问完数据后,要进行解锁操作(将内存中计数器的值修改回来)
互斥锁的代码操作流程:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>
int g_tickets = 100;//假设有100张火车票
//1. 定义互斥锁变量,并且这个变量也是一个临界资源,需要能够被各个线程访问到
//可以将互斥锁变量定义成为一个全局变量,也可以将其定义成局部变量,然后通过函数传参传递给线程
pthread_mutex_t mutex;
void *thr_Scalpers(void *arg)
{
//黄牛的毕生工作--抢票
//尽量避免对不需要加锁的操作进行加锁,会影响效率,因为在加锁期间别人都不能操作
//若加锁的操作越多,意味着需要的时间就更长
while(1) {
pthread_mutex_lock(&mutex);//加锁一定是在临界资源访问之前,保护的也仅仅是临界区
if (g_tickets > 0) {//当g_tickets=1的时候,判断成功,进入抢票流程
usleep(1000);//但是抢票有个过程,在这个期间,其他黄牛,也有可能判断成功,进入抢票流程
printf("I:[%p] got a train ticket:%d\n", pthread_self(), g_tickets);
g_tickets--;
//解锁是在临界资源访问完毕之后
pthread_mutex_unlock(&mutex);
usleep(1000);
}else {
//在任意有可能退出u线程的地方,记得解锁
//否则若退出没有解锁,则其它线程获取不到锁,就会卡死
pthread_mutex_unlock(&mutex);
pthread_exit(NULL);//没有票了就退出
}
}
return NULL;
}
int main()
{
int i = 0;
pthread_t tid[4];
//2. 初始化互斥锁,一定要在创建线程之前!!!
pthread_mutex_init(&mutex, NULL);
for (i = 0; i < 4; i++) {
int ret = pthread_create(&tid[i], NULL, thr_Scalpers, NULL);
if (ret != 0) {
printf("thread create failed!!\n");
return -1;
}
}
for (i = 0; i < 4; i++) {
pthread_join(tid[i], NULL); //等待普通线程退出,不想让主线程先退出
}
//5. 如果不使用互斥锁了,最终销毁互斥锁,释放资源
pthread_mutex_destroy(&mutex);
return 0;
}
1.定义互斥锁变量
pthreas_mutex_t mutex;
2.初始化互斥锁
mutex=PTHREAD_MUTEX_INITIALIZER
或者
int pthread_mutex_init(pthread_muvtex_t *mutex,pthread_mutexattr_t *attr)
mutex:互斥锁变量首地址
attr:互斥锁属性,通常置NULL
3.在对临界资源访问之前,先加锁(访问锁,判断是否可以访问)
int pthread_mutex_lock(pthread_muxtex_t *muxtex);
阻塞加锁,如果不能加锁,则一直等待。
int pthread_mutex_tylock(pthread_muxtex_t *muxtex);
非阻塞加锁,如果不能加锁,则立即报错返回。若可以加锁,则加锁后返回。
4.在对临界资源访问完毕之后,记得解锁(把状态标记变为可访问)
int pthreas_mutex_unlock(pthread_mutex_t *mutex);
5.不使用锁了,最终要释放资源,销毁互斥锁
int pthreas_mutex_destory(pthread_mutex_t *mutex);
两个问题便于理解:
a.在黄牛抢票的例子中,为什么加锁后一个黄牛抢完了所有的票?
解锁的同时刚好拿到了时间片,然后继续运行,继续解锁,再拿到时间片。所以一直会运行。直到进程结束。
b.没有票了,大家为什么不退出?
如果最后一个黄牛进入网站后发现没有票了,会直接退出。但进入网站时黄牛加了锁,退出后没有解锁网站。其他人没有办法进入网站,会直接卡死。所以一定要解锁。
互斥锁操作流程中,需要注意:
1.加锁后,在任意有可能退出线程的地方记得解锁。
2.锁的初始化一定要在创建线程之前。
3.锁的销毁一定是保证没有人使用互斥锁的时候。
条件变量和信号量有什么区别?
1.条件变量促使线程等待的条件判断需要用户自身完成,而信号量是通过自身计数进行判断的
2.条件变量需要搭配互斥锁一起使用,但是信号量不需要
信号量与互斥锁有什么区别?
信号量是对资源进行计数的计数器,主要是用于实现同步的。而互斥锁只能实现互斥。
信号量作用域在进程间或线程间(linux仅线程间)。互斥锁作用域在线程间