线程同步是在多线程编程过程中对数据保护的一种机制,保护的数据是共享数据
。共享数据就是多个线程共同访问的一块资源,也就是一块内存。假设有3个线程,其中A,B
线程在同一个时间点往这块内存中写数据,于此同时C线程往这块内存中读数据,注意事同一个时间点,如果不是一个时间点就没有线程同步的问题了,很显然,同一时间点读写数据,读出的数据很可能就出现问题,不是正确的数据。
假设有 4 个线程 A、B、C、D,当前一个线程 A 对内存中的共享资源进行访问的时候,其他线程 B, C, D 都不可以对这块内存进行操作,直到线程 A 对这块内存访问完毕为止,B,C,D 中的一个才能访问这块内存,剩余的两个需要继续阻塞等待,以此类推,直至所有的线程都对这块内存操作完毕。 线程对内存的这种访问方式就称之为线程同步
,通过对概念的介绍,我们可以了解到所谓的同步并不是多个线程同时对内存进行访问,而是按照先后顺序依次进行的
。也就是说线程同步,并不是让线程并行
去执行,而是让线程串行
去执行。
因此线程同步访问共享资源的时候,由于是串行的,所以效率肯定就变低了。但线程同步可以保证共享数据的安全性
·
1.1 为什么要同步
在研究线程同步之前,先来看一个两个线程同时数数(每个线程数 50 个数,交替数到 100)的例子:
#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;
// 线程处理函数
void* funcA_num(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_num(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(int argc, const char* argv[])
{
pthread_t p1, p2;
// 创建两个子线程
pthread_create(&p1, NULL, funcA_num, NULL);
pthread_create(&p2, NULL, funcB_num, NULL);
// 阻塞,资源回收
pthread_join(p1, NULL);
pthread_join(p2, NULL);
return 0;
}
上面的例子是两个线程进行同时数数
- 在main函数中,创建了2个子线程
p1,p2
, 另外还有一个主线程
,也就是总共3个线程。两个子线程分别执行任务函数funcA_num
和funcB_num
。并通过pthread_join
阻塞等待子线程执行完成并回收资源。 - 在
funcA_num
定义了一个for循环,执行MAX
也就是数Max=50
个数,输一次打印一次结果; 在funcB_num
中也是做了相同的操作,数了50个数。 - 数数的时候都是基于全局变量
number
进行数据递增,很显然两个线程各自数50个,并操作同一个共享全局变量,最终应该得到100.
编译并执行上面的测试程序,得到如下结果:
$ ./a.out
Thread B, id = 140504473724672, number = 1
Thread B, id = 140504473724672, number = 2
Thread A, id = 140504482117376, number = 2
Thread B, id = 140504473724672, number = 3
Thread A, id = 140504482117376, number = 4
Thread B, id = 140504473724672, number = 5
Thread A, id = 140504482117376, number = 6
Thread B, id = 140504473724672, number = 7
Thread B, id = 140504473724672, number = 8
Thread A, id = 140504482117376, number = 7
Thread B, id = 140504473724672, number = 8
Thread B, id = 140504473724672, number = 9
Thread A, id = 140504482117376, number = 8
Thread B, id = 140504473724672, number = 9
Thread A, id = 140504482117376, number = 9
Thread B, id = 140504473724672, number = 10
Thread B, id = 140504473724672, number = 11
Thread A, id = 140504482117376, number = 10
Thread B, id = 140504473724672, number = 11
Thread A, id = 140504482117376, number = 11
Thread B, id = 140504473724672, number = 12
Thread A, id = 140504482117376, number = 13
Thread B, id = 140504473724672, number = 14
Thread A, id = 140504482117376, number = 15
Thread B, id = 140504473724672, number = 16
Thread B, id = 140504473724672, number = 17
Thread B, id = 140504473724672, number = 18
Thread B, id = 140504473724672, number = 19
Thread A, id = 140504482117376, number = 17
Thread B, id = 140504473724672, number = 18
Thread B, id = 140504473724672, number = 19
Thread A, id = 140504482117376, number = 19
Thread B, id = 140504473724672, number = 20
Thread A, id = 140504482117376, number = 20
Thread B, id = 140504473724672, number = 21
Thread A, id = 140504482117376, number = 21
Thread B, id = 140504473724672, number = 22
Thread A, id = 140504482117376, number = 22
Thread B, id = 140504473724672, number = 23
Thread A, id = 140504482117376, number = 23
Thread B, id = 140504473724672, number = 24
Thread A, id = 140504482117376, number = 24
Thread B, id = 140504473724672, number = 25
Thread A, id = 140504482117376, number = 25
Thread B, id = 140504473724672, number = 26
Thread A, id = 140504482117376, number = 26
Thread B, id = 140504473724672, number = 27
Thread A, id = 140504482117376, number = 27
Thread B, id = 140504473724672, number = 28
Thread A, id = 140504482117376, number = 28
Thread B, id = 140504473724672, number = 29
Thread A, id = 140504482117376, number = 29
Thread B, id = 140504473724672, number = 30
Thread A, id = 140504482117376, number = 30
Thread B, id = 140504473724672, number = 31
Thread A, id = 140504482117376, number = 31
Thread B, id = 140504473724672, number = 32
Thread A, id = 140504482117376, number = 32
Thread B, id = 140504473724672, number = 33
Thread A, id = 140504482117376, number = 33
Thread B, id = 140504473724672, number = 34
Thread A, id = 140504482117376, number = 34
Thread B, id = 140504473724672, number = 35
Thread A, id = 140504482117376, number = 35
Thread B, id = 140504473724672, number = 36
Thread A, id = 140504482117376, number = 36
Thread B, id = 140504473724672, number = 37
Thread A, id = 140504482117376, number = 37
Thread B, id = 140504473724672, number = 38
Thread A, id = 140504482117376, number = 38
Thread B, id = 140504473724672, number = 39
Thread A, id = 140504482117376, number = 39
Thread A, id = 140504482117376, number = 40
Thread B, id = 140504473724672, number = 41
Thread B, id = 140504473724672, number = 42
Thread A, id = 140504482117376, number = 42
Thread A, id = 140504482117376, number = 43
Thread B, id = 140504473724672, number = 44
Thread B, id = 140504473724672, number = 45
Thread A, id = 140504482117376, number = 45
Thread B, id = 140504473724672, number = 46
Thread A, id = 140504482117376, number = 46
Thread B, id = 140504473724672, number = 47
Thread A, id = 140504482117376, number = 47
Thread B, id = 140504473724672, number = 48
Thread A, id = 140504482117376, number = 48
Thread B, id = 140504473724672, number = 49
Thread A, id = 140504482117376, number = 50
Thread B, id = 140504473724672, number = 51
Thread A, id = 140504482117376, number = 51
Thread B, id = 140504473724672, number = 52
Thread A, id = 140504482117376, number = 53
Thread A, id = 140504482117376, number = 54
Thread A, id = 140504482117376, number = 55
Thread A, id = 140504482117376, number = 56
Thread A, id = 140504482117376, number = 57
Thread A, id = 140504482117376, number = 58
Thread A, id = 140504482117376, number = 59
Thread A, id = 140504482117376, number = 60
Thread A, id = 140504482117376, number = 61
通过对上面例子的测试,可以看出虽然每个线程内部循环了 50 次每次数一个数,但是最终没有数到 100,通过输出的结果可以看到,有些数字被重复数了多次,其原因就是没有对线程进行同步处理,造成了数据的混乱。
两个线程在数数的时候需要分时复用 CPU 时间片
,并且测试程序中调用了 sleep() 导致线程的 CPU 时间片没用完就被迫挂起了
,这样就能让 CPU 的上下文切换
(保存当前状态,下一次继续运行的时候需要加载保存的状态)更加频繁
,更容易再现数据混乱
的这个现象。
CPU 对应寄存器、一级缓存、二级缓存、三级缓存是独占的,用于存储处理的数据和线程的状态信息,数据被 CPU 处理完成需要再次被写入到物理内存中
,物理内存数据也可以通过文件 IO 操作写入到磁盘中。
在测试程序中两个线程共用全局变量 number
当线程变成运行态之后开始数数,从物理内存加载数据,然后将数据放到 CPU 进行运算,最后将结果更新到物理内存中。如果数数的两个线程都可以顺利完成这个流程,那么得到的结果肯定是正确的。
如果线程 A 执行这个过程期间就失去了 CPU 时间片,线程 A 被挂起了最新的数据没能更新到物理内存
。线程 B 变成运行态之后从物理内存读数据,很显然它没有拿到最新数据
,只能基于旧的数据往后数,然后失去 CPU 时间片挂起。线程 A 得到 CPU 时间片变成运行态,第一件事儿就是将上次没更新到内存的数据更新到内存,但是这样会导致线程 B 已经更新到内存的数据被覆盖,活儿白干了,最终导致有些数据会被重复数很多次
。
如何解决这一现象,其实就可以利用线程同步来解决。线程同步解决了数据没被写回去的问题,如果做了线程同步,就能保证数据从cpu同步到物理内存中。同步回去之后,另外一个线程才能去读取处理共享数据。
1.2 同步方式
对于多个线程访问共享资源出现数据混乱的问题,需要进行线程同步
。常用的线程同步方式有四种
:互斥锁
、读写锁
、条件变量
、信号量
。所谓的共享资源
就是多个线程共同访问的变量
,这些变量通常为全局数据区变量或者堆区变量,这些变量对应的共享资源也被称之为临界资源
。
找到临界资源
之后,再找和临界资源相关的上下文代码,这样就得到了一个代码块,这个代码块可以称之为临界区
。确定好临界区(临界区越小越好)之后,就可以进行线程同步了,线程同步的大致处理思路是这样的:
在临界区代码的上边,添加加锁函数
,对临界区加锁。- 哪个线程调用这句代码,就会把这把锁锁上(获得cpu时间片),其他线程就只能阻塞并失去了cpu时间片。
- 在
临界区代码的下边,添加解锁函数
,对临界区解锁。- 此时堵塞在这把锁的线程就被唤醒,然后抢占时间片,进入临界区。
- 出临界区的线程会将锁定的那把锁打开,其他抢到锁的线程就可以进入到临界区了。
- 通过锁机制能保证临界区代码
最多只能同时有一个线程访问
,这样并行访问就变为串行访问了
。