线程属性
Linux下的线程属性是可以根据实际项目需求进行设置,之前我们讨论的是采用线程默认的属性。默认属性已经可以解决大多数问题。如果我们对程序的性能提出更高的要求那么需要设置线程属性,比如可以通过设置线程栈的大小来降低内存使用从而增加最大线程数量。
主要属性:作用域、栈尺寸、栈地址、优先级、分离状态、调度策略
线程属性值不能直接设置,需要通过相关函数(可以理解为接口)进行操作:
int pthread_attr_init(pthread_attr_t *attr); //初始化线程属性变量 |
int pthread_attr_destroy(pthread_attr_t *attr); //释放线程属性的资源 |
int pthread_attr_getdetachstate(const pthread_attr_t *attr,int *detachstate); //获取线程分离的状态属性 |
int pthread_attr_setdetachstate(pthread_attr_t *attr,int detachstate); //设置线程分离的状态属性 |
查看线程的属性方法: man pthread_attr_XXX
案例:
//创建一个线程属性变量
pthread_attr_t attr;
//初始化属性变量
pthread_attr_init(&attr);
//设置属性
pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED);
//设置线程栈的大小
pthread_attr_setstacksize(&attr,size);
.....
#include <stdio.h> #include <pthread.h> #include <string.h> void* func(void *arg) { printf("子线程:%lu\n",pthread_self()); return NULL; } int main(void) { pthread_t tid; //创建线程属性变量 pthread_attr_t attr; //初始化 pthread_attr_init(&attr); //设置线程分离 pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED); //设置栈大小 int size = 256*1024; pthread_attr_setstacksize(&attr,size); pthread_create(&tid,&attr,func,NULL); while(1) { sleep(1); void* retval; int err = pthread_join(tid,&retval); if(err) printf("-------------err= %s\n", strerror(err)); else printf("-----------%d\n",(int)retval); } return 0; }
线程使用注意事项
1.主线程退出其他线程不退出,主线程调用 pthread_exit
2.避免僵尸线程
pthread_join
pthread_detach
设置线程属性为分离,然后 pthread_create
3.malloc和mmap申请的内存可以被其他线程释放
4.应避免在多线程模型中调用fork,除非马上exec。子进程中只有调用frok的线程存在,其他线程在子进程中均pthread_exit
5.信号的复杂语义很难和多线程共存,应避免在多线程引入信号机制
线程同步
先说同步的概念(不要觉得啰嗦,方便我们去理解线程同步):
所谓同步,对于不同的研究对象而言是具有不同的含义的。例如:设备同步,是指在两个设备之间规定一个共同的时间参考。秦始皇的“书同文,车同轨”岂不也是一种同步。而在编程中的同步是指协同、协助、相互配合,主要是为了协调步骤,按预定的先后次序运行。
线程同步
同步即协同步骤,按预定的先后次序运行。大家有没有想过一个问题,为什么我们要强调按预定的先后次序,主要是因为同一个进程内的线程之间是资源共享的,加上并发的原因,假设一个线程想要修改每一个数据,还没修改完,另一个线程就把它取出,是不是就会产生问题。
专业一点说:线程同步,指一个线程发出某一功能调用时,在没有得到结果之前,该调用不返回。同时其他线程为保证数据一致性,不能调用该功能。(一个线程对某一共享的资源没有调用完,其它线程不能调用)
详细分析:
1.线程的主要优势在于能够通过全局变量来共享信息。不过,这种便捷(便捷是与进程间通信比较得出的)的共享是有代价的,必须确保多个线程不会同时修改同一变量或者某一线程不会读取正在由其他线程修改的变量(你会发现可以同时读)。
2.临界区是指访问某一共享资源的代码片段,并且这段代码的执行应为原子操作。也就是同时访问同一共享资源的其他线程不应终断该片段的执行。
3.当有一个线程在对内存操作时,其他线程都不可以对这个内存地址进程操作,直到该线程完成操作,其他线程才能对该内存进行操作,而其他线程则处于等待状态。
那么如何才能更好的保持这种原子操作呢?
互斥量、信号量、XXX锁....机制
不晓得有没有发现,很多时候的发展就是为了解决某一个问题。在任意条件下,很难做到十全十美,或者说很难画出一个完美的圆,我们一直在不断的创新不断的去接近这个完美的圆。好似我们永远没有算不完π一样。
互斥量(互斥锁)
先说一个通俗的理解:现在有一个房间,并且这个房间同一个时刻只能容纳一个人,防止两个人或以上的人进入,现在给这个房间置办一把锁,当有人进去时,看门的人就把房间锁上,当人出来,把锁打开。又有人进去时,把锁锁上,如此而已。其实很多计算机中解决问题的办法跟我们实际生活有很大的联系的,细细体会
1.为了避免线程更新(修改)共享变量时出现问题,可以使用互斥量(mutex)来确保同时仅有一个线程可以访问某项共享资源。可以使用互斥量来保证对任意共享资源的原子访问。
2.互斥量有两种状态:已锁定(locked)和未锁定(unlocked)。任何时候,至多只有一个线程可以锁定该互斥量。试图对已经锁定的某一互斥量再次加锁将可能阻塞线程或者报错失败,具体取决于加锁时使用的方法。(我们回到上面的例子,我们管理人员每次只对一把锁打开,如果加了两把锁,那么屋子里的人出不来,屋子外的人进不去,而且管理员已经开了一把锁,他认为房间已经空了,等待人进去。导致永远阻塞在这了,呜呜...)
3.一旦线程锁定互斥量,随即成为该互斥量的所有者,只有所有者才能给互斥量解锁。一般情况下,对每一个共享资源会使用不同的互斥量,每一个线程在访问同一资源时将采用如下协议:
- 针对共享资源锁定互斥量(加锁)
- 访问共享资源(访问)
- 对互斥量解锁(解锁)
4.如果有多个线程试图执行这一块代码(临界区),事实上只有一个线程能够持有该互斥量(其他线程将遭遇阻塞),即同时只有一个线程能够进入这段代码区域
举个例子:
通过"锁"将资源的访问变长了互斥操作,而后与时间有关的错误也不会产生(按预计的次序执行)。
说明:当A线程对某个全局变量加锁访问,B在访问前尝试加锁,拿不到锁,B阻塞。C线程不去加锁,而直接访问该全局变量,依然能够访问,但会出现数据混乱。
所以,互斥锁实质上是一把"建议锁"(又称为”协同锁“),建议程序中有多线程访问共享资源的时候使用该机制,但不是强制使用。(什么意思呢,就是某一个线程在访问共享资源之前,不访问锁,直接去访问共享资源,也可以访问到。我们使用互斥量需要按照规定步骤来,防止数据混乱。我直接进房间,我不管是否有管理员)
相关函数
互斥量的类型:pthread_mutex_t
pthread_mutex_init函数
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr);
作用:初始化互斥量
参数:mutex 需要初始化的互斥量变量
attr 互斥量相关属性,通常传 NULL
restrict:C语言的修饰符,被修饰的指针不能由另外一个指针进行操作
pthread_mutex_destroy函数
int pthread_mutex_destroy(pthread_mutex_t *mutex);
作用:释放互斥量的资源
pthread_mutex_lock函数
int pthread_mutex_lock(pthread_mutex_t *mutex);
作用:加锁,阻塞的(如果一个线程加锁了,那么其他线程只能阻塞等待)
pthread_mutex_trylock函数
int pthread_mutex_trylock(pthread_mutex_t *mutex);
作用:尝试加锁,非阻塞(如果加锁失败,不会阻塞,会直接返回)
pthread_mutex_unlock函数
int pthread_mutex_unlock(pthread_mutex_t *mutex);
作用:解锁
上个案例:(目标->能够完整打印HELLO WORLD或hello world)
不加互斥锁:
#include <stdio.h> #include <pthread.h> #include <unistd.h> pthread_mutex_t mutex; //定义为全局变量,不能定义为栈上的临时变量 void *func(void *arg) { srand(time(NULL)); while(1) { //pthread_mutex_lock(&mutex); //加锁 printf("hello"); sleep(rand()%3); printf("world\n"); //pthread_mutex_unlock(&mutex); //解锁 sleep(rand()%3); } return NULL; } int main(void) { int n = 5; pthread_t tid; srand(time(NULL)); //设置随机种子 //初始化互斥量,在创建线程之前 pthread_mutex_init(&mutex,NULL); //创建线程 pthread_create(&tid,NULL,func,NULL); while(n--) { //pthread_mutex_lock(&mutex); //加锁 printf("HELLO"); sleep(rand()%3); printf("WORLD\n"); //pthread_mutex_unlock(&mutex); //解锁 sleep(rand()%3); } //销毁锁 pthread_mutex_destroy(&mutex); //关闭子线程 pthread_cancel(tid); //回收子线程,或者设置线程分离 pthread_join(tid,NULL); //pthread_detach(tid); return 0; }
加互斥锁
#include <stdio.h> #include <pthread.h> #include <unistd.h> pthread_mutex_t mutex; //定义为全局变量,不能定义为栈上的临时变量 void *func(void *arg) { srand(time(NULL)); while(1) { pthread_mutex_lock(&mutex); //加锁 printf("hello"); sleep(rand()%3); printf("world\n"); pthread_mutex_unlock(&mutex); //解锁 sleep(rand()%3); } return NULL; } int main(void) { int n = 5; pthread_t tid; srand(time(NULL)); //设置随机种子 //初始化互斥量,在创建线程之前 pthread_mutex_init(&mutex,NULL); //创建线程 pthread_create(&tid,NULL,func,NULL); while(n--) { pthread_mutex_lock(&mutex); //加锁 printf("HELLO"); sleep(rand()%3); printf("WORLD\n"); pthread_mutex_unlock(&mutex); //解锁 sleep(rand()%3); } //销毁锁 pthread_mutex_destroy(&mutex); //关闭子线程 pthread_cancel(tid); //回收子线程,或者设置线程分离 pthread_join(tid,NULL); //pthread_detach(tid); return 0; }
我们的讨论到这里就结束了吗?
当然没有,我们来看一下特殊的情况
我现在把代码改成这个样子,会得到什么结果呢....
进入了死循环,主线程竞争不到CPU了
线程在操作完共享资源后本应该立即解锁,但修改后,线程抱着锁睡眠。睡醒解锁后又立即加锁。这两个库函数本身不会阻塞。所以在这两行代码之间失去CPU的概率很小。因此,另一个线程很难得到加锁的机会。
我们再来修改代码:
发现子线程没有结束,父线程阻塞等待回收子线程
这个原因就很明显了,pthread_join会阻塞等待子线程结束,子线程进入死循环中..所以...
死锁
死锁产生的四个必要条件当时学习操作系统时(课本上定义):
1.互斥条件(我们互斥锁解决的就是互斥的共享资源,某一时刻只能有一个线程进入)
2.请求和保持条件(每一个进程都有保持现有状态)
3.不剥夺条件(没有外力的影响)
4.循环等待条件(等待其他进程释放资源)
这是课本中给我们的定义,后面加上了一些解释,方便理解。
那么在实际编程过程中的场景(主要有三种情况):
1.忘记释放锁
2.重复加锁
3.多线程多锁,抢占锁资源
(第一个种情况很好理解,这里就不过多的解释了。我们重点分析下第二种和第三种情况)
有时,一个线程需要同时访问两个或更多不同的共享资源,而每一个共享资源都由不同的互斥量管理。当超过一个线程加锁同一组互斥量时,就可能产生死锁。(对同一个互斥量加锁两次)
解决:访问完共享资源后立即解锁,等待步骤完成之后再加锁
两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,如无外力作用,他们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁。(线程1拥有锁A,请求锁B,线程2拥有锁B,请求锁A)
解决:trylock替代lock函数并解锁(当不获取所有锁时主动放弃所有锁)
上案例:
#include <stdio.h> #include <pthread.h> #include <unistd.h> pthread_mutex_t mutex1; void* deadlock1(void *arg) { pthread_mutex_lock(&mutex1); printf("hello"); pthread_mutex_lock(&mutex1); printf("world1\n"); pthread_mutex_unlock(&mutex1); pthread_mutex_unlock(&mutex1); return NULL; } int main(void) { pthread_t tid1; //初始化 pthread_mutex_init(&mutex1,NULL); //创建线程 pthread_create(&tid1,NULL,deadlock1,NULL); //设置线程分离 pthread_detach(tid1); //退出主线程 pthread_exit(0); return 0; }
#include <stdio.h> #include <pthread.h> #include <unistd.h> pthread_mutex_t mutex1; pthread_mutex_t mutex2; void* deadlock1(void *arg) { pthread_mutex_lock(&mutex1); printf("hello"); sleep(4); pthread_mutex_lock(&mutex2); printf("world1\n"); pthread_mutex_unlock(&mutex2); pthread_mutex_unlock(&mutex1); return NULL; } void* deadlock2(void *arg) { // sleep(1); pthread_mutex_lock(&mutex2); printf("HELLOE"); sleep(3); pthread_mutex_lock(&mutex1); printf("WORLD\n"); pthread_mutex_unlock(&mutex1); pthread_mutex_unlock(&mutex2); return NULL; } int main(void) { pthread_t tid1,tid2; //初始化 pthread_mutex_init(&mutex1,NULL); pthread_mutex_init(&mutex2,NULL); //创建线程 pthread_create(&tid1,NULL,deadlock1,NULL); pthread_create(&tid2,NULL,deadlock2,NULL); //设置线程分离 pthread_detach(tid1); pthread_detach(tid2); //退出主线程 pthread_exit(0); return 0; }