初识多线程互斥量

文章探讨了多线程环境中全局变量g_count的并发操作可能导致的非预期结果,通过使用互斥锁(如pthread_mutex)实现原子操作,确保了线程安全。还介绍了原子操作的概念,以及如何在哲学家进餐问题中模拟互斥条件。
摘要由CSDN通过智能技术生成

问题

多个线程同时操作一个全局变量,会发生什么?

下面的程序输出什么?为什么?

多线程操作全局变量实验

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 个互斥锁来模拟筷子,哲学家需要拿到左右两双筷子才能进食

程序运行结果如下图所示:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值