什么是线程同步
所谓线程同步,不是指线程同步运行,而是指线程协同步调,通过我们的设置,按照我们预期的顺序或规则去执行。通过线程同步,可以解决共享资源数据不一致的问题。
一个例子
首先我们来看一个例子:
定义一个全局变量a,创建10个线程,每个线程对a进行自增(a++)1万次,所有线程自增结束主线程打印a的值。
#include <fcntl.h>
#include <pthread.h>
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <pthread.h>
int a = 0;
void* work(void*) {
for(int a = 1; i <= (int)1e6; i++) {
i++;
}
return NULL;
}
int main() {
pthread_t tids[10];
for(int i = 0; i < 10; i++) {
pthread_create(tids + i, NULL,
work, NULL);
}
for(int i = 0; i < 10; i++) {
pthread_join(tids[i], NULL);
}
printf("a = %d \n", a);
return 0;
}
运行结果
通过运行结果我们可以发现,几乎每次的结果都是不足 1e6次循环 * 10 个线程 = 10000000
的, 这是为什么呢? 其实就是因为我们没有线程同步。a++
这行语句看似只有一行语句,实际上他要执行以下几个操作
- 从内存中加载值:CPU 会根据
a
的地址,从内存中读取存储a
的值,将其加载到寄存器中 - 增加 1:CPU 将寄存器中的
a
值加上 1。 将结果存回内存:最后,CPU 将增加后的结果重新存储到
a
的地址处,覆盖原来的值比如说,现在有一个线程他执行了第一个步骤,将
a
的值存储到寄存器中,假设它存储的值是10, 这时候有另一个线程,他执行了第三个步骤,将 内存中a
的值更新成了 11, 这时候现在这个线程才执行到了后两步,因为它的寄存器中存储的是10, 他就又把a
的值设置为 11, 正常来说这两个线程会将a
自增两次 达到 12, 可是经过上边的过程,a
的值还是11, 这就是最终的结果比预期的小的原因。专业一点的说法就是
a++
这个操作不是原子性的。
原子操作
原子操作是指的是不可中断的操作,要么完全执行,要么完全不执行,不会被其他线程或进程中断。
解决方案
mutex 互斥锁
还拿上面的例子来说,想要解决它的问题,可以使用互斥锁解决,既然多个线程同时执行a++会出现问题,那么我们限制同一时刻只有一个线程可以执行a++
语句不就可以了吗, 通过mutex 就可以实现我们的想法。
当一个线程拿到互斥锁后, 只有这个线程可以访问到a++
, 其他线程在它释放锁之前只能干等着,这样就防止了多个线程同时执行a++这个非原子操作导致的数据不一致问题。
示例
#include <fcntl.h>
#include <pthread.h>
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <pthread.h>
int a = 0;
pthread_mutex_t mutex; // pthread 提供的互斥锁
void* work(void*) {
for(int i = 1; i <= (int)1e6; i++) {
pthread_mutex_lock(&mutex); // 访问a++这条语句前加锁,如果mutex已经被加锁了,线程会阻塞在这里
a++;
pthread_mutex_unlock(&mutex); //访问结束后解锁加锁
}
return NULL;
}
int main() {
pthread_mutex_init(&mutex, NULL); // 初始化互斥锁
pthread_t tids[10];
for(int i = 0; i < 10; i++) {
pthread_create(tids + i, NULL, //创建10个线程
work, NULL);
}
for(int i = 0; i < 10; i++) {
pthread_join(tids[i], NULL);
}
printf("a = %d \n", a);
pthread_mutex_destroy(&mutex); // 释放互斥锁
return 0;
}
运行结果
这次我们发现,运行的结果正确了,但是速度好像慢了不少,其实是因为,我们使用了互斥锁,同一时刻只能有一个线程执行a++这条语句,效果和单线程也差不多了,而且还多了线程切换的开销。其实对于我们的这段代码来说,完全没有必要使用多线程,并不会有提升,但是到了实际编程的时候,访问共享资源的代码只占一部分,是可以提升性能的。