提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
- 前言
- 一、为什么需要线程同步?
- 二、如何实现线程同步
- 一.互斥锁
- 1.互斥锁初始化
- 3.互斥锁加锁和解锁
- 4.pthread_mutex_trylock()函数
- 二.自旋锁(这个不是很了解,以后再补)
- 三.读写锁
- 1. 读写锁的两个规则:
- 2.读写锁基本使用
- 1.读写锁初始化
- 2.读写锁上锁解锁
- 四.条件变量
- 总结
前言
对于一个单线程进程来说,它不需要处理线程同步的问题,所以线程同步是在多线程环境下可能需要注意的一个问题。线程的主要优势在于,资源的共享性,譬如通过全局变量来实现信息共享,不过这种便捷的共享是有代价的,那就是多个线程并发访问共享数据所导致的数据不一致的问题。
提示:以下为本人学习记录,仅供参考
一、为什么需要线程同步?
线程同步是为了对共享资源的访问进行保护,保护的目的是为了解决数据一致性的问题,出现数据一致性问题其本质在于进程中的多个线程对共享资源的并发访问(同时访问)。举一个例子线程A读取一个变量的值,然后再给这个变量赋一个值,在线程A赋值完成之前,线程B也读取这个变量的值,这时就会导致数据不一致的问题。
代码示例:
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>
static int g_count = 0;
static void *td_fn(void *arg)
{
int loops = *((int *)arg);
int l_count, j;
for (j = 0; j < loops; j++) {
l_count = g_count;
l_count++;
g_count = l_count;
}
return (void *)0;
}
static int loops;
int main(int argc, char *argv[])
{
pthread_t tid1, tid2;
int ret;
if(2>argc)
loops=10000000;
//loops=1000;
else
loops=atoi(argv[1]);
pthread_create(&tid1, NULL, td_fn, &loops);
pthread_create(&tid2, NULL, td_fn, &loops);
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
printf("g_count = %d\n", g_count);
exit(0);
}
运行结果如下
由结果可知,理论上结果应该为20000000,但当loops较大时就会出现线程读取数据错误的现象
二、如何实现线程同步
一.互斥锁
互斥锁也叫互斥量,从本质上说是一把锁,在访问共享资源之前对互斥锁进行上锁,在访问完成后释放互斥锁(解锁),既然上了锁,任何其他试图访问这个共享资源的线程都不能够再访问这个共享文件,直到这个锁被解锁,但是依旧有一个问题就是如果释放互斥锁时有一个以上的线程阻塞,那么这些阻塞的线程会被唤醒,它们都会尝试对互斥锁进行加锁,当有一个线程成功对互斥锁上锁之后,其它线程就不能再次上锁了,只能再次陷入阻塞,等待下一次解锁。
代码如下(示例):
1.互斥锁初始化
互斥锁有两种初始化方式:一种是PTHREAD_MUTEX_INITIALIZER 宏初始化互斥锁(这个我没有用过就不说了),另一种是pthread_mutex_init()函数初始化互斥锁。
使用 pthread_mutex_init()函数对互斥锁进行初始化,其函数原型如下所示:
#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
下面是两种初始化示例
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, NULL);
//或者
pthread_mutex_t *mutex = malloc(sizeof(pthread_mutex_t));
pthread_mutex_init(mutex, NULL);
3.互斥锁加锁和解锁
互斥锁初始化之后,处于一个未锁定状态,调用函数 pthread_mutex_lock()可以对互斥锁加锁、获取互
斥锁,而调用函数 pthread_mutex_unlock()可以对互斥锁解锁、释放互斥锁。
#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
4.pthread_mutex_trylock()函数
trylock顾名思义尝试解锁,如果互斥锁处于未锁住状态,调用这个函数就能加锁成功,如果互斥锁已经被其它线程锁住那就会加锁失败,但是并不会阻塞,只会返回一个错误码EBUSY。
二.自旋锁(这个不是很了解,以后再补)
三.读写锁
相比于以上两种锁,读写锁有一种说法是读写锁也叫做共享互斥锁,读写锁相比以上两种锁而言,它比互斥锁具有更高的并发性,并且读写锁具有三种状态读模式下的加锁状态、写模式下的加锁状态和不加锁状态,一次只有一个线程可以占有写模式的读写锁,但是可以有多个线程同时占有读模式的读写锁。
1. 读写锁的两个规则:
⚫ 当读写锁处于写加锁状态时,在这个锁被解锁之前,所有试图对这个锁进行加锁操作(不管是以读
模式加锁还是以写模式加锁)的线程都会被阻塞。
⚫ 当读写锁处于读加锁状态时,所有试图以读模式对它进行加锁的线程都可以加锁成功;但是任何以
写模式对它进行加锁的线程都会被阻塞,直到所有持有读模式锁的线程释放它们的锁为止。
简单来说就是写加锁状态时,所有对这个锁的加锁操作都会阻塞,读加锁状态时,可以再进行读加锁,但是写加锁会被阻塞,直到所有读模式锁被解锁。
2.读写锁基本使用
1.读写锁初始化
使用 pthread_rwlock_init()函数对其进行初始化,当读写锁不再使用时,需要使用pthread_rwlock_destroy()函数将其销毁,其函数原型如下所示:
#include <pthread.h>
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
int pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr);
当读写锁不再使用时,需要调用 pthread_rwlock_destroy()函数将其销毁。
pthread_rwlock_t rwlock;
pthread_rwlock_init(&rwlock, NULL);
......
pthread_rwlock_destroy(&rwlock);
2.读写锁上锁解锁
读模式上锁: pthread_rwlock_rdlock()
写模式上锁:pthread_rwlock_wrlock()
解锁:pthread_rwlock_unlock() (无论你是什么模式的读写锁都可以用这个函数解锁)
函数原型如下
#include <pthread.h>
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
简单使用示例
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>
static pthread_rwlock_t rwlock;//定义读写锁
static int g_count = 0;
static void *read_thread(void *arg)
{
int number = *((int *)arg);
int j;
for (j = 0; j < 10; j++) {
pthread_rwlock_rdlock(&rwlock); //以读模式获取锁
printf("读线程<%d>, g_count=%d\n", number+1, g_count);
pthread_rwlock_unlock(&rwlock);//解锁
sleep(1);
}
return (void *)0;
}
static void *write_thread(void *arg)
{
int number = *((int *)arg);
int j;
for (j = 0; j < 10; j++) {
pthread_rwlock_wrlock(&rwlock); //以写模式获取锁
printf("写线程<%d>, g_count=%d\n", number+1, g_count+=20);
pthread_rwlock_unlock(&rwlock);//解锁
sleep(1);
}
return (void *)0;
}
static int nums[5] = {0, 1, 2, 3, 4};
int main(int argc, char *argv[])
{
pthread_t tid[10];
int j;
/* 对读写锁进行初始化 */
pthread_rwlock_init(&rwlock, NULL);
/* 创建 5 个读 g_count 变量的线程 */
for (j = 0; j < 5; j++)
pthread_create(&tid[j], NULL, read_thread, &nums[j]);
/* 创建 5 个写 g_count 变量的线程 */
for (j = 0; j < 5; j++)
pthread_create(&tid[j+5], NULL, write_thread, &nums[j]);
/* 等待线程结束 */
for (j = 0; j < 10; j++)
pthread_join(tid[j], NULL);//回收线程
/* 销毁自旋锁 */
pthread_rwlock_destroy(&rwlock);
exit(0);
}
使用读写锁来实现线程同步,全局变量 g_count 作为线程间的共享变量,主线程中创建了 5 个读取 g_count 变量的线程,它们使用同一个函数 read_thread,这 5 个线程仅仅对 g_count 变量进行读取,并将其打印出来,连带打印线程的编号(1~5);主线程中还创建了 5 个写 g_count 变量的线程,它们使用同一个函数 write_thread,write_thread 函数中会将 g_count 变量的值进行累加,循环 10 次,每
次将 g_count 变量的值在原来的基础上增加 20,并将其打印出来,连带打印线程的编号(1~5)。
运行结果如下
四.条件变量
条件变量通常是和互斥锁一起使用,所以函数int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);中参数 mutex 指向一个互斥锁对象,在初始化条件变量时也需要初始化一个互斥锁,在调用这个函数之前互斥锁需要加锁,当调用了这个函数后并且条件满足时互斥锁就会重新加锁,(这两步是同时完成的,即为原子操作),那什么时候解锁的呢,其实就是在调用这个函数时就解锁了,看图比较好理解。
总结
以上只是线程同步的基本知识,实际上更多的内容没有写,比如锁的属性,死锁等等,先初步了解,以后再慢慢补。