目录
2 生产者和消费者(通常条件变量都是配合生产者和消费者模型进行工作)
线程同步的概念
假设有 4 个线程 A、B、C、D,当前一个线程 A 对内存中的共享资源进行访问的时候,其他线程 B, C, D 都不可以对这块内存进行操作,直到线程 A 对这块内存访问完毕为止,B,C,D 中的一个才能访问这块内存,剩余的两个需要继续阻塞等待,以此类推,直至所有的线程都对这块内存操作完毕。 线程对内存的这种访问方式就称之为线程同步,通过对概念的介绍,我们可以了解到所谓的同步并不是多个线程同时对内存进行访问,而是按照先后顺序依次进行的。
-
同步方式
对于多个线程访问共享资源出现数据混乱的问题,需要进行线程同步。常用的线程同步方式有四种:互斥锁、读写锁、条件变量、信号量。所谓的共享资源就是多个线程共同访问的变量,这些变量通常为全局数据区变量或者堆区变量,这些变量对应的共享资源也被称之为临界资源。
下面会依次详细介绍这四种线程同步的方法。
一,互斥锁
互斥锁是线程同步最常用的一种方式,通过互斥锁可以锁定一个代码块,被锁定的这个代码块,所有的线程只能顺序执行 (不能并行处理),这样多线程访问共享资源数据混乱的问题就可以被解决了,需要付出的代价就是执行效率的降低,因为默认临界区多个线程是可以并行处理的,现在只能串行处理。
1.互斥锁函数
- 在 Linux 中互斥锁的类型为 pthread_mutex_t,创建一个这种类型的变量就得到了一把互斥锁:pthread_mutex_t mutex;
// 初始化互斥锁
// restrict: 是一个关键字, 用来修饰指针, 只有这个关键字修饰的指针可以访问指向的内存地址, 其他指针是不行的
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr);
// 释放互斥锁资源
int pthread_mutex_destroy(pthread_mutex_t *mutex);
- 参数
- mutex: 互斥锁变量的地址
- attr: 互斥锁的属性,一般使用默认属性即可,这个参数指定为 NULL
1.1加锁与解锁函数
// 修改互斥锁的状态, 将其设定为锁定状态, 这个状态被写入到参数 mutex 中
int pthread_mutex_lock(pthread_mutex_t *mutex);
// 修改互斥锁的状态, 将其设定为锁定状态, 这个状态被写入到参数 mutex 中
int pthread_mutex_lock(pthread_mutex_t *mutex);
2.互斥锁使用
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <pthread.h>
#define MAX 50
// 全局变量
int number;
//创建一把互斥锁
pthread_mutex_t mutex;
// 线程处理函数
void* funcA_num(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_num(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);
usleep(5);
}
return NULL;
}
int main(int argc, const char* argv[])
{
pthread_t p1, p2;
pthread_mutex_init(&mutex, NULL);
// 创建两个子线程
pthread_create(&p1, NULL, funcA_num, NULL);
pthread_create(&p2, NULL, funcB_num, NULL);
// 阻塞,资源回收
pthread_join(p1, NULL);
pthread_join(p2, NULL);
pthread_mutex_destroy(&mutex);
return 0;
}
二,读写锁
读写锁是互斥锁的升级版,在做读操作的时候可以提高程序的执行效率,如果所有的线程都是做读操作, 那么读是并行的,但是使用互斥锁,读操作也是串行的。
读写锁是一把锁,锁的类型为 pthread_rwlock_t,有了类型之后就可以创建一把互斥锁了:pthread_rwlock_t rwlock;
1.读写锁函数
#include <pthread.h>
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);
- 参数:
- rwlock: 读写锁的地址,传出参数
- attr: 读写锁属性,一般使用默认属性,指定为 NULL
1.1读写锁的加锁与解锁函数
// 在程序中对读写锁加读锁, 锁定的是读操作
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
调用这个函数,如果读写锁是打开的,那么加锁成功;如果读写锁已经锁定了读操作,调用这个函数依然可以加锁成功,因为读锁是共享的;如果读写锁已经锁定了写操作,调用这个函数的线程会被阻塞。// 在程序中对读写锁加写锁, 锁定的是写操作
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
调用这个函数,如果读写锁是打开的,那么加锁成功;如果读写锁已经锁定了读操作或者锁定了写操作,调用这个函数的线程会被阻塞。// 解锁, 不管锁定了读还是写都可用解锁
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
2.读写锁的使用
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <pthread.h>
#define MAX 50
// 全局变量
int number;
//创建一把 读写锁
pthread_rwlock_t rwlock;
// 线程处理函数
void* write_num(void* arg)
{
for(int i=0; i<MAX; ++i)
{
pthread_rwlock_wrlock(&rwlock);
int cur = number;
cur++;
usleep(10);
number = cur;
printf("Thread_write, id = %lu, number = %d\n", pthread_self(), number);
pthread_rwlock_unlock(&rwlock);
}
return NULL;
}
void* read_num(void* arg)
{
for(int i=0; i<MAX; ++i)
{
pthread_rwlock_rdlock(&rwlock);
printf("Thread_read, id = %lu, number = %d\n", pthread_self(), number);
pthread_rwlock_unlock(&rwlock);
usleep(rand()%5);
}
return NULL;
}
int main(int argc, const char* argv[])
{
pthread_t p1[3], p2[5];
pthread_rwlock_init(&rwlock, NULL);
//8 个线程操作同一个全局变量,3 个线程不定时写同一全局资源,5 个线程不定时读同一全局资源。
for(int i = 0; i < 3; i++)
{
pthread_create(&p1[i], NULL, write_num, NULL);
}
for(int i = 0; i < 5; i++)
{
pthread_create(&p2[i], NULL, read_num, NULL);
}
// 阻塞,资源回收
for(int i = 0; i < 3; i++)
{
pthread_join(p1[i], NULL);
}
for(int i = 0; i < 5; i++)
{
pthread_join(p2[i], NULL);
}
pthread_rwlock_destroy(&rwlock);
return 0;
}
三,条件变量
1 条件变量函数
严格意义上来说,条件变量的主要作用不是处理线程同步,而是进行线程的阻塞。如果在多线程程序中只使用条件变量无法实现线程的同步,必须要配合互斥锁来使用。虽然条件变量和互斥锁都能阻塞线程,但是二者的效果是不一样的,二者的区别如下:
假设有 A-Z 26 个线程,这 26 个线程共同访问同一把互斥锁,如果线程 A 加锁成功,那么其余 B-Z 线程访问互斥锁都阻塞,所有的线程只能顺序访问临界区
条件变量只有在满足指定条件下才会阻塞线程,如果条件不满足,多个线程可以同时进入临界区,同时读写临界资源,这种情况下还是会出现共享资源中数据的混乱。
一般情况下条件变量用于处理生产者和消费者模型,并且和互斥锁配合使用。条件变量类型对应的类型为 pthread_cond_t,这样就可以定义一个条件变量类型的变量了:pthread_cond_t cond;
条件变量操作函数函数原型如下:
#include <pthread.h>
pthread_cond_t cond;
// 初始化
int pthread_cond_init(pthread_cond_t *restrict cond,
const pthread_condattr_t *restrict attr);
// 销毁释放资源
int pthread_cond_destroy(pthread_cond_t *cond);
- 参数:
- cond: 条件变量的地址
- attr: 条件变量属性,一般使用默认属性,指定为 NULL
// 线程阻塞函数, 哪个线程调用这个函数, 哪个线程就会被阻塞
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
// 唤醒阻塞在条件变量上的线程, 至少有一个被解除阻塞
int pthread_cond_signal(pthread_cond_t *cond);
// 唤醒阻塞在条件变量上的线程, 被阻塞的线程全部解除阻塞
int pthread_cond_broadcast(pthread_cond_t *cond);
2 生产者和消费者(通常条件变量都是配合生产者和消费者模型进行工作)
/* 使用条件变量实现生产者和消费者模型,生产者有 5 个,往链表头部添加节点,消费者也有 5 个,删除链表头部的节点。*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>
pthread_cond_t cond; //定义条件变量, 控制消费者线程
pthread_mutex_t mutex; //互斥锁变量
//链表的节点
struct node{
int date;
struct node* next;
};
//指向头结点的指针
struct node* head = NULL;
void* producer(void* arg);
void* consumer(void* arg);
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, producer, NULL);
pthread_create(&t2[i], NULL, consumer, NULL);
}
for(int i = 0; i < 5; i++)
{
pthread_join(t1[i], NULL);
pthread_join(t2[i], NULL);
}
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cond);
return 0;
}
void* producer(void* arg)
{
while(1)
{
pthread_mutex_lock(&mutex);
//创建一个链表的新节点
struct node* newNode = (struct node*)malloc(sizeof(struct node));
// 节点初始化
newNode->date = rand()%100 + 1;
newNode->next = head;
head = newNode;
printf("生产者, ID: %ld, date: %d\n", pthread_self(), newNode->date);
pthread_mutex_unlock(&mutex);
// 生产了任务, 通知消费者消费
pthread_cond_broadcast(&cond);
sleep(rand()%3);
}
return NULL;
}
void* consumer(void* arg)
{
while(1)
{
pthread_mutex_lock(&mutex);
while(head == NULL)
{
pthread_cond_wait(&cond, &mutex);
}
struct node* head_node = head;
printf("消费者, ID: %ld, date: %d\n", pthread_self(), head_node->date);
head = head->next;
free(head_node);
pthread_mutex_unlock(&mutex);
sleep(rand()%3);
}
return NULL;
}
四,信号量
信号量用在多线程多任务同步的,一个线程完成了某一个动作就通过信号量告诉别的线程,别的线程再进行某些动作。信号量不一定是锁定某一个资源,而是流程上的概念,比如:有 A,B 两个线程,B 线程要等 A 线程完成某一任务以后再进行自己下面的步骤,这个任务并不一定是锁定某一资源,还可以是进行一些计算或者数据处理之类。
信号量(信号灯)与互斥锁和条件变量的主要不同在于” 灯” 的概念,灯亮则意味着资源可用,灯灭则意味着不可用。信号量主要阻塞线程,不能完全保证线程安全,如果要保证线程安全,需要信号量和互斥锁一起使用。
信号量和条件变量一样用于处理生产者和消费者模型,用于阻塞生产者线程或者消费者线程的运行。信号的类型为 sem_t 对应的头文件为 <semaphore.h>
1 信号量函数
#include <semaphore.h>
// 初始化信号量/信号灯
int sem_init(sem_t *sem, int pshared, unsigned int value);
// 资源释放, 线程销毁之后调用这个函数即可
// 参数 sem 就是 sem_init() 的第一个参数
int sem_destroy(sem_t *sem);
- 参数:
- sem:信号量变量地址
- pshared:
- 0:线程同步
- 非 0:进程同步
- value:初始化当前信号量拥有的资源数(>=0),如果资源数为 0,线程就会被阻塞了
// 参数 sem 就是 sem_init() 的第一个参数
// 函数被调用sem中的资源就会被消耗1个, 资源数-1
int sem_wait(sem_t *sem);
当线程调用这个函数,并且 sem 中的资源数 >0,线程不会阻塞,线程会占用 sem 中的一个资源,因此资源数 - 1,直到 sem 中的资源数减为 0 时,资源被耗尽,因此线程也就被阻塞了。
2.信号量使用
(1)总资源数为 1时
如果生产者和消费者线程使用的信号量对应的总资源数为 1,那么不管线程有多少个,可以工作的线程只有一个,其余线程由于拿不到资源,都被迫阻塞了,因此不使用互斥锁也可以实现线程同步。
/* 使用信号量实现生产者和消费者模型,生产者有 5 个,往链表头部添加节点,消费者也有 5 个,删除链表头部的节点。*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>
#include <semaphore.h> //信号量的头文件
//生产者线程信号量
sem_t psem;
//消费者线程信号量
sem_t csem;
// pthread_mutex_t mutex; //互斥锁变量
//链表的节点
struct node{
int date;
struct node* next;
};
//指向头结点的指针
struct node* head = NULL;
void* producer(void* arg);
void* consumer(void* arg);
int main()
{
// 初始化信号量
// 生产者和消费者拥有的资源的总和为1
sem_init(&psem, 0, 1); // 生成者线程一共有1个资源
sem_init(&csem, 0, 0); // 消费者线程一共有0个资源
pthread_t t1[5], t2[5];
for(int i = 0; i < 5; i++)
{
pthread_create(&t1[i], NULL, producer, NULL);
pthread_create(&t2[i], NULL, consumer, NULL);
}
for(int i = 0; i < 5; i++)
{
pthread_join(t1[i], NULL);
pthread_join(t2[i], NULL);
}
sem_destroy(&psem);
sem_destroy(&csem);
return 0;
}
void* producer(void* arg)
{
while(1)
{
// 生产者资源减一
sem_wait(&psem);
//创建一个链表的新节点
struct node* newNode = (struct node*)malloc(sizeof(struct node));
// 节点初始化
newNode->date = rand()%100 + 1;
newNode->next = head;
head = newNode;
printf("生产者, ID: %ld, date: %d\n", pthread_self(), newNode->date);
// 通知消费者消费, 给消费者的资源加一
sem_post(&csem);
sleep(rand()%3);
}
return NULL;
}
void* consumer(void* arg)
{
while(1)
{
//消费者的资源减一
sem_wait(&csem);
struct node* head_node = head;
printf("消费者, ID: %ld, date: %d\n", pthread_self(), head_node->date);
head = head->next;
free(head_node);
// 通知生产者生成, 给生产者的资源加一
sem_post(&psem);
sleep(rand()%3);
}
return NULL;
}
(2) 总资源数大于 1时
如果生产者和消费者线程使用的信号量对应的总资源数为大于 1,这种场景下出现的情况就比较多了:
- 多个生产者线程同时生产
- 多个消费者同时消费
- 生产者线程和消费者线程同时生产和消费
以上不管哪一种情况都可能会出现多个线程访问共享资源的情况,如果想防止共享资源出现数据混乱,那么就需要使用互斥锁进行线程同步,处理代码如下:
/* 使用信号量实现生产者和消费者模型,生产者有 5 个,往链表头部添加节点,消费者也有 5 个,删除链表头部的节点。*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>
#include <semaphore.h> //信号量的头文件
//生产者线程信号量
sem_t psem;
//消费者线程信号量
sem_t csem;
pthread_mutex_t mutex; //互斥锁变量
//链表的节点
struct node{
int date;
struct node* next;
};
//指向头结点的指针
struct node* head = NULL;
void* producer(void* arg);
void* consumer(void* arg);
int main()
{
// 初始化互斥锁
pthread_mutex_init(&mutex, NULL);
// 初始化信号量
// 生产者和消费者拥有的资源的总和为5
sem_init(&psem, 0, 5); // 生成者线程一共有5个资源
sem_init(&csem, 0, 0); // 消费者线程一共有0个资源
pthread_t t1[5], t2[5];
for(int i = 0; i < 5; i++)
{
pthread_create(&t1[i], NULL, producer, NULL);
pthread_create(&t2[i], NULL, consumer, NULL);
}
for(int i = 0; i < 5; i++)
{
pthread_join(t1[i], NULL);
pthread_join(t2[i], NULL);
}
pthread_mutex_destroy(&mutex);
sem_destroy(&psem);
sem_destroy(&csem);
return 0;
}
void* producer(void* arg)
{
while(1)
{
// 生产者资源减一
sem_wait(&psem);
// 加锁, 这句代码放到 sem_wait()上边, 有可能会造成死锁
pthread_mutex_lock(&mutex);
//创建一个链表的新节点
struct node* newNode = (struct node*)malloc(sizeof(struct node));
// 节点初始化
newNode->date = rand()%100 + 1;
newNode->next = head;
head = newNode;
printf("生产者, ID: %ld, date: %d\n", pthread_self(), newNode->date);
pthread_mutex_unlock(&mutex);
// 通知消费者消费, 给消费者的资源加一
sem_post(&csem);
sleep(rand()%3);
}
return NULL;
}
void* consumer(void* arg)
{
while(1)
{
//消费者的资源减一
sem_wait(&csem);
// 加锁, 这句代码放到 sem_wait()上边, 有可能会造成死锁
pthread_mutex_lock(&mutex);
struct node* head_node = head;
printf("消费者, ID: %ld, date: %d\n", pthread_self(), head_node->date);
head = head->next;
free(head_node);
pthread_mutex_unlock(&mutex);
// 通知生产者生成, 给生产者的资源加一
sem_post(&psem);
sleep(rand()%3);
}
return NULL;
}