问题
多个线程同时操作一个全局变量,会发生什么?
下面的程序输出什么?为什么?
多线程操作全局变量实验
test1.c
#define _GNU_SOURCE /* To get pthread_getattr_np() declaration */
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <memory.h>
#include <sys/syscall.h>
pid_t gettid(void)
{
return syscall(SYS_gettid);
}
int g_count = 0;
void* thread_entry(void* arg)
{
int i = 0;
while( i < 10000 )
{
g_count++;
i++;
}
pthread_detach(pthread_self());
return NULL;
}
int main()
{
pthread_t t = 0;
int r = 0;
for(r=0; r<5; r++)
{
pthread_create(&t, NULL, thread_entry, NULL);
}
sleep(5);
printf("g_count = %d\n", g_count);
return 0;
}
该程序创建了 5 个线程,每个线程对全局变量 g_count 进行 10000 次的后置 ++ 操作
将该程序执行多次,程序运行结果如下图所示:
我们预期的 g_count 的值应该为 50000,但某一次的运行结果 g_count 的值为 44053,并不符合我们的预期
这是因为 g_count++ 并不是原子操作,g_count++ 会经历三个步骤,首先会先将 g_count 的值赋值给一个临时变量,然后将 g_count 的值加一,最后返回这个临时变量的值,如果在这三个步骤之间进行线程切换,那么就可能会导致 g_count 的值不符合我们的预期
什么是原子操作?
这种操作一旦开始,就一直执行到结束,中途不会被打断
原子操作可以是一个步骤,也可以是多个步骤的集合
原子操作的顺序不可以被打乱,也不可以被切割而只执行其中的一部分
原子操作在多 任务/线程 并发时能够保证操作结果的正确性
思考
程序中的 i++ 是原子操作吗?
i++ 在 C/C++ 语言中不是原子操作,因此在多 任务/线程 并发场景中无法保证语义正确性
结论:应该避免多个线程同时操作一个全局变量
需求:保证操作的原子性!
临界区:
- 临界区是访问共享资源的代码片段 (共享资源无法同时被多线程访问)
- 临界区一次仅允许一个线程进入执行 (临界区具有原子性)
- 当有线程进入临界区时,其他线程必须等待 (线程之间存在竞争关系)
临界区的访问方式
Linux 中的互斥量
互斥量:用来保证临界区的原子性,可理解为临界区 "门锁"
Linux 中的互斥量 API 函数
互斥锁实验
test2.c
#define _GNU_SOURCE /* To get pthread_getattr_np() declaration */
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <memory.h>
#include <sys/syscall.h>
pid_t gettid(void)
{
return syscall(SYS_gettid);
}
int g_count = 0;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
void* thread_entry(void* arg)
{
int i = 0;
while( i < 10000 )
{
pthread_mutex_lock(&mutex);
g_count++;
pthread_mutex_unlock(&mutex);
i++;
}
pthread_detach(pthread_self());
return NULL;
}
int main()
{
int r = 0;
pthread_t t = 0;
for(r=0; r<5; r++)
{
pthread_create(&t, NULL, thread_entry, NULL);
}
sleep(5);
printf("g_count = %d\n", g_count);
return 0;
}
pthread_mutex_lock() 用于获取互斥锁,获取成功则继续向下执行,获取失败则线程进入阻塞状态,等待互斥锁释放
pthread_mutex_unlock() 用于释放互斥锁,会通知阻塞在这个互斥锁上的线程来抢夺互斥锁
通过 mutex 来对共享资源 g_count 进行互斥访问,保证只有某个线程访问好了 g_count,下一个线程才能访问,确保 g_count 的值是正确的
将该程序执行多次,程序运行结果如下图所示:
g_count 的值一直为 50000,符合我们的预期
另一个多线程示例
pthread_mutex_trylock() 实验
test3.c
#define _GNU_SOURCE /* To get pthread_getattr_np() declaration */
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <memory.h>
#include <sys/syscall.h>
pid_t gettid(void)
{
return syscall(SYS_gettid);
}
int g_count = 0;
void* thread_entry(void* arg)
{
pthread_mutex_t* pm = arg;
int i = 0;
while( i < 10000 )
{
if( pthread_mutex_trylock(pm) )
continue;
g_count++;
pthread_mutex_unlock(pm);
i++;
}
pthread_detach(pthread_self());
return NULL;
}
int main()
{
int r = 0;
pthread_t t = 0;
pthread_mutex_t mutex;
pthread_mutexattr_t mattr;
r = pthread_mutexattr_init(&mattr);
r = pthread_mutex_init(&mutex, &mattr);
for(r=0; r<5; r++)
{
pthread_create(&t, NULL, thread_entry, &mutex);
}
sleep(5);
printf("g_count = %d\n", g_count);
return 0;
}
pthread_mutex_trylock() 会尝试去获取互斥锁,如果获取不到则立即返回,不会进入阻塞状态
这里使用 pthread_mutex_trylock() 来对共享资源 g_count 进行互斥访问,但是会存在一个性能问题,如果获取不到锁则会一直尝试去获取,会消耗较多的 cpu 资源
将该程序执行多次,程序运行结果如下图所示:
g_count 的值一直为 50000,符合我们的预期
哲学家进餐问题
多线程模型建立
哲学家:线程模拟,只有两个动作 think() 和 eat()
筷子:互斥量模拟,每个互斥量代表一只筷子
解决方案流程
哲学家进餐实验
philosopher.c
#include <pthread.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#define NUM 5
static pthread_mutex_t mutex[NUM];
static void think(int i)
{
printf("philosopher %d is thinking...\n", i);
usleep(200 * 1000);
}
static void eat(int i)
{
printf("philosopher %d is eating...\n", i);
usleep(500 * 1000);
}
static void pick_up(int i)
{
if((i >= 0) && (i < NUM))
{
pthread_mutex_lock(&mutex[i]);
}
}
static void put_down(int i)
{
if((i >= 0) && (i < NUM))
{
pthread_mutex_unlock(&mutex[i]);
}
}
static void pick_up_left(int i)
{
int index = i % NUM;
pick_up(index);
}
static void pick_up_right(int i)
{
int index = (i + 1) % NUM;
pick_up(index);
}
static void put_down_left(int i)
{
int index = i % NUM;
put_down(index);
}
static void put_down_right(int i)
{
int index = (i + 1) % NUM;
put_down(index);
}
static void* pthread_entry(void* arg)
{
long i = (long)arg;
pthread_detach(pthread_self());
while(1)
{
think(i);
pick_up_left(i);
pick_up_right(i);
eat(i);
put_down_right(i);
put_down_left(i);
}
return NULL;
}
int main(int argc, char* argv[])
{
pthread_t tid[NUM] = {0};
for(long i = 0; i < NUM; i++)
{
pthread_mutex_init(&mutex[i], NULL);
pthread_create(&tid[i], NULL, pthread_entry, (void*)i);
}
while(1)
{
sleep(1);
}
return 0;
}
该程序创建了 5 个线程来模拟哲学家,用 5 个互斥锁来模拟筷子,哲学家需要拿到左右两双筷子才能进食
程序运行结果如下图所示: