我们先来看一段代码:
#include <stdio.h> #include <stdlib.h> #include <pthread.h> //创建两个线程,分别对两个全变量进行++操作,判断两个变量是否相等,不相等打印 int a = 0; int b = 0; // 未初始化 和0初始化的成员放在bbs pthread_mutex_t mutex; void* route() { while(1) //初衷不会打印 { a++; b++; if(a != b) { printf("a =%d, b = %d\n", a, b); a = 0; b = 0; } } } int main() { pthread_t tid1, tid2; pthread_create(&tid1, NULL, route, NULL); pthread_create(&tid2, NULL, route, NULL); pthread_join(tid1, NULL); pthread_join(tid2, NULL); return 0; }
这段代码的运行结果优点出乎我们的预料:
我们预计的结构应该是不会打印的,而这里去打印出了我们意想不到的结果。连相等的数据都打印了出来,为什么会出现这样的情况呢?
解释:两个线程互相抢占CPU资源,一个线程对全局变量做了++操作之后,还没来得及比较输出操作,另一个线程抢占CPU,进行比较打印输出。为了避免这样的情况,就需要用到下面介绍的互斥锁。
互斥量(锁):用于保护关键的代码段,以确保其独占式的访问。
1.定义互斥量: pthread_mutex_t mutex;
2.初始化互斥量: pthread_mutex_init(&mutex, NULL); //第二个参数不研究置NULL; //初始化为 1 (仅做记忆)
3.上锁 pthread_mutex_lock(&mutex); 1->0; 0 等待
4.解锁 pthread_mutex_unlock(&mutex); 置1 返回
5.销毁 pthread_mutex_destroy(&mutex);
返回值:若成功返回0,若出错返回错误编号。
说明: 互斥锁,在多个线程对共享资源进行访问时,在访问共享资源前对互斥量进行加锁,在访问完再进行解锁,在互斥量加锁后其他的线程将阻塞,直到当前的线程访问完毕并释放锁。如果释放互斥锁时有多个线程阻塞,所有阻塞线程都会变成可运行状态,第一个变成可运行状态的线程可以对互斥量加锁。这样就保证了每次只有一个线程访问共享资源。
至此,我们好像能通过互斥锁解决上面的问题:
#include <stdio.h> #include <stdlib.h> #include <pthread.h> int a = 0; int b = 0; // 未初始化 和0初始化的成员放在bbs pthread_mutex_t mutex; void* route() { while(1) //初衷不会打印 { pthread_mutex_lock(&mutex); a++; b++; if(a != b) { printf("a =%d, b = %d\n", a, b); a = 0; b = 0; } pthread_mutex_unlock(&mutex); } } int main() { pthread_t tid1, tid2; pthread_mutex_init(&mutex, NULL); pthread_create(&tid1, NULL, route, NULL); pthread_create(&tid2, NULL, route, NULL); pthread_join(tid1, NULL); pthread_join(tid2, NULL); pthread_mutex_destroy(&mutex); return 0; }
现有如下场景:线程1和线程2,线程1执行函数A,线程2执行函数B,现只使用一把锁,分别对A,B函数的执行过程加锁和解锁。
#include <stdio.h> #include <stdlib.h> #include <pthread.h> #include <unistd.h> //线程的取消动作发生在加锁和解锁过程中时,当发生线程2取消后而没有进行解锁时,就会出现线程1将一直阻塞 pthread_mutex_t mutex; void* odd(void* arg) { int i = 1; for(; ; i+=2) { pthread_mutex_lock(&mutex); printf("%d\n", i); pthread_mutex_unlock(&mutex); } } void* even(void* arg) { int i = 0; for(; ; i+=2) { pthread_mutex_lock(&mutex); printf("%d\n", i); pthread_mutex_unlock(&mutex); } } int main() { pthread_t t1, t2; pthread_mutex_init(&mutex, NULL); pthread_create(&t1, NULL, even, NULL); pthread_create(&t2, NULL, odd, NULL); //pthread_create(&t3, NULL, even, NULL); sleep(3); pthread_cancel(t2); //取消线程2,这个动作可能发生在线程2加锁之后和解锁之前 pthread_join(t1, NULL); pthread_join(t2, NULL); pthread_mutex_destroy(&mutex); return 0; }
一种极限情况是:线程2的取消发生在线程2的解锁之前,那么就会导致因为锁没解开,而线程1无法继续运行。
解决这样的问题我们可以用到下面的宏函数:
宏: //注册线程回调函数,可用来防止线程取消后没有解锁的问题
void pthread_cleanup_push(void (*routine)(void *), //回调函数
void *arg); //回调函数的参数
//回调函数执行时机
1.pthread_exit
2.pthread_cancel
3.cleanaup_pop参数不为0,当执行到cleaup_pop时,调用回调函数
void pthread_cleanup_pop(int execute);
#include <stdio.h> #include <stdlib.h> #include <pthread.h> #include <unistd.h> //线程的取消动作发生在加锁和解锁过程中时,当发生线程2取消后而没有进行解锁时,就会出现线程1将一直阻塞 pthread_mutex_t mutex; void callback(void* arg) //在cancel中进行解锁 { printf("callback\n"); sleep(1); pthread_mutex_unlock(&mutex); } void* odd(void* arg) { int i = 1; for(; ; i+=2) { pthread_cleanup_push(callback, NULL);//因为调用了cancel函数,从而触发了回调函数。 pthread_mutex_lock(&mutex); printf("%d\n", i); pthread_mutex_unlock(&mutex); pthread_cleanup_pop(0); } } void* even(void* arg) { int i = 0; for(; ; i+=2) { pthread_mutex_lock(&mutex); printf("%d\n", i); pthread_mutex_unlock(&mutex); } } int main() { pthread_t t1, t2; pthread_mutex_init(&mutex, NULL); pthread_create(&t1, NULL, even, NULL); pthread_create(&t2, NULL, odd, NULL); //pthread_create(&t3, NULL, even, NULL); sleep(3); pthread_cancel(t2); //取消线程2,这个动作可能发生在线程2加锁之后和解锁之前 //pthread_mutex_unlock(&mutex); 有问题,如果执行even的程序有两个,而一个取消线程的函数执行时正好t3函数阻塞,就会导致t3和t1同时在执行even pthread_join(t1, NULL); pthread_join(t2, NULL); pthread_mutex_destroy(&mutex); return 0; }
注意:
1.不要销毁一个已经加锁的互斥量,销毁的互斥量确保后面不会再有线程使用。
2.上锁和解锁函数要成对的使用
3.选择合适的锁的粒度(数量)。如果粒度太粗,就会出现很多线程阻塞等待相同锁,源自并发性的改善微乎其微。如果锁的粒度太细,那么太多的锁的开销会使系统的性能受到影响,而且代码会变得相当复杂。
4.加锁要加最小(范围)锁,减少系统负担
使用互斥锁一定要注意避免死锁:《Linux高性能服务器编程》 14.5.3 介绍了两个互斥量因请求顺序产生死锁问题
如果线程试图对同一个互斥量加锁两次,那么它自身就会陷入死锁状态,使用互斥量时,还有其他更不明显的方式也能产生死锁。例如,程序中使用多个互斥量时,如果允许一个线程一直占有第一个互斥量,并且在试图锁住第二个互斥量时处于阻塞状态,但是拥有第二个互斥量的线程也在试图锁住第一个互斥量,这时就会发生死锁。因为两个线程都在相互请求另一个线程拥有的资源,所以这两个线程都无法向前运行,于是就产生死锁。
可以通过小心地控制互斥量加锁的顺序来避免死锁的发生。例如,假设需要对两个互斥量A和B同时加锁,如果所有线程总是在对互斥量B加锁之前锁住互斥量A,那么使用这两个互斥量不会产生死锁(当然在其他资源上仍可能出现死锁);类似地,如果所有的线程总是在锁住互斥量A之前锁住互斥量B,那么也不会发生死锁。只有在一个线程试图以与另一个线程相反的顺序锁住互斥量时,才可能出现死锁。
为了应对死锁,在实际的编程中除除了加上同步互斥量之外,还可以通过以下三原则来避免写出死锁的代码:
1>短:写的代码尽量简洁
2>平:代码中没有复杂的函数调用
3>快:代码的执行速度尽可能快
自旋锁: 应用在实时性要求较高的场合(缺点:CPU浪费较大)
pthread_mutex_spin;
pthread_spin_lock() ; //得不到时,进入忙等待,不断向CPU进行询问请求
pthread_spin_unlock();
pthread_spin_destroy(pthread_spinlock_t *lock);
pthread_spin_init(pthread_spinlock_t *lock, int pshared);
读写锁(共享-独占锁):应用场景---大量的读操作 较少的写操作
注意:读读共享, 读写互斥,写优先级高(同时到达)
1. pthread_rwlock_t rwlock;//定义
2.int pthread_rwlock_init()//初始化
3.pthread_rwlock_rdlock()//pthread_rwlock_wrlock//读锁/写锁
4.pthread_rwlock_unlock() // 解锁
5.int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);//销毁锁
返回值:成功返回0,出错返回错误编号
说明:不管什么时候要增加一个作业到队列中或者从队列中删除作业,都用写锁,
不管何时搜索队列,首先获取读模式下的锁,允许所有的工作线程并发的搜索队列。在这样的情况下只有线程
搜索队列的频率远远高于增加或删除作业时,使用读写锁才可能改善性能。
#include <stdio.h> #include <stdlib.h> #include <pthread.h> #include <unistd.h> //创建8个线程,3个写线程,5个读线程 pthread_rwlock_t rwlock; int counter = 0; void* readfunc(void* arg) { int id = *(int*)arg; free(arg); while(1) { pthread_rwlock_rdlock(&rwlock); printf("read thread %d : %d\n", id, counter); pthread_rwlock_unlock(&rwlock); usleep(100000); } } void* writefunc(void* arg) { int id = *(int*)arg; free(arg); while(1) { int t = counter; pthread_rwlock_wrlock(&rwlock); printf("write thread %d : t= %d, %d\n", id, t, ++counter); pthread_rwlock_unlock(&rwlock); usleep(100000); } } int main() { pthread_t tid[8]; pthread_rwlock_init(&rwlock, NULL); int i = 0; for(i = 0; i < 3; i++) { int* p =(int*) malloc(sizeof(int)); *p = i; pthread_create(&tid[i], NULL, writefunc, (void*)p); } for(i = 0; i < 5; i++) { int* p = (int*)malloc(sizeof(int)); *p = i; pthread_create(&tid[3+i], NULL, readfunc, (void*)p); } for(i = 0; i < 8; i++) { pthread_join(tid[i], NULL); } pthread_rwlock_destroy(&rwlock); return 0; }
条件变量: 如果说互斥锁是用于同步线程对共享数据的访问的化,那么条件变量这是用于在线程之间同步共享数据的值。条件变量提供了一种线程间的通信机制:当某个共享数据达到某个值的时候,唤醒等待这个共享数据的线程
1.定义条件变量 pthread_cond_t cond;
2.初始化 pthread_cond_init(&cond, NULL);
3.等待条件 pthread_cond_wait(&cond, &mutex);
mutex :如果没有在互斥环境,形同虚设
在互斥环境下:wait函数将mutex置1,wait返回,mutex恢复成原来的值
4.修改条件 pthread_cond_signal(&cond);
5.销毁条件 pthread_cond_destroy(&cond);
规范写法: pthread_mutex_lock(); while(条件不满足) pthread_cond_wait(); //为什么会使用while? //因为pthread_cond_wait是阻塞函数,可能被信号打断而返回(唤醒),返回后从当前位置向下执行, 被信号打断而返回(唤醒),即为假唤醒,继续阻塞 pthread_mutex_unlock(); pthread_mutex_lock(); pthread_cond_signal(); //信号通知 ---- 如果没有线程在等待,信号会被丢弃(不会保存起来)。 pthread_mutex_unlock();
#include <stdio.h> #include <pthread.h> #include <unistd.h> #include <stdlib.h> //创建两个线程一个wait print,一个signal sleep() pthread_cond_t cond; pthread_mutex_t mutex; void* f1(void* arg) { while(1) { pthread_cond_wait(&cond, &mutex); printf("running!\n"); } } void* f2(void* arg) { while(1) { sleep(1); pthread_cond_signal(&cond); } } int main() { pthread_t tid1, tid2; pthread_cond_init(&cond, NULL); pthread_mutex_init(&mutex, NULL); pthread_create(&tid1, NULL, f1, NULL); pthread_create(&tid2, NULL, f2, NULL); pthread_join(tid1, NULL); pthread_join(tid2, NULL); pthread_cond_destroy(&cond); pthread_mutex_destroy(&mutex); return 0; }
System V //基于内核持续性
信号量: POSIX //基于文件持续性的信号量
1.定义信号量: sem_t sem;
2,初始化信号量: sem_init(sem_t* sem,
int shared, //0表示进程内有多少个线程使用
int val); //信号量初值
3.PV操作 int sem_wait(sem_t* sem); //sem--;如果小于0,阻塞 P操作
int sem_post(sem_t* sem); //sem++; V操作
4.销毁 sem_destroy(sem_t* sem);
信号量实现生产者消费者模型:
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <pthread.h> #include <semaphore.h> //仓库中装产品编号,没装产品的位置,置为-1,装了的地方置为产品的编号 #define PRO_COUNT 3 #define CON_COUNT 2 #define BUFSIZE 5 sem_t sem_full; //标识可生产的产品个数 sem_t sem_empty; //表示可消费的产品个数 pthread_mutex_t mutex; //互斥量 int num = 0; //产品编号 int buf[BUFSIZE]; //仓库 int wr_idx; //写索引 int rd_idx; //读索引 void* pro(void* arg) { int i = 0; int id = *(int*)arg; free(arg); while(1) { sem_wait(&sem_full); //先判断仓库是否满 pthread_mutex_lock(&mutex); //互斥的访问具体的仓库的空闲位置 printf("%d生产者开始生产%d\n", id, num); for(i = 0; i < BUFSIZE; i++) { printf("\tbuf[%d]=%d", i, buf[i]); if(i == wr_idx) { printf("<====="); } printf("\n"); } buf[wr_idx] = num++; //存放产品 wr_idx = (wr_idx + 1) % BUFSIZE; printf("%d生产者结束生产\n", id); pthread_mutex_unlock(&mutex); sem_post(&sem_empty); sleep(rand()%3); } } void* con(void* arg) { int i = 0; int id = *(int*)arg; free(arg); while(1) { sem_wait(&sem_empty); pthread_mutex_lock(&mutex); printf("%d消费者开始消费%d\n", id, num); for(i = 0; i < BUFSIZE; i++) { printf("buf[%d]=%d", i, buf[i]); if(i == rd_idx) { printf("=====>"); } printf("\n"); } int r = buf[rd_idx]; buf[rd_idx] = -1; rd_idx = (rd_idx+1)%BUFSIZE; sleep(rand()%4); printf("%d\n消费者消费完%d\n", id, r); pthread_mutex_unlock(&mutex); sem_post(&sem_full); sleep(rand()%2); } } int main() { pthread_t tid[PRO_COUNT+CON_COUNT]; pthread_mutex_init(&mutex, NULL); //初始化 sem_init(&sem_empty, 0, 0); sem_init(&sem_full, 0, BUFSIZE); srand(getpid()); int i = 0; for(i = 0; i < BUFSIZE; i++) //初始化仓库 -1表示没有品 buf[i] = -1; for(i = 0; i < PRO_COUNT; i++) //产生生产者 { int *p = (int*)malloc(sizeof(int)); *p = i; pthread_create(&tid[i], NULL, pro, p); } for(i = 0; i < CON_COUNT; i++) { int *p = (int*)malloc(sizeof(int)); *p = i; pthread_create(&tid[i+CON_COUNT], NULL, con, p); } for(i = 0; i < PRO_COUNT + CON_COUNT; i++) { pthread_join(tid[i], NULL); } pthread_mutex_destroy(&mutex); //销毁 sem_destroy(&sem_empty); sem_destroy(&sem_full); return 0; }
拓展学习:
乐观锁和悲观锁?
乐观锁:
在关系数据库管理系统里,乐观并发控制(又名”乐观锁”,Optimistic Concurrency Control,缩写”OCC”)是一种并发控制的方法。它假设多用户并发的事务在处理时不会彼此互相影响,各事务能够在不产生锁的情况下处理各自影响的那部分数据。在提交数据更新之前,每个事务会先检查在该事务读取数据后,有没有其他事务又修改了该数据。如果其他事务有更新的话,正在提交的事务会进行回滚。
乐观并发控制的事务包括以下阶段:
1. 读取:事务将数据读入缓存,这时系统会给事务分派一个时间戳。
2. 校验:事务执行完毕后,进行提交。这时同步校验所有事务,如果事务所读取的数据在读取之后又被其他事务修改,则产生冲突,事务被中断回滚。
3. 写入:通过校验阶段后,将更新的数据写入数据库。
优点和不足:
乐观并发控制相信事务之间的数据竞争(data race)的概率是比较小的,因此尽可能直接做下去,直到提交的时候才去锁定,所以不会产生任何锁和死锁。但如果直接简单这么做,还是有可能会遇到不可预期的结果,例如两个事务都读取了数据库的某一行,经过修改以后写回数据库,这时就遇到了问题。
悲观锁:
在关系数据库管理系统里,悲观并发控制(又名”悲观锁”,Pessimistic Concurrency Control,缩写”PCC”)是一种并发控制的方法。它可以阻止一个事务以影响其他用户的方式来修改数据。如果一个事务执行的操作读某行数据应用了锁,那只有当这个事务把锁释放,其他事务才能够执行与该锁冲突的操作。
优点和不足:悲观并发控制实际上是“先取锁再访问”的保守策略,为数据处理的安全提供了保证。但是在效率方面,处理加锁的机制会让数据库产生额外的开销,还有增加产生死锁的机会;另外,在只读型事务处理中由于不会产生冲突,也没必要使用锁,这样做只能增加系统负载;还有会降低了并行性,一个事务如果锁定了某行数据,其他事务就必须等待该事务处理完才可以处理那行数
系统最多能够创建多少个线程? (一般以实测为准,但根据每次开辟的栈的大小不同,测试结果也会不同)。
一个是直接在命令行查看 cat /proc/sys/kernel/threads-max 我的电脑显示是 7572
另一个是自己计算 用户空间大小3G 即是3072M/8M栈空间 = 380
第三个写程序: 跑到32754(理论值 32768)
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <pthread.h> //创建线程 void* foo(void* arg) { } int main() { int count = 0; pthread_t thread; while(1) { if(pthread_create(&thread, NULL, foo, NULL) != 0) return 1; count++; printf("MAX = %d\n", count); } return 0; }
转自:(9条消息) 线程的几种锁及基本操作_czf的编程工坊-CSDN博客_线程锁