同步和互斥
互斥:是指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的。
同步:是指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问。。
互斥锁机制
互斥锁是用一种简单的加锁方法来控制对共享资源的访问,互斥锁有两种状态,上锁和解锁,可以把互斥锁看作成某种意义上的全局变量。在同一时刻只能有一个线程掌握这个互斥锁,拥有上锁状态的线程能够对共享资源按照顺序进行操作。若其他线程希望上锁一个已经被上锁的互斥锁,则该线程会被挂起,直到互斥锁被上锁的线程释放(解锁)。
互斥锁的基本操作
1.初始化互斥锁
一般由主线程来初始化一个互斥锁,有动态和静态两种初始化方式
具体实现:
静态:pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
动态: pthread_mutex_init 函数
- 申请互斥锁(上锁)
在访问共享资源前,首先申请互斥锁,如果互斥锁处于开锁状态,则就可以申请到,并立即占有这个该互斥锁。以防止其他线程访问该资源。
具体实现: pthread_mutex_lock 函数
- 释放互斥锁 (解锁)
具体实现: pthread_mutex_unlock 函数
4.销毁互斥锁
具体实现 pthread_mutex_destroy函数
代码示例1
c
#include <stdio.h>
#include "header.h"
/**
* 通过主线程给子线程传递数据在多个任务函数间共享互斥锁变量
* 效果:set的值要和get的值对应
*/
void my_perror(char *msg, int errnum)
{
if (errnum <= 0)
{
return;
}
char *err_str = strerror(errnum);
// 格式化拼接
char errMsg[50] = {0};
snprintf(errMsg, 50, "%s: %s\n", msg, err_str);
puts(errMsg);
}
//定义多个线程共享的资源
int shared_source = -1;
//线程1执行的任务函数
void* task1(void* arg)
{
//获取形式参数传递的数据
pthread_mutex_t* mutext_p = (pthread_mutex_t*)arg;
printf("task1: mutext: %p\n", mutext_p);
//设置共享资源
for(int i = 0; i<6; i++)
{
usleep(10000);
//2.加锁---lock
pthread_mutex_lock(mutext_p);
shared_source = rand() % 3;
//打印随机数---共享资源的值
printf("task1 ++++ set: %d\n", shared_source);
//3.解锁---unlock
pthread_mutex_unlock(mutext_p);
}
return NULL;
}
//线程2执行的任务函数
void* task2(void* arg)
{
//获取形式参数传递的数据
pthread_mutex_t* mutext_p = (pthread_mutex_t*)arg;
printf("task2: mutext: %p\n", mutext_p);
//获取共享资源
for(int i = 0; i<6; i++)
{
usleep(10000);
//2.加锁---lock
pthread_mutex_lock(mutext_p);
//打印随机数---共享资源的值
printf("task2 -------- get: %d\n", shared_source);
//3.解锁---unlock
pthread_mutex_unlock(mutext_p);
}
return NULL;
}
int main(int argc, char const *argv[])
{
//0.全局变量共享互斥锁
pthread_mutex_t mutex;
//1.给互斥锁变量初始化
pthread_mutex_init(&mutex, NULL);
printf("main: mutext: %p\n", &mutex);
//设置随机数种子
srand(time(NULL));
//创建两个线程
pthread_t t1, t2;
int r1 = pthread_create(&t1, NULL, task1, &mutex);
int r2 = pthread_create(&t2, NULL, task2, &mutex);
if (r1 != 0 || r2 != 0)
{
my_perror("t1 failed", r1);
my_perror("t2 failed", r2);
}
//等待线程的结束
r1 = pthread_join(t1, NULL);
r2 = pthread_join(t2, NULL);
if (r1 != 0 || r2 != 0)
{
my_perror("t1 failed", r1);
my_perror("t2 failed", r2);
}
//4.销毁互斥锁
pthread_mutex_destroy(&mutex);
return 0;
}
该代码实现了通过主线程给子线程传递数据,并在多个任务函数间共享互斥锁变量的功能。
首先,在全局定义了一个int类型的共享资源变量shared_source和一个pthread_mutex_t类型的互斥锁变量mutex。
然后,定义了两个任务函数task1和task2,这两个函数分别在循环中对共享资源进行设置和获取操作。在对共享资源进行操作之前,通过pthread_mutex_lock函数进行加锁,操作完成后通过pthread_mutex_unlock函数进行解锁,保证了在一个任务函数中对共享资源的操作期间,其他任务函数无法对其进行操作,实现了互斥。
在main函数中,首先通过pthread_mutex_init函数对互斥锁变量mutex进行初始化,然后通过srand函数设置随机数种子。
接下来,创建了两个线程t1和t2,分别调用了task1和task2作为线程函数。
最后,通过pthread_join函数等待两个线程的结束,并通过pthread_mutex_destroy函数销毁了互斥锁。
总结:通过互斥锁的加锁和解锁操作,实现了对共享资源的安全访问,保证了共享资源的一致性。
代码示例2
c
#include <stdio.h>
#include "header.h"
/*
*/
//定义全局变量:模拟多个线程共享数据---总票数
int ticket = 100;
//全局位置定义互斥锁变量
pthread_mutex_t mutex;
//定义任务函数
void* task1(void* arg)
{
while (1)
{
//2.加锁
pthread_mutex_lock(&mutex);
if (ticket > 0)
{
//t1, t2, t3, t4
usleep(10000);
printf("%s sell ticket num = %d\n", (char*)arg, ticket);
//票卖出去后,总票数减去1
ticket--;
}
else
{
//3.解锁----当函数执行结束的出口要注意解除互斥锁:互斥锁必须要解锁(unlock),否则会一直锁住
pthread_mutex_unlock(&mutex);
//当总票数等于0时,停止卖票
break;
}
//3.解锁
pthread_mutex_unlock(&mutex);
}
}
int main(int argc, char const *argv[])
{
//1.初始化互斥锁变量
pthread_mutex_init(&mutex, NULL);
//创建四个线程用于模拟卖票的4个窗口
pthread_t t1, t2, t3, t4;
//创建线程
pthread_create(&t1, NULL, task1, "thread-1");
pthread_create(&t2, NULL, task1, "thread-2");
pthread_create(&t3, NULL, task1, "thread-3");
pthread_create(&t4, NULL, task1, "thread-4");
//等待线程结束
pthread_join(t1, NULL);
pthread_join(t2, NULL);
pthread_join(t3, NULL);
pthread_join(t4, NULL);
//4.销毁互斥锁
pthread_mutex_destroy(&mutex);
return 0;
}
该代码是一个模拟多个线程卖票的程序。程序中定义了一个全局变量ticket表示总票数,并使用互斥锁mutex来保护对ticket的访问。
在主函数中,首先初始化互斥锁变量mutex,然后创建了四个线程t1、t2、t3、t4,并指定这四个线程的任务函数为task1,参数分别为"thread-1"、"thread-2"、"thread-3"、"thread-4"。接着使用pthread_join函数等待线程结束。最后,在程序结束之前销毁互斥锁mutex。
任务函数task1中使用while循环来模拟一直卖票的过程。在循环中,首先加锁pthread_mutex_lock(&mutex),然后判断ticket的值。如果ticket大于0,说明还有票可以卖出,此时打印出卖票信息,然后将ticket减1。如果ticket等于0,说明票已经卖完,此时跳出循环。最后,解锁pthread_mutex_unlock(&mutex)。
这样,通过互斥锁的加锁和解锁,可以保证同一时刻只有一个线程能够卖票,从而避免出现多个线程同时访问ticket导致的竞争问题。
互斥锁相关函数
读写锁(rwlock)机制
我们对共享资源的访问,往往存在两种方式,向共享资源写入数据,或从共享资源中获取数据,写操作对数据来说往往是不安全的,而读
操作是安全的。 如果线程间采用所有操作都独占的方式,势必会引起资源访问的效率问题。
读写锁与互斥量类似可以实现线程对临界资源的互斥访问,但和互斥量不同的是:互斥量对临界资源的访问是独占的方式;然而读写锁会视对临界资源的具体访问方式来决定是独占还是共享的方式来访问。
相对互斥量只有加锁和不加锁两种状态,读写锁有三种状态:读模式下的加锁,写模式下的加锁,不加锁。
读写锁的使用规则:
● 只要没有写模式下的加锁,任意线程都可以进行读模式下的加锁;
● 只有读写锁处于不加锁状态时,才能进行写模式下的加锁;
读写锁也称为共享-独占(shared-exclusive)锁,当读写锁以读模式加锁时,它是以共享模式锁住,当以写模式加锁时,它是以独占模式锁住。读写锁非常适合读数据的频率远大于写数据的频率的应用中。这样可以在任何时刻运行多个读线程并发的执行,给程序带来了更高的并发度。
读写锁的基本操作
-
初始化读写锁
静态:pthread_rwlock_t mutex = PTHREAD_RWLOCK_INITIALIZER;
动态: pthread_rwlock_init 函数
-
写模式上锁
在以写的方式访问共享资源前,首先申请写模式上锁锁,
具体实现: pthread_rwlock_wrlock 函数
-
读模式上锁
在以读的方式访问共享资源前,首先申请读模式上锁锁,
具体实现: pthread_rwlock_rdlock 函数
-
释放互斥锁 (解锁)
具体实现: pthread_rwlock_unlock 函数
-
销毁互斥锁
具体实现 pthread_rwlock_destroy函数
读写锁相关函数
代码示例
c
#include <stdio.h>
#include "header.h"
/**
* 读写过的效果和互斥锁的效果类似,但读写锁比互斥锁代码执行的效率高
*/
void my_perror(char *msg, int errnum)
{
if (errnum <= 0)
{
return;
}
char *err_str = strerror(errnum);
// 格式化拼接
char errMsg[50] = {0};
snprintf(errMsg, 50, "%s: %s\n", msg, err_str);
puts(errMsg);
}
//定义多个线程共享的资源
int shared_source = -1;
//定义读写锁变量
pthread_rwlock_t rwlock;
//线程1执行的任务函数----写操作
void* task1(void* arg)
{
//设置共享资源
for(int i = 0; i<6; i++)
{
usleep(1000);
//2.给写操作加写模式(互斥)的锁
pthread_rwlock_wrlock(&rwlock);
shared_source = rand() % 3;
//打印随机数---共享资源的值
printf("task1 ++++ set: %d\n", shared_source);
//3.解除写模式锁
pthread_rwlock_unlock(&rwlock);
}
return NULL;
}
//线程2执行的任务函数---读操作
void* task2(void* arg)
{
//获取共享资源
for(int i = 0; i<6; i++)
{
usleep(1000);
//2.给读操作加读模式(共享)的锁
pthread_rwlock_rdlock(&rwlock);
//打印随机数---共享资源的值
printf("task2 -------- get: %d\n", shared_source);
//3.解除读模式的锁
pthread_rwlock_unlock(&rwlock);
}
return NULL;
}
int main(int argc, char const *argv[])
{
//1.初始化读写锁
pthread_rwlock_init(&rwlock, NULL);
//设置随机数种子
srand(time(NULL));
//创建两个线程
pthread_t t1, t2;
int r1 = pthread_create(&t1, NULL, task1, NULL);
int r2 = pthread_create(&t2, NULL, task2, NULL);
if (r1 != 0 || r2 != 0)
{
my_perror("t1 failed", r1);
my_perror("t2 failed", r2);
}
//等待线程的结束
r1 = pthread_join(t1, NULL);
r2 = pthread_join(t2, NULL);
if (r1 != 0 || r2 != 0)
{
my_perror("t1 failed", r1);
my_perror("t2 failed", r2);
}
//4.销毁读写锁
pthread_rwlock_destroy(&rwlock);
return 0;
}
这段代码实现了读写锁的使用。主函数创建了两个线程,分别执行task1和task2函数。task1函数是写操作,任务是设置shared_source变量的值为随机数,并打印设置的值。task2函数是读操作,任务是读取shared_source的值并打印。
代码中使用了pthread_rwlock_t类型的变量rwlock来表示读写锁,并在主函数中调用pthread_rwlock_init函数进行初始化,调用pthread_rwlock_destroy函数销毁读写锁。
在task1函数中,写操作之前调用pthread_rwlock_wrlock函数给写操作加写模式(互斥)的锁,写操作完成后调用pthread_rwlock_unlock函数解除写模式锁。
在task2函数中,读操作之前调用pthread_rwlock_rdlock函数给读操作加读模式(共享)的锁,读操作完成后调用pthread_rwlock_unlock函数解除读模式锁。
需要注意的是,读写锁的特点是多个线程可以同时读共享资源,但只有一个线程能够写共享资源。这样可以提高读操作的效率。
至于代码中的其他部分,主要是一些错误处理和线程创建、等待的逻辑。
总结起来,这段代码展示了读写锁的使用示例,通过读写锁实现了多个线程对共享资源的读写操作,提高了读操作的效率。