一、互斥锁
1.互斥锁函数
Linux 中互斥锁的类型为 pthread_mutex_t
pthread_mutex_t mutex;
创建的锁对象保存当前锁的状态(打开还是锁定),如果是锁定状态,还记录着给这把锁加锁的线程信息(线程ID)。一个互斥锁变量只能被一个线程锁定,被锁定之后其他线程再对互斥锁变量加锁就会被阻塞,直到这把互斥锁被解锁,被阻塞的线程才能被解除阻塞。一般情况下,每一个共享资源对应一个把互斥锁,锁的个数和线程的个数无关。
// 初始化互斥锁
// restrict: 是一个关键字, 用来修饰指针, 只有这个关键字修饰的指针可以访问指向的内存地址, 其他指针是不行的
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr);
// 释放互斥锁资源
int pthread_mutex_destroy(pthread_mutex_t *mutex);
参数:
- mutex: 互斥锁变量的地址
- attr: 互斥锁的属性,一般使用默认属性即可,这个参数指定为 NULL
// 修改互斥锁的状态, 将其设定为锁定状态, 这个状态被写入到参数 mutex 中
int pthread_mutex_lock(pthread_mutex_t *mutex);
这个函数被调用,首先会判断参数 mutex 互斥锁中的状态是不是锁定状态:
- 没有被锁定,是打开的,这个线程可以加锁成功,这个这个锁中会记录是哪个线程加锁成功了
- 如果被锁定了,其他线程加锁就失败了,这些线程都会阻塞在这把锁上
- 当这把锁被解开之后,这些阻塞在锁上的线程就解除阻塞了,并且这些线程是通过竞争的方式对这把锁加锁,没抢到锁的线程继续阻塞
// 尝试加锁
int pthread_mutex_trylock(pthread_mutex_t *mutex);
调用这个函数对互斥锁变量加锁还是有两种情况:
- 如果这把锁没有被锁定是打开的,线程加锁成功
- 如果锁变量被锁住了,调用这个函数加锁的线程,不会被阻塞,加锁失败直接返回错误号
// 对互斥锁解锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);
不是所有的线程都可以对互斥锁解锁,哪个线程加的锁,哪个线程才能解锁成功。
2.互斥锁使用
#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 100
// 全局变量
int number;
// 创建一把互斥锁
// 全局变量, 多个线程共享
pthread_mutex_t mutex;
// 线程处理函数
void* funcA_num(void* arg)
{
for(int i=0; i<MAX; ++i)
{
// 如果线程A加锁成功, 不阻塞
// 如果B加锁成功, 线程A阻塞
pthread_mutex_lock(&mutex);
int cur = number;
cur++;
usleep(10);
number = cur;
pthread_mutex_unlock(&mutex);
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)
{
// a加锁成功, b线程访问这把锁的时候是锁定的
// 线程B先阻塞, a线程解锁之后阻塞解除
// 线程B加锁成功了
pthread_mutex_lock(&mutex);
int cur = number;
cur++;
number = cur;
pthread_mutex_unlock(&mutex);
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_mutex_init(&mutex, NULL);
// 创建两个子线程
pthread_create(&p1, NULL, funcA_num, NULL);
pthread_create(&p2, NULL, funcB_num, NULL);
// 阻塞,资源回收
pthread_join(p1, NULL);
pthread_join(p2, NULL);
// 销毁互斥锁
// 线程销毁之后, 再去释放互斥锁
pthread_mutex_destroy(&mutex);
return 0;
}
二、死锁
后果:所有的线程都被阻塞,并且线程的阻塞是无法解开的(因为可以解锁的线程也被阻塞了)。
造成死锁的场景:
- 加锁之后忘记解锁
- 重复加锁,造成死锁
void func()
{
for(int i=0; i<6; ++i)
{
// 当前线程A加锁成功
// 其余的线程阻塞
pthread_mutex_lock(&mutex);
// 锁被锁住了, A线程阻塞
pthread_mutex_lock(&mutex);
....
.....
pthread_mutex_unlock(&mutex);
}
}
// 隐藏的比较深的情况
void funcA()
{
for(int i=0; i<6; ++i)
{
// 当前线程A加锁成功
// 其余的线程阻塞
pthread_mutex_lock(&mutex);
....
.....
pthread_mutex_unlock(&mutex);
}
}
void funcB()
{
for(int i=0; i<6; ++i)
{
// 当前线程A加锁成功
// 其余的线程阻塞
pthread_mutex_lock(&mutex);
funcA(); // 重复加锁
....
.....
pthread_mutex_unlock(&mutex);
}
}
- 在程序中有多个共享资源,因此有很多把锁,随意加锁,导致相互被阻塞
场景描述:
- 有两个共享资源:X, Y,X对应锁A, Y对应锁B
- 线程A访问资源X, 加锁A
- 线程B访问资源Y, 加锁B- 线程A要访问资源Y, 线程B要访问资源X,因为资源X和Y已经被对应的锁锁住了,因此这个两个线程被阻塞
- 线程A被锁B阻塞了, 无法打开A锁
- 线程B被锁A阻塞了, 无法打开B锁
避免死锁:
- 避免多次锁定,多检查
- 对共享资源访问完毕之后,一定要解锁,或者在加锁的使用 trylock
- 如果程序中有多把锁,可以控制对锁的访问顺序 (顺序访问共享资源,但在有些情况下是做不到的),另外也可以在对其他互斥锁做加锁操作之前,先释放当前线程拥有的互斥锁。
- 项目程序中可以引入一些专门用于死锁检测的模块
三、读写锁
读写锁函数
当读线程比写线程多时用读写锁。
创建读写锁
//读写锁类型为:pthread_rwlock_t
pthread_rwlock_t rwlock;
之所以称其为读写锁,是因为这把锁既可以锁定读操作,也可以锁定写操作。为了方便理解,可以大致认为在这把锁中记录了这些信息:
- 锁的状态:锁定 / 打开
- 锁定的是什么操作:读操作 / 写操作,使用读写锁锁定了读操作,需要先解锁才能去锁定写操作,反之亦然。
- 哪个线程将这把锁锁上了
读写锁的使用方式也互斥锁的使用方式是完全相同的:找共享资源,确定临界区,在临界区的开始位置加锁(读锁 / 写锁),临界区的结束位置解锁。
读写锁特点:
- 使用读写锁的读锁锁定了临界区,线程对临界区的访问是并行的,读锁是共享的。
- 使用读写锁的写锁锁定了临界区,线程对临界区的访问是串行的,写锁是独占的。
- 使用读写锁分别对两个临界区加了读锁和写锁,两个线程要同时访问这两个临界区,访问写锁临界区的线程继续运行,访问读锁临界区的线程阻塞,因为写锁比读锁的优先级高。
如果说程序中所有的线程都对共享资源做写操作,使用读写锁没有优势,和互斥锁是一样的,如果说程序中所有的线程都对共享资源有写也有读操作,并且对共享资源读的操作越多,读写锁更有优势。
Linux 提供的读写锁操作函数:
#include <pthread.h>
pthread_rwlock_t rwlock;
// 初始化读写锁
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
const pthread_rwlockattr_t *restrict attr); //attr默认为NULL
// 释放读写锁占用的系统资源
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
// 在程序中对读写锁加读锁, 锁定的是读操作
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock)
//调用这个函数,如果读写锁是打开的,那么加锁成功;如果读写锁已经锁定了读操作,调用这个函数依然可以加锁成功,因为读锁是共享的;如果读写锁已经锁定了写操作,调用这个函数的线程会被阻塞。
// 这个函数可以有效的避免死锁
// 如果加读锁失败, 不会阻塞当前线程, 直接返回错误号
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
//1.调用这个函数,如果读写锁是打开的,那么加锁成功;
//2.如果读写锁已经锁定了读操作,调用这个函数依然可以加锁成功,因为读锁是共享的;
//3.如果读写锁已经锁定了写操作,调用这个函数加锁失败,对应的线程不会被阻塞,可以在程序中对函数返回值进行判断,添加加锁失败之后的处理动作。
// 在程序中对读写锁加写锁, 锁定的是写操作
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
//调用这个函数,如果读写锁是打开的,那么加锁成功;如果读写锁已经锁定了读操作或者锁定了写操作,调用这个函数的线程会被阻塞。
// 这个函数可以有效的避免死锁
// 如果加写锁失败, 不会阻塞当前线程, 直接返回错误号
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
// 解锁, 不管锁定了读还是写都可用解锁
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
四、信号量
信号量起通知作用,线程A在等待某件事,线程B完成了这件事后就可以给线程A发信号。
(可控制线程的执行顺序)
信号量API
- 初始化信号量
函数原型如下:
初始化信号量
#include <semaphore.h>
int sem_init(sem_t *sem,int pshared,unsigned int value);
该函数可以初始化一个信号量,第一个参数传入sem_t类型指针;
第二个参数传入0代表线程控制,否则为进程控制;
第三个参数表示信号量的初始值,0代表阻塞,1代表运行。
待初始化结束信号量后,若执行成功会返回0。
- 信号量P/V操作
函数原型如下:
信号量PV操作(阻塞)
#include <pthread.h>
int sem_wait(sem_t *sem);
int sem_post(sem_t *sem);
成功:返回0
sem_wait函数作用为检测指定信号量是否有资源可用,若无资源可用会阻塞等待,若有资源可用会自动的执行“sem-1”的操作。所谓的“sem-1”是与上述初始化函数中第三个参数值一致,成功执行会返回0。
sem_post函数会释放指定信号量的资源,执行“sem+1”操作。
通过以上2个函数可以完成所谓的PV操作,即信号量的申请与释放,完成对线程执行顺序的控制。
- 信号量申请(非阻塞方式)
函数原型如下:
信号量申请资源(非阻塞)
#include <pthread.h>
int sem_trywait(sem_t *sem);
成功:返回0
此函数是信号量申请资源的非阻塞函数,功能与sem_wait一致,唯一区别在于此函数为非阻塞。
- 信号量销毁
函数原型如下:
信号量销毁
#include <pthread.h>
int sem_destory(sem_t *sem);
成功:返回0
该函数为信号量销毁函数,执行过后可将信号量进行销毁。
示例代码:
1 #define _GNU_SOURCE
2 #include <pthread.h>
3 #include <stdio.h>
4 #include <unistd.h>
5 #include <errno.h>
6 #include <semaphore.h>
7
8 sem_t sem1,sem2,sem3;//申请的三个信号量变量
9
10 void *fun1(void *arg)
11 {
12 sem_wait(&sem1);//因sem1本身有资源,所以不被阻塞 获取后sem1-1 下次会会阻塞
13 printf("%s:Pthread Come!\n",__FUNCTION__);
14 sem_post(&sem2);// 使得sem2获取到资源
15 pthread_exit(NULL);
16 }
17
18 void *fun2(void *arg)
19 {
20 sem_wait(&sem2);//因sem2在初始化时无资源会被阻塞,直至14行代码执行 不被阻塞 sem2-1 下次会阻塞
21 printf("%s:Pthread Come!\n",__FUNCTION__);
22 sem_post(&sem3);// 使得sem3获取到资源
23 pthread_exit(NULL);
24 }
25
26 void *fun3(void *arg)
27 {
28 sem_wait(&sem3);//因sem3在初始化时无资源会被阻塞,直至22行代码执行 不被阻塞 sem3-1 下次会阻塞
29 printf("%s:Pthread Come!\n",__FUNCTION__);
30 sem_post(&sem1);// 使得sem1获取到资源
31 pthread_exit(NULL);
32 }
33
34 int main()
35 {
36 int ret;
37 pthread_t tid1,tid2,tid3;
38 ret = sem_init(&sem1,0,1); //初始化信号量1 并且赋予其资源
39 if(ret < 0){
40 perror("sem_init");
41 return -1;
42 }
43 ret = sem_init(&sem2,0,0); //初始化信号量2 让其阻塞
44 if(ret < 0){
45 perror("sem_init");
46 return -1;
47 }
48 ret = sem_init(&sem3,0,0); //初始化信号3 让其阻塞
49 if(ret < 0){
50 perror("sem_init");
51 return -1;
52 }
53 ret = pthread_create(&tid1,NULL,fun1,NULL);//创建线程1
54 if(ret != 0){
55 perror("pthread_create");
56 return -1;
57 }
58 ret = pthread_create(&tid2,NULL,fun2,NULL);//创建线程2
59 if(ret != 0){
60 perror("pthread_create");
61 return -1;
62 }
63 ret = pthread_create(&tid3,NULL,fun3,NULL);//创建线程3
64 if(ret != 0){
65 perror("pthread_create");
66 return -1;
67 }
68 /*回收线程资源*/
69 pthread_join(tid1,NULL);
70 pthread_join(tid2,NULL);
71 pthread_join(tid3,NULL);
72
73 /*销毁信号量*/
74 sem_destroy(&sem1);
75 sem_destroy(&sem2);
76 sem_destroy(&sem3);
77
78 return 0;
79 }
该例程加入了信号量,使得线程的执行顺序变为可控的。在初始化信号量时,将信号量1填入资源,第一个线程调用sem_wait函数可以成功获得信号量,在执行完逻辑后使用sem_pos函数来释放。当执行函数sem_wait后,会执行sem自减操作,使下一次竞争被阻塞,直至通过sem_pos被释放。
上述例程因38行初始化信号量1时候,使其默认获取到资源;第43、48行初始化信号量2、3时候,使之没有资源。于是在线程处理函数中,每个线程通过sem_wait函数来等待资源,发生阻塞。因信号量1初始值为有资源,故可以先执行线程1的逻辑。待执行完第12行sem_wait函数,会导致sem1-1,使得下一次此线程会被阻塞。继而执行至14行,通过sem_post函数使sem2信号量获取资源,从而冲破阻塞执行线程2的逻辑…以此类推完成线程的有序控制。
五、条件变量
条件变量时一种同步机制,用来通知其他线程条件满足了。一般是用来通知对方共享数据的状态信息,因此条件变量时结合互斥量来使用的。
条件变量API
- 创建和销毁条件变量
函数原型如下:
#include <pthread.h>
// 初始化条件变量
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr);//cond_attr通常为NULL
// 销毁条件变量
int pthread_cond_destroy(pthread_cond_t *cond);
这些函数成功时都返回0
- 等待条件变量
函数原型如下:
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
这需要结合互斥量一起使用,示例代码如下:
pthread_mutex_lock(&g_tMutex);
pthread_cond_wait(&g_tConVar, &g_tMutex); // 如果条件不满足则,会unlock g_tMutex
// 条件满足后被唤醒,会lock g_tMutex
/* 操作临界资源 */
pthread_mutex_unlock(&g_tMutex);
- 通知条件变量
函数原型如下:
int pthread_cond_signal(pthread_cond_t *cond);
pthread_cond_signal函数只会唤醒一个等待cond条件变量的线程,示例代码如下:
pthread_cond_signal(&g_tConVar);
示例代码:
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <semaphore.h>
#include <string.h>
static char g_buf[1000];
static pthread_mutex_t g_tMutex = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t g_tConVar = PTHREAD_COND_INITIALIZER;
static void *my_thread_func (void *data)
{
while (1)
{
//sleep(1);
/* 等待通知 */
//while (g_hasData == 0);
pthread_mutex_lock(&g_tMutex);
pthread_cond_wait(&g_tConVar, &g_tMutex);
/* 打印 */
printf("recv: %s\n", g_buf);
pthread_mutex_unlock(&g_tMutex);
}
return NULL;
}
int main(int argc, char **argv)
{
pthread_t tid;
int ret;
char buf[1000];
/* 1. 创建"接收线程" */
ret = pthread_create(&tid, NULL, my_thread_func, NULL);
if (ret)
{
printf("pthread_create err!\n");
return -1;
}
/* 2. 主线程读取标准输入, 发给"接收线程" */
while (1)
{
fgets(buf, 1000, stdin);
pthread_mutex_lock(&g_tMutex);
memcpy(g_buf, buf, 1000);
pthread_cond_signal(&g_tConVar); /* 通知接收线程 */
pthread_mutex_unlock(&g_tMutex);
}
return 0;
}