使用互斥锁实现线程同步

使用互斥锁实现线程同步

c语言中文网:使用互斥锁实现线程同步

互斥锁实现多线程同步的核心思想是:有线程访问进程空间中的公共资源时,该线程执行“加锁”操作(将资源“锁”起来),阻止其它线程访问。访问完成后,该线程负责完成“解锁”操作,将资源让给其它线程。当有多个线程想访问资源时,谁最先完成“加锁”操作,谁就最先访问资源。

当有多个线程想访问“加锁”状态下的公共资源时,它们只能等待资源“解锁”,所有线程会排成一个==等待(阻塞)队列==。资源解锁后,==操作系统会唤醒等待队列中的所有线程,第一个访问资源的线程会率先将资源“锁”起来,其它线程则继续等待。==

本质上,互斥锁就是一个全局变量,它只有 “lock” 和 “unlock” 两个值,含义分别是:

  • “unlock” 表示当前资源可以访问,第一个访问资源的线程负责将互斥锁的值改为 “lock”,访问完成后再重置为“unlock”;
  • “lock” 表示有线程正在访问资源,其它线程需等待互斥锁的值为 “unlock” 后才能开始访问。

通过对资源进行 “加锁(lock)”和 “解锁(unlock)”,可以确保同一时刻最多有 1 个线程访问该资源,从根本上避免了“多线程抢夺资源”的情况发生。

再次强调,对资源进行“加锁”和“解锁”操作的必须是同一个线程。换句话说,哪个线程对资源执行了“加锁”操作,那么“解锁”操作也必须由该线程负责。

互斥锁的用法

POSIX 标准规定,用 pthread_mutex_t 类型的变量来表示一个互斥锁,该类型以结构体的形式定义在<pthread.h>头文件中。举个例子:

1
pthread_mutex_t myMutex;

我们成功地定义了一个名为 myMutex 的互斥锁,但要想使用它,还要进行初始化操作。

1) 互斥锁的初始化

初始化 pthread_mutex_t 变量的方式有两种,分别为:

  1. 使用特定的宏 PTHREAD_MUTEX_INITIALIZER
  2. 调用初始化的函数 pthread_mutex_init()
1
2
3
4
5
6
//1、使用特定的宏
pthread_mutex_t myMutex = PTHREAD_MUTEX_INITIALIZER;

//2、调用初始化的函数
pthread_mutex_t myMutex;
pthread_mutex_init(&myMutex , NULL);

以上两种初始化方式是完全等价的,PTHREAD_MUTEX_INITIALIZER 宏和 pthread_mutex_init() 函数都定义在 <pthread.h> 头文件中,它们的主要区别在于:

  • pthread_mutex_init() 函数可以自定义互斥锁的属性(具体自定义的方法,这里不再进行讲解)。
  • 对于调用 malloc() 函数分配动态内存的互斥锁,只能以第 2 种方法完成初始化;

pthread_mutex_init() 函数专门用于初始化互斥锁,语法格式如下:

1
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);

mutex 参数表示要初始化的互斥锁;attr 参数用于自定义新建互斥锁的属性,attr 的值为 NULL 时表示以默认属性创建互斥锁。

pthread_mutex_init() 函数成功完成初始化操作时,返回数字 0;如果初始化失败,函数返回非零数。

注意,不能对一个已经初始化过的互斥锁再进行初始化操作,否则会导致程序出现无法预料的错误。

2) 互斥锁的“加锁”和“解锁”

对于互斥锁的“加锁”和“解锁”操作,常用的函数有以下 3 种:

1
2
3
int pthread_mutex_lock(pthread_mutex_t* mutex);   //实现加锁
int pthread_mutex_trylock(pthread_mutex_t* mutex); //实现加锁
int pthread_mutex_unlock(pthread_mutex_t* mutex); //实现解锁

参数 mutex 表示我们要操控的互斥锁。函数执行成功时返回数字 0,否则返回非零数。

pthread_mutex_unlock() 函数用于对指定互斥锁进行“解锁”操作,pthread_mutex_lock() 和 pthread_mutex_trylock() 函数都用于实现“加锁”操作,不同之处在于当互斥锁已经处于“加锁”状态时:

  • 执行 pthread_mutex_lock() 函数会使线程进入等待(阻塞)状态,直至互斥锁得到释放;

  • 执行 pthread_mutex_trylock() 函数不会阻塞线程,直接返回非零数(表示加锁失败)。

3) 互斥锁的销毁

对于使用动态内存创建的互斥锁,例如:

1
pthread_mutex_t myMutex = (pthread_mutex_t *)malloc(sizeof(pthread_mutex_t));pthread_mutex_init(&myMutex , NULL);

手动释放 myMutex 占用的内存(调用 free() 函数)之前,必须先调用 pthread_mutex_destory() 函数销毁该对象。

pthread_mutex_destory() 函数用于销毁创建好的互斥锁,语法格式如下:

1
int pthread_mutex_destroy(pthread_mutex_t *mutex);

