马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?立即注册
x
同步与互斥概念
同步: 多个线程同时运行, 线程与线程之间可能存在某种关系, 须要让线程按照某种顺序举行执行.
如: 线程 A 产生须要处理的数据 X, 而处理这个数据的任务交给了线程 B. 那么线程 A, B 之间的运行关系应该是, A 线程先运行, 在 A 产生了数据 X 之后, B 线程在运行举行处理
互斥: 线程共享同一进程内的资源, 一个共享资源在被多个线程同时访问时, 就有可能会出现问题. 所以, 为了应对这种情况, 就须要让这些进程互斥的去访问这个共享资源.
如: 多个线程同时访问同一个文件, 然后同时向文件内写入数据, 在这种场景下, 每个线程存放的数据都有可能会被打乱, 导致文件中存储的数据不完整/无法识别. 所以我们就要让这些线程互斥的去向文件中写入数据.
锁的概念
在上面了解了同步和互斥的基本概念. 在了解锁之前还须要了解一些其他概念.
临界区资源: 被多个线程共享访问的资源就称为临界资源
临界区: 在每个线程内部, 访问临界区资源的代码, 就被称为临界区
原子性: 不会被打断的操纵, 这个操纵只有两种状态, 要么完成, 要么未完成 (不存在完成一部分)
大部分情况下, 线程使用的都是线程内的局部变量, 这些变量存储在线程的栈空间, 其他线程无法访问. 但也存在多线程共享某一个资源.
如经典例子: 卖票系统, 一共由10 0000张票, 两个线程同时售卖这10 0000张票.
- #include<pthread.h>
- #include<stdio.h>
- int sum = 100000000;
- void* func1(void* arg)
- {
- int x = 0;
- while(sum > 0)
- {
- --sum;
- }
- return NULL;
- }
- int main()
- {
- pthread_t t1;
- pthread_create(&t1, NULL, func1, NULL);
- pthread_t t2;
- pthread_create(&t2, NULL, func1, NULL);
- pthread_t t3;
- pthread_create(&t3, NULL, func1, NULL);
- pthread_join(t1, NULL);
- pthread_join(t2, NULL);
- pthread_join(t3, NULL);
- printf("sum: %d\n", sum);
- return 0;
- }
复制代码 会得到 sum 最后的效果为 -1.
我们来分析一下:
那么为了办理上面的问题: 就出现了锁.
对于多个线程都要访问的共享资源, 要求最多只能由一个线程举行访问. 当有线程已经在访问共享资源之后, 其他进程就不能进入访问. 互斥锁就能完成这样的功能.
在同一时间内, 最多只能有一个线程获得锁, 然后向后执行代码, 其他没有获得锁的线程, 就会在获取锁的地方举行阻塞, 直到锁被开释并获取到锁之后, 才会继续向后执行.
此时, 这些线程通过竞争这把锁, 它们之间就形成了互斥关系.
互斥锁的使用
1. 初始化互斥锁
初始化锁有两种方式: 静态初始化 和 动态初始化
- #include <pthread.h>
- // 静态初始化
- pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
- // 动态初始化
- pthread_mutex_t mutex;
- if (pthread_mutex_init(&mutex, NULL) != 0)
- {
- perror("pthread_mutex_init");
- }
复制代码 2. 加锁. 获得锁之后访问共享资源
使用函数 pthread_mutex_lock() 函数举行加锁
- pthread_mutex_lock(&mutex);
- // 访问共享资源
复制代码 如果获得了锁, 那么就会继续向后执行代码.
如果锁已经被其他线程获取, 那么线程就会被阻塞在这里, 直到锁被开释, 然后再一次竞争锁.
3. 开释锁
开释锁须要使用函数 pthread_mutex_unlock() 函数来解锁.
- pthread_mutex_unlock(&mutex);
复制代码 调用这个函数后, 线程就开释了获取的锁, 如果想要再次获得锁, 就须要和全部的线程去竞争
4. 烧毁锁
当不须要使用锁后, 可以使用函数 pthread_mutex_destroy() 函数烧毁, 开释资源.
- if (pthread_mutex_destroy(&mutex) != 0)
- {
- perror("pthread_mutex_destroy");
- }
复制代码 如果是通过静态初始化得到的锁, 不须要烧毁. 通过动态初始化得到的锁才须要使用这个函数举行烧毁.
那么现在就能对上面的代码举行优化了:
- #include<pthread.h>#include<stdio.h>pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;int sum = 100000000;void* func1(void* arg){ int x = 0; pthread_mutex_lock(&mutex); while(sum > 0) { --sum; } pthread_mutex_unlock(&mutex); return NULL;}int main(){ pthread_t t1; pthread_create(&t1, NULL, func1, NULL); pthread_t t2; pthread_create(&t2, NULL, func1, NULL); pthread_t t3; pthread_create(&t3, NULL, func1, NULL); pthread_join(t1, NULL); pthread_join(t2, NULL); pthread_join(t3, NULL); printf("sum: %d\n", sum); return 0;}
复制代码
当参加了互斥锁之后, 对于共享资源的访问就变得更加安全可控了.
线程同步
上面我们使用互斥锁让线程对于共享资源的访问变得更加安全可控.
在上面的阐明中也了解到了, 线程同步就是让线程按肯定的顺序来运行.
如: 一个线程先生产数据, 当数据生产完成后, 由另一个线程来举行处理.
那么线程生产了数据后, 就要放入共享资源中, 这样才能被其他的线程访问并获取.
这样就须要使用锁, 线程不断地访问共享资源, 检测数据是否被生产出来, 这就须要不断地加锁解锁.由于就近原则, 这个消耗线程开释锁后, 离锁更近, 就有更大的可能性获得锁. 这样下去就有可能导致生产线程恒久无法获得锁.
在上面的例子中, 我们须要让生产线程先运行生产数据, 当数据产生后, 再让消耗线程开始运行.
这里可以引入: 条件变量 (Condition Variable). 条件变量通常和互斥锁一起使用
1. 初始化条件变量
同样也分为两种: 静态初始化 和 动态初始化.
- pthread_cond_t cond; // 定义条件变量
- // 1. 静态初始化
- cond = PTHREAD_COND_INITIALIZER;
- // 2. 动态初始化
- pthread_cond_init(&cond, NULL)
复制代码 2. 等候条件变量
调用函数 pthread_cond_wait() 来举行等候.
- pthread_mutex_lock(&mutex); // 加锁while(!condition) // 当条件不符合时, 调用 pthread_cond_wait() 函数举行等候{ pthread_cond_wait(&cond, &mutex); // 等候条件变量, 此时线程就在这里举行了等候} // 直到被唤醒, 然后线程才会向下继续向下执行pthread_mutex_unlock(&mutex);// 在调用 pthread_cond_wait() 举行等候后, mutex 锁会被开释, 后续可以被其他线程竞争锁// 当线程被唤醒后, 线程会重新去竞争锁, 当竞争成功后, 才会继续执行, 否则照旧会被阻塞
复制代码 3. 通知条件变量
有两种方法通知正在等候的线程
1. pthread_cond_signal() 函数: 唤醒正在等候的某一个线程
2. pthread_cond_broadcast() 函数: 唤醒全部在等候的线程
- pthread_mutex_lock(&mutex); // 加锁, 并不会阻塞在这里, 因为上面的 wait 等将 mutex 开释condition = 1;pthread_cond_signal(&cond); // 通知单个线程, 然后本线程继续向后执行// 大概使用函数 pthread_cond_broadcast(&cond); 通知全部线程, 然后继续向后执行pthread_mutex_unlock(&mutex); // 解锁
复制代码 4. 烧毁条件变量
调用函数 pthread_cond_destroy() 举行烧毁.
- pthread_cond_destroy(&cond)
复制代码 下面实现一个例子:
- #include<pthread.h>#include<stdio.h>#include<unistd.h>pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;pthread_cond_t cond = PTHREAD_COND_INITIALIZER;int sum = 0;void* func1(void* arg){ pthread_mutex_lock(&mutex); if(sum <= 0) { printf("线程1举行等候\n"); pthread_cond_wait(&cond, &mutex); } printf("线程1被唤醒\n"); pthread_mutex_unlock(&mutex); return NULL;}void* func2(void* arg){ sleep(5); pthread_mutex_lock(&mutex); ++sum; pthread_cond_signal(&cond); pthread_mutex_unlock(&mutex); return NULL;}int main(){ pthread_t t1; pthread_create(&t1, NULL, func1, NULL); pthread_t t2; pthread_create(&t2, NULL, func2, NULL); pthread_join(t1, NULL); pthread_join(t2, NULL); pthread_mutex_destroy(&mutex); pthread_cond_destroy(&cond); return 0;}
复制代码 运行这段代码, 能观察到屏幕上会先打印出 "线程1举行等候", 当过了5秒之后线程2唤醒线程1, 然后线程1开始执行打印 "线程1被唤醒".
死锁概念
死锁是指多个线程在执行过程中, 因为资源抢夺造成的一种僵局.
发生死锁的四个须要条件:
- 互斥哀求: 资源在某段时间内只能有一个线程访问
- 哀求与保持: 一个线程已经获得了最少一个资源, 并且还在哀求其他资源, 但是其他资源已经被其他线程持有. 自己不会开释已经获得的资源, 而是不停哀求.
- 不可抢占: 线程与线程之间不可逼迫抢夺资源. 一个线程已经获得了资源, 其他线程不能直接抢夺.
- 循环等候: 线程都分别获得一些资源, 但是还须要其他线程已获取的资源. 其他线程同理, 也须要其他线程已获取的资源. 线程之间形成循环等候资源的场景.
想要粉碎死锁, 那么就只要粉碎上面的任意一个条件即可.
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |