文章目录
1.同步概念
所谓同步,即同时起步,协调一致。不同的对象,对“同步”的理解方式略有不同。
如,设备同步,是指在两个设备之间规定一个共同的时间参考;
数据库同步,是指让两个或多个数据库内容保持一致,或者按需要部分保持一致;
文件同步,是指让两个或多个文件夹里的文件保持一致。等等
而编程中、通信中所说的同步与生活中大家印象中的同步概念略有差异。
“同”字应是指协同、协助、互相配合。主旨在协同步调,按预定的先后次序运行。
2.线程同步
同步即协同步调,按预定的先后次序运行。
线程同步,指一个线程发出某一功能调用时,在没有得到结果之前,该调用不返回。
同时其它线程为保证数据一致性,不能调用该功能。
举例 1: 银行存款 5000。柜台,折:取 3000;提款机,卡:取 3000。剩余:2000
举例 2: 内存中 100 字节,线程 T1 欲填入全 1, 线程 T2 欲填入全 0。但如果 T1 执行了 50 个字节失去 cpu,
T2执行,会将 T1 写过的内容覆盖。当 T1 再次获得 cpu 继续 从失去 cpu 的位置向后写入 1,
当执行结束,内存中的100 字节,既不是全 1,也不是全 0。
产生的现象叫做“与时间有关的错误”(time related)。为了避免这种数据混乱,线程需要同步。
“同步”的目的,是为了避免数据混乱,解决与时间有关的错误。
实际上,不仅线程间需要同步,进程间、信号间等等都需要同步机制。
因此,所有“多个控制流,共同操作一个共享资源”的情况,都需要同步。
3.数据混乱原因:
- 资源共享(独享资源则不会)
- 调度随机(意味着数据访问会出现竞争)
- 线程间缺乏必要的同步机制。
以上 3 点中,前两点不能改变,欲提高效率,传递数据,资源必须共享。只要共享资源,就一定会出现竞争。
只要存在竞争关系,数据就很容易出现混乱。
所以只能从第三点着手解决。使多个线程在访问共享资源的时候,出现互斥。
4.互斥量 mutex
Linux 中提供一把互斥锁 mutex(也称之为互斥量)。
每个线程在对资源操作前都尝试先加锁,成功加锁才能操作,操作结束解锁。
资源还是共享的,线程间也还是竞争的,
但通过“锁”就将资源的访问变成互斥操作,而后与时间有关的错误也不会再产生了。
但,应注意:同一时刻,只能有一个线程持有该锁。
当 A 线程对某个全局变量加锁访问,B 在访问前尝试加锁,拿不到锁,B 阻塞。C 线程不去加锁,而直接访问
该全局变量,依然能够访问,但会出现数据混乱。
所以,互斥锁实质上是操作系统提供的一把“建议锁”(又称“协同锁”),建议程序中有多线程访问共享资源
的时候使用该机制。但,并没有强制限定。
因此,即使有了 mutex,如果有线程不按规则来访问数据,依然会造成数据混乱。
5.mutex 主要应用函数:
pthread_mutex_init 函数
pthread_mutex_destroy 函数
pthread_mutex_lock 函数
pthread_mutex_trylock 函数
pthread_mutex_unlock 函数
以上 5 个函数的返回值都是:成功返回 0, 失败返回错误号。
pthread_mutex_t 类型,其本质是一个结构体。为简化理解,应用时可忽略其实现细节,简单当成整数看待。
pthread_mutex_t mutex; 变量 mutex 只有两种取值 1、0。
5.1 pthread_mutex_init 函数
初始化一个互斥锁(互斥量) —> 初值可看作 1
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr);
参 1:传出参数,调用时应传 &mutex
restrict 关键字:只用于限制指针,告诉编译器,所有修改该指针指向内存中内容的操作,
只能通过本指针完成。不能通过除本指针以外的其他变量或指针修改
参 2:互斥量属性。是一个传入参数,通常传 NULL,选用默认属性(线程间共享)。 参 APUE.12.4 同步属性
1. 静态初始化:如果互斥锁 mutex 是静态分配的(定义在全局,或加了 static 关键字修饰),
可以直接使用宏进行初始化。e.g. pthead_mutex_t muetx = PTHREAD_MUTEX_INITIALIZER;
3. 动态初始化:局部变量应采用动态初始化。e.g. pthread_mutex_init(&mutex, NULL)
5.2 pthread_mutex_destroy 函数
销毁一个互斥锁
int pthread_mutex_destroy(pthread_mutex_t *mutex);
5.3 pthread_mutex_lock 函数
加锁。可理解为将 mutex–(或 -1),操作后 mutex 的值为 0。
int pthread_mutex_lock(pthread_mutex_t *mutex);
5.4 pthread_mutex_unlock 函数
解锁。可理解为将 mutex ++(或 +1),操作后 mutex 的值为 1。
int pthread_mutex_unlock(pthread_mutex_t *mutex);
5.5 pthread_mutex_trylock 函数
尝试加锁
int pthread_mutex_trylock(pthread_mutex_t *mutex);
5.6 加锁与解锁
lock 与 unlock:
lock 尝试加锁,如果加锁不成功,线程阻塞,阻塞到持有该互斥量的其他线程解锁为止。
unlock 主动解锁函数,同时将阻塞在该锁上的所有线程全部唤醒,至于哪个线程先被唤醒,
取决于优先级、调度。默认:先阻塞、先唤醒。
例如:T1 T2 T3 T4 使用一把 mutex 锁。T1 加锁成功,其他线程均阻塞,直至 T1 解锁。
T1 解锁后,T2 T3 T4 均被唤醒,并自动再次尝试加锁。
可假想 mutex 锁 init 成功初值为 1。lock 功能是将 mutex--。而 unlock 则将 mutex++。
5.7 lock 与 trylock:
lock 加锁失败会阻塞,等待锁释放。
trylock 加锁失败直接返回错误号(如:EBUSY),不阻塞。
6.加锁代码演示
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
void *tfn(void *arg)
{
srand(time(NULL));
while (1) {
printf("hello ");
sleep(rand() % 3); /*模拟长时间操作共享资源,导致 cpu 易主,产生与时间有关的错误*/
printf("world\n");
sleep(rand() % 3);
}
return NULL;
}
int main(void)
{
pthread_t tid;
srand(time(NULL));
pthread_create(&tid, NULL, tfn, NULL);
while (1) {
printf("HELLO ");
sleep(rand() % 3);
printf("WORLD\n");
sleep(rand() % 3);
}
pthread_join(tid, NULL);
return 0;
}
zhaoxr@zhaoxr-ThinkPad-E450:~/pthread$ ./pthread_mutex
HELLO hello world
WORLD
HELLO hello WORLD
HELLO WORLD
HELLO world
WORLD
hello HELLO WORLD
HELLO world
WORLD
hello world
HELLO WORLD
^Z
[1]+ 已停止 ./pthread_mutex
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
pthread_mutex_t mutex;
void *tfn(void *arg)
{
srand(time(NULL));
while (1) {
pthread_mutex_lock(&mutex);
printf("hello ");
sleep(rand() % 3); /*模拟长时间操作共享资源,导致 cpu 易主,产生与时间有关的错误*/
printf("world\n");
pthread_mutex_unlock(&mutex);
sleep(rand() % 3);
}
return NULL;
}
int main(void)
{
pthread_t tid;
srand(time(NULL));
pthread_mutex_init(&mutex,NULL);//mutex=1
pthread_create(&tid, NULL, tfn, NULL);
while (1) {
pthread_mutex_lock(&mutex);
printf("HELLO ");
sleep(rand() % 3);
printf("WORLD\n");
pthread_mutex_unlock(&mutex);
sleep(rand() % 3);
}
pthread_join(tid, NULL);
pthread_mutex_destroy(&mutex);
return 0;
}
zhaoxr@zhaoxr-ThinkPad-E450:~/pthread$ ./pthread_mutex
HELLO WORLD
hello world
HELLO WORLD
hello world
HELLO WORLD
hello world
HELLO WORLD
hello world
HELLO WORLD
HELLO WORLD
hello world
HELLO WORLD
hello world
HELLO WORLD
^Z
[4]+ 已停止 ./pthread_mutex
结论:
在访问共享资源前加锁,访问结束后立即解锁。锁的“粒度”应越小越好。
7.死锁
- 线程试图对同一个互斥量 A 加锁两次。
- 线程 1 拥有 A 锁,请求获得 B 锁;线程 2 拥有 B 锁,请求获得 A 锁
以上为死锁的两种情况。
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
pthread_mutex_t mutex;
void *tfn(void *arg)
{
srand(time(NULL));
while (1) {
pthread_mutex_lock(&mutex);
printf("hello ");
sleep(rand() % 3); /*模拟长时间操作共享资源,导致 cpu 易主,产生与时间有关的错误*/
printf("world\n");
pthread_mutex_lock(&mutex);/*加锁两次,死锁*/
sleep(rand() % 3);
}
return NULL;
}
int main(void)
{
pthread_t tid;
srand(time(NULL));
pthread_mutex_init(&mutex,NULL);//mutex=1
pthread_create(&tid, NULL, tfn, NULL);
while (1) {
pthread_mutex_lock(&mutex);
printf("HELLO ");
sleep(rand() % 3);
printf("WORLD\n");
pthread_mutex_unlock(&mutex);
sleep(rand() % 3);
}
pthread_join(tid, NULL);
pthread_mutex_destroy(&mutex);
return 0;
}
zhaoxr@zhaoxr-ThinkPad-E450:~/pthread$ ./pthread_mutex_death
HELLO WORLD
hello world
^Z
[5]+ 已停止 ./pthread_mutex_death
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
pthread_mutex_t mutex_A;
pthread_mutex_t mutex_B;
void *tfn(void *arg)
{
srand(time(NULL));
while (1) {
pthread_mutex_lock(&mutex_B);
sleep(1);
pthread_mutex_lock(&mutex_A);
printf("hello ");
printf("world\n");
pthread_mutex_unlock(&mutex_A);
pthread_mutex_unlock(&mutex_B);
sleep(rand() % 3);
}
return NULL;
}
int main(void)
{
pthread_t tid;
srand(time(NULL));
pthread_mutex_init(&mutex_A,NULL);//mutex=1
pthread_mutex_init(&mutex_B,NULL);
pthread_create(&tid, NULL, tfn, NULL);
while (1) {
pthread_mutex_lock(&mutex_A);
sleep(1);
pthread_mutex_lock(&mutex_B);
printf("HELLO ");
printf("WORLD\n");
pthread_mutex_unlock(&mutex_B);
pthread_mutex_unlock(&mutex_A);
sleep(rand() % 3);
}
pthread_join(tid, NULL);
pthread_mutex_destroy(&mutex_A);
pthread_mutex_destroy(&mutex_B);
return 0;
}
zhaoxr@zhaoxr-ThinkPad-E450:~/pthread$ ./pthread_mutex_death
^Z
[6]+ 已停止 ./pthread_mutex_death
8.如何解决死锁
使用pthread_mutex_trylock函数,尝试加锁,若不成功,就释放自己手中的已有的锁。
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
pthread_mutex_t mutex_A;
pthread_mutex_t mutex_B;
void *tfn(void *arg)
{
srand(time(NULL));
while (1) {
int death_pid;
pthread_mutex_lock(&mutex_B);
sleep(1);
if(pthread_mutex_trylock(&mutex_A)!=0){
pthread_mutex_unlock(&mutex_B);
printf("trylock fail,delete mutex_B\n");
sleep(1);
}
else{
printf("hello ");
printf("world\n");
pthread_mutex_unlock(&mutex_A);
pthread_mutex_unlock(&mutex_B);
sleep(rand() % 3);
}
}
return NULL;
}
int main(void)
{
pthread_t tid;
srand(time(NULL));
pthread_mutex_init(&mutex_A,NULL);//mutex=1
pthread_mutex_init(&mutex_B,NULL);
pthread_create(&tid, NULL, tfn, NULL);
while (1) {
pthread_mutex_lock(&mutex_A);
sleep(1);
pthread_mutex_lock(&mutex_B);
printf("HELLO ");
printf("WORLD\n");
pthread_mutex_unlock(&mutex_B);
pthread_mutex_unlock(&mutex_A);
sleep(rand() % 3);
}
pthread_join(tid, NULL);
pthread_mutex_destroy(&mutex_A);
pthread_mutex_destroy(&mutex_B);
return 0;
}
zhaoxr@zhaoxr-ThinkPad-E450:~/pthread$ ./pthread_mutex_death
trylock fail,delete mutex_B
HELLO WORLD
trylock fail,delete mutex_B
HELLO WORLD
trylock fail,delete mutex_B
HELLO WORLD
trylock fail,delete mutex_B
HELLO WORLD
trylock fail,delete mutex_B
HELLO WORLD
HELLO trylock fail,delete mutex_B
WORLD
trylock fail,delete mutex_B
HELLO WORLD
hello world
HELLO WORLD
trylock fail,delete mutex_B
HELLO WORLD
trylock fail,delete mutex_B
HELLO WORLD
trylock fail,delete mutex_B
HELLO WORLD
trylock fail,delete mutex_B
HELLO WORLD
hello world
HELLO WORLD
hello world
^Z
[11]+ 已停止 ./pthread_mutex_death
9.读写锁
与互斥量类似,但读写锁允许更高的并行性。其特性为:写独占,读共享。
读写锁也叫共享-独占锁。
当读写锁以读模式锁住时,它是以共享模式锁住的;
当它以写模式锁住时,它是以独占模式锁住的。
写独占、读共享。
读写锁非常适合于对数据结构读的次数远大于写的情况。
9.1 读写锁状态:
特别强调:读写锁只有一把,但其具备两种状态:
- 读模式下加锁状态 (读锁)
- 写模式下加锁状态 (写锁)
9.2 读写锁特性:
- 读写锁是“写模式加锁”时, 解锁前,所有对该锁加锁的线程都会被阻塞。
- 读写锁是“读模式加锁”时, 如果线程以读模式对其加锁会成功;如果线程以写模式加锁会阻塞。
- 读写锁是“读模式加锁”时, 既有试图以写模式加锁的线程,也有试图以读模式加锁的线程。那么读写锁会阻塞随后的读模式锁请求。优先满足写模式锁。读锁、写锁并行阻塞,写锁优先级高
10.读写锁函数
pthread_rwlock_init 函数
pthread_rwlock_destroy 函数
pthread_rwlock_rdlock 函数
pthread_rwlock_wrlock 函数
pthread_rwlock_tryrdlock 函数
pthread_rwlock_trywrlock 函数
pthread_rwlock_unlock 函数
以上 7 个函数的返回值都是:成功返回 0, 失败直接返回错误号。
pthread_rwlock_t 类型 用于定义一个读写锁变量。
pthread_rwlock_t rwlock;
10.1 pthread_rwlock_init 函数
初始化一把读写锁
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
const pthread_rwlockattr_t *restrict attr);
参 2:attr 表读写锁属性,通常使用默认属性,传 NULL 即可。
10.2 pthread_rwlock_destroy 函数
销毁一把读写锁
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
10.3 pthread_rwlock_rdlock 函数
以读方式请求读写锁。(常简称为:请求读锁)
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
10.4 pthread_rwlock_wrlock 函数
以写方式请求读写锁。(常简称为:请求写锁)
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
10.5 pthread_rwlock_unlock 函数
解锁
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
10.6 pthread_rwlock_tryrdlock 函数
非阻塞以读方式请求读写锁(非阻塞请求读锁)
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
10.7 读写锁示例
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
int counter;
pthread_rwlock_t rwlock;
/* 3 个线程不定时写同一全局资源,5 个线程不定时读同一全局资源 */
void *th_write(void *arg)
{
int t, i = (int)arg;
while (1) {
pthread_rwlock_wrlock(&rwlock);
t = counter;
usleep(1000);
printf("=======write %d: %lu: counter=%d ++counter=%d\n", i, pthread_self(), t, ++counter);
pthread_rwlock_unlock(&rwlock);
usleep(10000);
}
return NULL;
}
void *th_read(void *arg)
{
int i = (int)arg;
while (1) {
pthread_rwlock_rdlock(&rwlock);
printf("----------------------------read %d: %lu: %d\n", i, pthread_self(), counter);
pthread_rwlock_unlock(&rwlock);
usleep(2000);
}
return NULL;
}
int main(void)
{
int i;
pthread_t tid[8];
pthread_rwlock_init(&rwlock, NULL);
for (i = 0; i < 3; i++)
pthread_create(&tid[i], NULL, th_write, (void *)i);
for (i = 0; i < 5; i++)
pthread_create(&tid[i+3], NULL, th_read, (void *)i);
for (i = 0; i < 8; i++)
pthread_join(tid[i], NULL);
pthread_rwlock_destroy(&rwlock);
return 0;
}