线程同步
并非让线程并行,而是有先后的顺序执行,当有一个线程对内存操作时,其他线程不可以对这个内存地址操作
线程之间的分工合作
线程的优势之一:能够通过全局变量共享信息
临界区:访问某一共享资源的代码片段,此段代码应为原子操作
全局变量在共享内存中
一个进程中有多个线程,共享全局变量,这是一件既有用又有些危险的特性
需要线程同步来维护临界资源的安全性
下面的程序中两个线程堆全局变量number进行增加操作,因异步而发生错误
#include<unistd.h>
#include<string.h>
#include<pthread.h>
#include<stdio.h>
#include<stdlib.h>
#define MAX 50
int number;
void* funcA(void* arg){
for(int i = 0;i<MAX;i++){
int cur = number;
cur++;
usleep(10);
number = cur;
printf("thread A,id = %lu,number = %d\n",pthread_self(),number);
}
return NULL;
}
void* funcB(void* arg){
for(int i = 0;i<MAX;i++){
int cur = number;
cur++;
number = cur;
printf("thread B,id = %lu,number = %d\n",pthread_self(),number);
usleep(5);
}
return NULL;
}
int main()
{
pthread_t t1,t2;
pthread_create(&t1,NULL,funcA,NULL);
pthread_create(&t2,NULL,funcB,NULL);
pthread_join(t1,NULL);
pthread_join(t2,NULL);
return 0;
}
//结果多数情况并非100
由于物理内存,缓存,cpu的读写,A和B拿到的不总是最新的数据
A在失去时间片被挂起前没有将数据更新到物理内存,则B获得时间片后获取的数据为旧数据
使用线程同步,保证数据写回物理内存,其他线程才会有资格执行
同步的方法:
对共享资源加锁(一般为全局数据区变量或堆区)这类共享资源又叫临界资源
四种:互斥锁,读写锁,条件变量,信号量
互斥锁
互斥锁确保同时仅有一个线程可以访问某共享资源,实现对任意共享资源的原子访问
互斥锁锁定一个代码块,使得线程之间顺序处理(串行)
互斥锁状态:(1)locked (2)unlocked
任何时候至多一个线程可以锁定该互斥锁,只有锁的所有者才能解锁
//互斥锁
pthread_mutex_t mutex;
//锁对象中保存了锁的状态信息,若为锁定状态则还记录给这把锁加锁的线程id,一个互斥锁变量只能被一个线程锁定,一般一个共享资源对应一把互斥锁
//互斥锁操作:(1)初始化
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr);
//要想对mutex指针指向的内存进行操作,加restrict关键字来修饰指针,其他指针不能访问,unique,attr为互斥锁的属性
//(2)释放互斥锁资源
int pthread_mutex_destroy(pthread_mutex_t *mutex);
//(3)加锁解锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
//首先会判断参数 mutex 互斥锁中的状态是不是锁定状态,没上锁则加锁成功且记录线程,如果已锁则线程会阻塞在这把锁上
int pthread_mutex_trylock(pthread_mutex_t *mutex);
//如果已锁,则调用此函数加锁的线程不会被阻塞,失败直接返回错误号
int pthread_mutex_unlock(pthread_mutex_t *mutex);
//加锁解锁的应为同一线程
//修改之前的计数程序
#define MAX 50
int number;
pthread_mutex_t mutex;
void* funcA(void* arg){
for(int i = 0;i<MAX;i++){
//临界区在满足条件下越小越好
pthread_mutex_lock(&mutex);
int cur = number;
cur++;
usleep(10);
number = cur;
printf("thread A,id = %lu,number = %d\n",pthread_self(),number);
pthread_mutex_unlock(&mutex);
}
return NULL;
}
void* funcB(void* arg){
for(int i = 0;i<MAX;i++){
pthread_mutex_lock(&mutex);
int cur = number;
cur++;
number = cur;
printf("thread B,id = %lu,number = %d\n",pthread_self(),number);
pthread_mutex_unlock(&mutex);
}
return NULL;
}
int main()
{
pthread_t t1,t2;
pthread_mutex_init(&mutex,NULL);
pthread_create(&t1,NULL,funcA,NULL);
pthread_create(&t2,NULL,funcB,NULL);
pthread_join(t1,NULL);
pthread_join(t2,NULL);
pthread_mutex_destroy(&mutex);
return 0;
}
死锁
使用互斥锁可能会带来死锁
死锁使一个或多个线程被挂起(阻塞),无法继续执行
产生死锁的几种情况:
(1)在一个线程中对一个已经加锁的普通锁再次加锁
lock();
lock();//上面已经加了锁,这里阻塞,所以也无法释放锁
(2)多个线程按着不同顺序来申请多个互斥锁,容易产生死锁
(3)忘记释放锁
实例:
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<pthread.h>
int a = 0;
int b = 0;
pthread_mutex_t mutex_a;
pthread_mutex_t mutex_b;
void * thread(void* arg){
pthread_mutex_lock(&mutex_a);
printf("child thread,got mutex_a,waiting for mutex_b\n");
sleep(5);//这里休眠5秒目的是让main thread可以获得锁
++a;
pthread_mutex_lock(&mutex_b);
a+=b++;
pthread_mutex_unlock(&mutex_b);
pthread_mutex_unlock(&mutex_a);
pthread_exit(NULL);
}
int main()
{
//main thread
pthread_t id;
pthread_mutex_init(&mutex_a,NULL);
pthread_mutex_init(&mutex_b,NULL);
pthread_create(&id,NULL,thread,NULL);
pthread_mutex_lock(&mutex_b);
printf("main thread,got mutex_b,waiting for mutex_a\n");
sleep(5);//和子线程中的sleep同理
++b;
pthread_mutex_lock(&mutex_a);
b+=a++;
pthread_mutex_unlock(&mutex_b);
pthread_mutex_unlock(&mutex_a);
pthread_join(id,NULL);
pthread_mutex_destroy(&mutex_a);
pthread_mutex_destroy(&mutex_b);
return 0;
}
线程A锁住了资源1,现在想去给资源2加锁,而这时线程B锁住了资源2,想去给资源1加锁
造成循环等待,死锁
如图所示,蚌住了
- 加锁使用trylock
- 顺序访问共享资源
- 引入死锁检测方案(第三方库)
- 检查代码避免多次加锁
读者/写者问题,读写锁
读写锁可以提高读写操作的效率,如果使用互斥锁则读操作之间是串行的,用读写锁的读操作是并行的
特点:读写互斥,写优先级更高,读读并行,写写互斥
多个线程有读有写,且读操作较多,使用读写锁才更优秀
pthread_rwlock_t rwlock;//会记录锁的状态,锁定的是读还是写,哪个线程将此锁锁定
// 初始化读写锁
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
const pthread_rwlockattr_t *restrict attr);
// 释放读写锁
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
//成功返回0,失败返回错误号
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
//此函数锁定读操作若读写锁已锁定读,依然可以用此函数加锁但是锁定了写再调用就会阻塞
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
//锁定写操作,类似互斥锁
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
// 失败不会阻塞当前线程, 直接返回错误号
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
//解锁:
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
示例:
#include<unistd.h>
#include<string.h>
#include<pthread.h>
#include<stdio.h>
#include<stdlib.h>
#define MAX 50
int number;
pthread_rwlock_t rwlock;
void* read_func(void* arg){
for(int i = 0;i<MAX;i++){
//临界区在满足条件下越小越好
pthread_rwlock_rdlock(&rwlock);
//读操作
printf("thread A,id = %lu,number = %d\n",pthread_self(),number);
pthread_rwlock_unlock(&rwlock);
usleep(rand()%5);
}
return NULL;
}
void* write_func(void* arg){
for(int i = 0;i<MAX;i++){
pthread_rwlock_wrlock(&rwlock);
int cur = number;
cur++;
number = cur;
printf("thread B,id = %lu,number = %d\n",pthread_self(),number);
pthread_rwlock_unlock(&rwlock);
usleep(5);
}
return NULL;
}
int main()
{
pthread_t t_read[5],t_write[3];
pthread_rwlock_init(&rwlock,NULL);
for(int i = 0;i<5;i++){
pthread_create(&t_read[i],NULL,read_func,NULL);
}
for(int i = 0;i<3;i++){
pthread_create(&t_write[i],NULL,write_func,NULL);
}
for(int i = 0;i<5;i++){
pthread_join(t_read[i],NULL);
}
for(int i = 0;i<3;i++){
pthread_join(t_write[i],NULL);
}
pthread_rwlock_destroy(&rwlock);
return 0;
}
增加结果有序且正确
条件变量
有些情况不能实现线程同步,和互斥锁配合使用
条件变量只有在满足指定的条件下才会阻塞线程,不满足条件则多个线程可以同时进入临界区
pthread_cond_t cond;
//阻塞多个线程,要存储多个线程的id
// 初始化
int pthread_cond_init(pthread_cond_t *restrict cond,
const pthread_condattr_t *restrict attr);
// 销毁
int pthread_cond_destroy(pthread_cond_t *cond);
//线程阻塞
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
//互斥锁用于同步,条件变量用于阻塞
// 将线程阻塞一定的时间长度, 时间到达后解除阻塞
//在阻塞线程时候,如果线程已经对互斥锁 mutex 上锁,那么会将这把锁打开,这样做是为了避免死锁
//当线程解除阻塞的时候,函数内部会帮助这个线程再次将这个 mutex 互斥锁锁上,继续向下访问临界区
int pthread_cond_timedwait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);
//秒和纳秒
struct timespec {
time_t tv_sec;
long tv_nsec;
};
//体中记录的时间是从1970.1.1到某个时间点的时间
time_t time(time_t * timer)
//函数返回从TC1970-1-1 0:0:0开始到现在的秒数
从现在开始到将来xx秒:
struct timespec tm;
tm.tv_nsec = 0;
tm.tv_sec = time(NULL) + 100;
// 唤醒阻塞在条件变量上的线程, 至少有一个被解除阻塞,个数不定
int pthread_cond_signal(pthread_cond_t *cond);
// 唤醒阻塞在条件变量上的线程, 被阻塞的线程全部解除阻塞
int pthread_cond_broadcast(pthread_cond_t *cond);
生产者消费者问题
系统中有一组生产者进程和一组消费者进程,生产者进程每次生产一个产品放入缓冲区,消费者进程每次从缓冲区中取出一个产品并使用。
生产者、消费者共享一个初始为空、大小为n的缓冲区。
只有缓冲区没满时,生产者才能吧产品放入缓冲区,否则必须等待。
只有缓冲区不为空时,消费者才能从中取出产品,否则必须等待。
缓冲区是临界资源,各进程必须互斥地访问。
生产者消费者协作,互相唤醒
#include<iostream>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
#include<pthread.h>
#include<fcntl.h>
#include<list>
#include<forward_list>
using namespace std;
struct Node{
int num;
};
forward_list<Node>linkedlist;
void* produ(void* arg){
while(1){
sleep(1);
Node newnode{rand()%1000};
linkedlist.push_front(newnode);
printf("add node,number = %d,tid:%ld\n",newnode.num,pthread_self());
}
}
void* customer(void* arg){
while(1){
sleep(1);
Node newnode = linkedlist.front();
linkedlist.pop_front();
printf("del node,number = %d,tid:%ld\n",newnode.num,pthread_self());
}
}
int main()
{
pthread_t pro[5],cus[5];
for(int i = 0;i<5;i++){
pthread_create(&pro[i],nullptr,produ,nullptr);
pthread_create(&cus[i],nullptr,customer,nullptr);
}
for(int i = 0;i<5;i++){
pthread_join(pro[i],nullptr);
pthread_join(cus[i],nullptr);
}
while(1){
}
return 0;
}
加锁:
多个线程向链表添加节点应互斥进行,取走节点也应互斥进行
所以应对生产者临界区,消费者临界区加锁
链表的生产没有上限,但是消费有,所以若为空则阻塞消费者线程
生产者添加了节点则通知消费者解除阻塞
pthread_mutex_t mutex;
pthread_cond_t cond;
struct Node{
int num;
struct Node* next;
};
struct Node* head;//头节点
void* func_producer(void* arg){//添加节点
while(1){
pthread_mutex_lock(&mutex);
struct Node* newnode = (struct Node*)malloc(sizeof(struct Node));
newnode->num = rand()%1000;
newnode->next = head;
head = newnode;
printf("生产者,id:%lu, number:%d\n",pthread_self(),newnode->num);
pthread_mutex_unlock(&mutex);
pthread_cond_broadcast(&cond);
sleep(rand()%3);
}
return NULL;
}
void* func_consumer(void* arg){//添加节点
while(1){
pthread_mutex_lock(&mutex);
while(head == NULL){//链表为空
//阻塞消费者
pthread_cond_wait(&cond,&mutex);
}
struct Node* node = head;
printf("生产者,id:%lu, number:%d\n",pthread_self(),node->num);
head = head->next;
free(node);
pthread_mutex_unlock(&mutex);
sleep(rand()%3);
}
return NULL;
}
int main()
{
pthread_mutex_init(&mutex,NULL);
pthread_cond_init(&cond,NULL);
pthread_t t1[5],t2[5];
for(int i = 0;i<5;i++){
pthread_create(&t1[i],NULL,func_producer,NULL);
}
for(int i = 0;i<5;i++){
pthread_create(&t2[i],NULL,func_consumer,NULL);
}
for(int i = 0;i<5;i++){
pthread_join(t1[i],NULL);
}
for(int i = 0;i<5;i++){
pthread_join(t2[i],NULL);
}
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cond);
}
pthread_cond_wait在函数内部会自动的把互斥锁打开,其他线程则可以抢到这把互斥锁,生产者才能生产,生产1个后唤醒多个消费者,消费者又执行while(head==NULL)pthread_cond_wait
若唤醒多个阻塞在wait的线程,则多个线程会抢占这把互斥锁,没抢到的仍然阻塞
标签:复健,int,cond,互斥,Unix,线程,pthread,mutex,多线程 From: https://www.cnblogs.com/ziggystardust-pop/p/17067239.html