参数 mutex 表示要销毁的互斥锁。如果函数成功销毁指定的互斥锁,返回数字 0,反之返回非零数。

对于这个例子,销毁操作为:

1
2
int res;
res = pthread_mutex_destroy(&myMutex);

注意,对于用 PTHREAD_MUTEX_INITIALIZER 或者 pthread_mutex_init() 函数直接初始化的互斥锁,无需调用 pthread_mutex_destory() 函数手动销毁。

只有malloc新建互斥锁对象的需要销魂

互斥锁的实际应用

接下来,我们使用互斥锁对《线程同步机制》一节中模拟“4 个售票员卖 10 张票”的程序进行改良,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#include<stdio.h>
#include<stdlib.h>
#include<pthread.h>
#include<unistd.h>
int ticket_sum = 10;
//创建互斥锁
pthread_mutex_t myMutex = PTHREAD_MUTEX_INITIALIZER;
//模拟售票员卖票
void *sell_ticket(void *arg) {
//输出当前执行函数的线程 ID
printf("当前线程ID:%u\n", pthread_self());
int i;
int islock = 0;
for (i = 0; i < 10; i++)
{
//当前线程“加锁”
islock = pthread_mutex_lock(&myMutex);
//如果“加锁”成功,执行如下代码
if (islock == 0) {
//如果票数 >0 ,开始卖票
if (ticket_sum > 0)
{
sleep(1);
printf("%u 卖第 %d 张票\n", pthread_self(), 10 - ticket_sum + 1);
ticket_sum--;
}
//当前线程模拟完卖票过程,执行“解锁”操作
pthread_mutex_unlock(&myMutex);
}
}
return 0;
}
int main() {
int flag;
int i;
void *ans;
//创建 4 个线程,模拟 4 个售票员
pthread_t tids[4];
for (i = 0; i < 4; i++)
{
flag = pthread_create(&tids[i], NULL, &sell_ticket, NULL);
if (flag != 0) {
printf("线程创建失败!");
return 0;
}
}
sleep(10); //等待 4 个线程执行完成
for (i = 0; i < 4; i++)
{
//阻塞主线程,确认 4 个线程执行完成
flag = pthread_join(tids[i], &ans);
if (flag != 0) {
printf("tid=%d 等待失败!", tids[i]);
return 0;
}
}
return 0;
}

假设程序编写在 thread.c 文件中,执行过程为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[root@localhost ~]# gcc thread.c -o thread.exe -lpthread
[root@localhost ~]# ./thread.exe
当前线程ID:149493504
当前线程ID:170473216
当前线程ID:159983360
当前线程ID:139003648
149493504 卖第 1 张票
149493504 卖第 2 张票
149493504 卖第 3 张票
139003648 卖第 4 张票
139003648 卖第 5 张票
139003648 卖第 6 张票
139003648 卖第 7 张票
139003648 卖第 8 张票
159983360 卖第 9 张票
159983360 卖第 10 张票

程序中共创建了 4 个线程,每个线程“开始卖票”前都会进行“加锁”操作(第 17 行),“卖票结束”后再执行“解锁”操作(第 28 行)。通过执行结果可以看到,互斥锁很好地解决了“线程间竞争资源”的问题,实现了线程同步。

chatgpt:C++互斥锁教程

chatgpt:C++互斥锁教程

在 C++ 中使用互斥锁(mutex)可以保证线程安全,避免竞态条件和数据竞争等问题。下面是一个简单的互斥锁使用示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
#include <thread>
#include <mutex>

std::mutex g_mutex; // 全局互斥锁

void print(int num)
{
g_mutex.lock(); // 上锁
std::cout << "Thread " << num << " is printing." << std::endl;
g_mutex.unlock(); // 解锁
}

int main()
{
std::thread t1(print, 1);
std::thread t2(print, 2);
t1.join();
t2.join();
return 0;
}

在这个例子中,我们定义了一个全局互斥锁 g_mutex,并在 print 函数中使用了它来保护输出操作。在 print 函数中,先调用 g_mutex.lock() 上锁,执行输出操作,最后调用 g_mutex.unlock() 解锁。

main 函数中,我们创建了两个线程 t1t2,分别调用 print 函数,并使用 join() 函数等待线程结束。

使用互斥锁时需要注意以下事项:

  1. 互斥锁的生命周期应该尽量短,以避免锁的持有时间过长,影响程序性能。
  2. 在需要共享数据的线程中,需要使用相同的互斥锁来保护共享数据。
  3. 在使用互斥锁时,要注意避免死锁和饥饿等问题。
  4. 可以使用 RAII 技术来管理互斥锁,以避免忘记解锁而导致的死锁问题。
  5. C++11 中提供了更加灵活和高效的锁,如 std::unique_lockstd::shared_lockstd::lock_guard 等,可以根据实际情况选择适合的锁类型。

这是互斥锁的简单介绍,如果需要更深入的了解,可以参考 C++ 的相关文档或教程。