在Linux下使用多线程编程,主要是使用glibc库中的接口
线程基础
1.线程概述
- 线程是轻量级的进程(称作LWP)。进程是资源分配的最小单位,线程是OS调度执行的单位。
- 线程和进程的区别:
- 每个进程拥有自己独立的地址空间,而多个线程可以共用一个地址空间,代码段、堆区、全局数据区、打开的文件(内核中管理的文件描述符表)都是线程共享的
- 在一个地址空间中多个线程独享:每个线程都有自己的栈区,寄存器(在内核中管理的)。
- 线程是程序的最小执行单位,进程是操作系统中最小的资源分配单位
- CPU的调度和切换:线程的上下文切换比进程快得多
2.线程的创建
- 线程的创建:
- 函数原型如下:
#include <pthread.h> int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg); 函数参数: thread:传出参数,线程创建成功,会将线程id写入到这个指针指向的内存中 attr:线程的属性 start_routine:函数指针,表示线程的处理动作 arg:作为实参传递到start_routine函数指针指向的函数 返回值:线程创建成功返回0,失败则返回对应的错误号
- 线程创建示例
#include <pthread.h> #include <stdio.h> #include <unistd.h> void* func(void* arg) { printf("子线程id:%ld\n", pthread_self()); for (int i = 0; i < 5; i++) { printf("child:%d\n", i); } return NULL; } int main() { pthread_t tid; pthread_create(&tid, NULL, func, NULL); printf("子线程id:%ld,主线程id:%ld\n", tid, pthread_self()); for (int i = 0; i < 5; i++) { printf("main:%d\n", i); } // 休眠一秒钟,让出CPU时间片 // 防止主线程退出,导致虚拟地址空间被释放,子线程就一并被销毁 sleep(1); return 0; }
- 如果将类方法作为新创建线程的执行函数,只能是静态方法,而不能是非静态方法。因为非静态方法在由编译器编译后,第一个参数是一个指向类类型的this指针不符合pthread_create第三个参数的声明。问题来了,只能传递静态成员方法,由于静态成员方法没有this指针,如何调用类中的实例方法呢?答案是创建线程时,传递类的实例对象作为线程执行函数的参数。示例如下:
#include <stdio.h>
#include <pthread.h>
class Test {
public:
static void* threadFunc(void*);
void other() {
printf("other work\n");
}
};
void* Test::threadFunc(void* arg) {
Test* ptr = (Test*)arg;
ptr->other();
pthread_exit(NULL);
}
int main() {
Test test;
pthread_t tid;
pthread_create(&tid, nullptr, Test::threadFunc, &test);
pthread_join(tid, nullptr);
return 0;
}
3.线程的退出
- 在编写多线程程序的时候,如果想让线程退出但是不会导致虚拟地址空间的释放,就可以调用线程库中的线程退出函数。只要调用该函数,调用函数的当前线程就会马上退出,并且不会影响到其他线程的执行。
- 函数原型如下:
#include <pthread.h> void pthread_exit(void *retval);
- 使用示例:
#include <pthread.h> #include <stdio.h> #include <unistd.h> void* func(void* arg) { printf("子线程id:%ld\n", pthread_self()); for (int i = 0; i < 100; i++) { printf("child:%d\n", i); sleep(1); } return NULL; } int main() { pthread_t tid; pthread_create(&tid, NULL, func, NULL); printf("子线程id:%ld,主线程id:%ld\n", tid, pthread_self()); for (int i = 0; i < 5; i++) { printf("main:%d\n", i); } // 主线程调用线程退出函数退出,虚拟地址空间不会释放 // 不会影响到子线程的执行 pthread_exit(NULL); return 0; }
4.线程的回收
- 线程和进程一样,子线程退出的时候其内核资源主要由主线程回收,线程库中提供的线程回收函叫做pthread_join(),这个函数是一个阻塞函数,如果还有子线程在运行,调用该函数就会阻塞,子线程退出,pthread_join函数解除阻塞进行资源的回收,函数被调用一次,只能回收一个子线程,如果有多个子线程则需要循环进行回收。另外通过线程回收函数还可以获取到子线程退出时传递出来的数据。
- 函数原型如下:
#include <pthread.h> // 这是一个阻塞函数, 子线程在运行这个函数就阻塞 // 子线程退出, 函数解除阻塞, 回收对应的子线程资源, 类似于回收进程使用的函数 wait() int pthread_join(pthread_t thread, void **retval); 函数参数: thread:要被回收的子线程的线程id retval:是一个传出参数,这个二级指针指向一级指针的地址, 地址存储了子线程调用线程退出函数pthread_exit的退出状态 返回值 线程回收成功返回0,失败则返回错误号
- 线程之间的数据传递:在子线程退出的时候调用
pthread_exit()
函数,将返回值通过参数传递出去。这样主线程在回收这个子线程的时候可以通过phread_join()的第二个参数来接收子线程传递的返回值。主线程接受子线程传递的数据有很多种处理方式,下面来列举几种- 使用全局变量或者静态变量或者堆分配内存的变量。位于同一虚拟地址空间中的线程,虽然不能够共享栈区数据,但是可以共享数据段和堆区数据。
#include <pthread.h> #include <unistd.h> #include <stdio.h> #include <string.h> #include <stdlib.h> struct Data { int age; char name[32]; }; // struct Data data; void* func(void* arg) { printf("子线程id:%ld\n", pthread_self()); // static struct Data data; // data.age = 23; // strcpy(data.name, "NrvCer"); // pthread_exit(&data); struct Data* data = (struct Data*)malloc(sizeof(struct Data)); data->age = 23; strcpy(data->name, "NrvCer"); pthread_exit(data); } int main() { pthread_t tid; pthread_create(&tid, NULL, func, NULL); // 主线程获取子线程返回的数据 void* ptr = NULL; // 阻塞等待子线程退出 pthread_join(tid, &ptr); struct Data* data = (struct Data*) ptr; printf("%d,%s,%ld\n", data->age, data->name, pthread_self()); if (data) free(data); return 0; }
- 使用主线程栈:如果主线程是最后退出的,则可以将子线程返回的数据保存到主线程的栈区内存中。
#include <pthread.h> #include <unistd.h> #include <stdio.h> #include <string.h> struct Data { int age; char name[32]; }; void* func(void* arg) { printf("子线程id:%ld\n", pthread_self()); struct Data* data = (struct Data*)arg; data->age = 23; strcpy(data->name, "NrvCer"); pthread_exit(data); } int main() { pthread_t tid; struct Data data; pthread_create(&tid, NULL, func, &data); void* ptr; pthread_join(tid, &ptr); struct Data* recvData= (struct Data*) ptr; printf("%d,%s,%ld\n", recvData->age, recvData->name, pthread_self()); printf("%d,%s,%ld\n", data.age, data.name, pthread_self()); return 0; }
5.线程分离
- 在某些情况下,程序中的主线程有属于自己的业务处理流程,如果让主线程负责子线程的资源回收,调用pthread_join()只要子线程不退出主线程就会一直被阻塞,主线程的业务就不能执行了。
- 线程分离函数pthread_detach,调用这个函数之后指定的子线程就可以和主线程分离,当子线程退出的时候,其占用的内核资源就被系统的其他进程接管并回收了。线程分离之后在主线程中使用pthread_join()就回收不到子线程资源了。
- 函数原型如下:
#include <pthread.h> // 参数为子线程的线程ID int pthread_detach(pthread_t thread);
- 使用示例:
#include <pthread.h> #include <stdio.h> #include <unistd.h> void* func(void* arg) { for (int i = 0; i < 100; i++) { printf("child:%ld,%d\n", pthread_self(), i); sleep(1); } return NULL; } int main() { pthread_t tid; pthread_create(&tid, NULL, func, NULL); // 设置主线程和子线程分离 pthread_detach(tid); for (int i = 0; i < 5; i++) printf("main:%ld,%d\n", pthread_self(), i); // 主线程退出,进程的虚拟地址空间不会释放 pthread_exit(NULL); return 0; }
6.其他线程函数
- 线程id获取:
- 使用
pthread_t pthread_self()
函数 - 使用
pthread_create
函数创建线程时会将线程id传出 - 使用
syscall
库函数,方式3和方式12的区别是,方式12获取的线程id可能不是系统唯一的,而方式3获取的线程id在系统范围内全局唯一。
#include <unistd.h> #include <sys/syscall.h> #include <stdio.h> #include <pthread.h> int main(int argc, char *argv[]) { pid_t tid; tid = syscall(SYS_gettid); // 139808617809728 126139 printf("%ld\t%d\n", pthread_self(), tid); }
- 使用
- 线程id比较:使用
pthread_equal
函数
线程同步
1.线程同步的概念
- 线程同步:多个线程按照先后顺序依次对共享内存区域中的共享资源进行访问。所谓的共享资源就是多个线程可以共同访问的变量,这些变量通常为数据段上的内容或者堆区变量,这些变量对应的共享资源称之为临界资源。
- 在Linux上线程同步方式有以下几种,如下所示:
- 互斥锁:mutex,这是两个单词的全称,Mutual Exclusion(相互排斥)
- 读写锁
- 条件变量
- 信号量
2.互斥锁
- 互斥锁是线程同步最常用的一种方式,通过互斥锁可以锁定一个代码块,被锁定的这个代码块,所有的线程只能顺序执行,而不能并行执行。
- Linux中互斥锁的类型为pthread_mutex_t,创建一个这种类型的变量就得到了一把互斥锁
- 在创建的锁对象中保存了当前这把锁的状态信息:锁定还是打开,如果是锁定状态还记录了给这把锁加锁的线程信息(线程ID)。一个互斥锁变量只能被一个线程锁定,被锁定之后其他线程再对互斥锁变量加锁就会被阻塞,直到这把互斥锁被解锁,被阻塞的线程抢到锁后才能被解除阻塞。一般情况下,每一个共享资源对应一把互斥锁,锁的个数和线程的个数无关。
- 互斥锁的种类:有非递归锁、递归锁(又称可重入锁)、检错锁。对于递归锁,同一个线程可以对已经持有的锁重复加锁,这个重复加锁类似于引用计数,加锁次数需要和解锁次数相等。
- Linux下提供的互斥锁操作函数如下:
- 初始化互斥锁
1. 方式1:使用pthread_mutex_init函数 pthread_mutex_init (pthread_mutex_t *__mutex, const pthread_mutexattr_t *__mutexattr) 函数参数: mutex:互斥锁变量的地址 attr:互斥锁的属性,通常使用默认属性,这个参数指定为NULL。表示初始化非递归锁 如果需要初始化递归锁,则需要使用pthread_mutexattr_settype函数设置互斥锁的属性 返回值: 函数调用成功会返回0 调用失败会返回相应的错误号 2. 方式2:使用这种方式初始化的互斥锁无需销毁 使用PTHREAD_MUTEX_INITIALIZER 初始化非递归锁 使用PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP初始化递归锁
- 释放互斥锁的资源
int pthread_mutex_destroy (pthread_mutex_t *__mutex)
- 修改互斥锁的状态,将其设定为锁定状态
int pthread_mutex_lock (pthread_mutex_t *__mutex)
- 尝试加锁
int pthread_mutex_trylock (pthread_mutex_t *__mutex) 函数返回值: 如果锁没有被锁定,状态是打开的,则线程加锁成功 如果锁变量被锁住了,调用这个函数加锁的线程,不会被阻塞,而是加锁失败返回错误号
- 对互斥量解锁
int pthread_mutex_unlock (pthread_mutex_t *__mutex)
- 互斥锁的属性:
- 初始化互斥锁属性对象:int pthread_mutexattr_init(pthread_mutexattr_t *attr);
- 销毁互斥锁属性对象:int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);
- 设置互斥锁的属性:
int pthread_mutexattr_settype(pthread_mutexattr_t* attr, int type); type的取值: PTHREAD_MUTEX_NORMAL:普通互斥锁,创建互斥锁默认就是这个 PTHREAD_MUTEX_ERRORCHECK:检错锁,其他或者当前线程再次加锁会返回EDEADLOCK错误 PTHREAD_MUTEX_RECURSIVE:递归锁,允许同一个线程对持有的锁进行重复加锁
- 获取互斥锁的属性:
int pthread_mutexattr_gettype(const pthread_mutexattr_t* restrict attr, int* restrict type);
- 使用示例:四个售货员,售卖10张票。一个售货员就把票卖完了hhh。。
#include <pthread.h>
#include <unistd.h>
#include <stdio.h>
// 创建一把互斥锁
// 一个全局变量,多个线程共享
pthread_mutex_t mutex;
int ticket = 10;
void* sellTicket(void* arg) {
for (int i = 0; i < 10; i++) {
pthread_mutex_lock(&mutex);
if (ticket > 0) {
sleep(2);
printf("线程id:%ld,售出第%d张票\n", pthread_self(), 10 - ticket + 1);
ticket--;
}
pthread_mutex_unlock(&mutex);
}
pthread_exit(NULL);
}
int main() {
// 初始化互斥锁
pthread_mutex_init(&mutex, NULL);
pthread_t threads[4];
for (int i = 0; i < 4; i++) {
int flag = pthread_create(&threads[i], NULL, sellTicket, NULL);
if (flag != 0) { perror("线程创建失败!\n"); return 0; }
}
// 保证四个线程创建完成,即四个售票员
sleep(10);
for (int i = 0; i < 4; i++) {
int flag = pthread_join(threads[i], NULL);
if (flag != 0) { perror("等待失败!\n"); return 0; }
}
// 销毁互斥锁资源
pthread_mutex_destroy(&mutex);
return 0;
}
3.读写锁
- 读写锁是互斥锁的升级,在做读操作的时候可以提高程序的执行效率。如果所有的线程都是做读操作,那么读是并行的。但是使用互斥锁,读操作是和写操作一样都是串行的。Linux中的读写锁和互斥锁一样,也是一把锁,读写锁的类型为pthread_rwlock_t
- 读写锁的特点:
- 使用读写锁的读锁锁定了临界区,则线程对临界区的访问是并行的,因为读锁是共享的。
- 使用读写锁的写锁锁定了临界区,则线程对临界区的访问是串行的,因为写锁是独占的。
- 使用读写锁分别对两个临界区加了读锁和写锁,两个线程要同时访问这两个临界区,访问写锁临界区的线程继续运行,访问读锁临界区的线程阻塞
- 读写锁的使用场景:如果说程序中所有的线程都对共享资源有写也有读操作,并且对共享资源的读操作更多,则使用读写锁更有优势。
- 读写锁的相关操作函数:
- 读写锁的初始化
// 方式1 int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr); // 方式2 使用PTHREAD_RWLOCK_INITIALIZER;
- 释放读写锁占用的系统资源
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
- 在程序中对读写锁加读锁,锁定的是读操作
int pthread_rwlock_rdlock (pthread_rwlock_t *rwlock)
- 尝试加读锁,如果加读锁失败,不会阻塞当前线程,直接返回错误号
int pthread_rwlock_tryrdlock (pthread_rwlock_t *rwlock)
- 对读写锁加写锁,锁定的是写操作
int pthread_rwlock_wrlock (pthread_rwlock_t *rwlock)
- 尝试加写锁,如果加写锁失败,不会阻塞当前线程,直接返回错误号
int pthread_rwlock_trywrlock (pthread_rwlock_t *rwlock)
- 解锁。不管锁定了读还是写,都可以解锁
int pthread_rwlock_unlock (pthread_rwlock_t *rwlock)
- 读写锁类型的设置
int pthread_rwlockattr_setkind_np(pthread_rwlockattr_t* attr, int pref); 函数参数: attr:由int pthread_rwlockattr_init函数初始化的属性对象 pref:设置读写锁的类型,其值有如下几种 //读者优先(同时请求读锁和写锁时,请求读锁的线程优先获得锁) PTHREAD_RWLOCK_PREFER_READER_NP, // 默认 //读者优先 PTHREAD_RWLOCK_PREFER_WRITER_NP, //写者优先(同时请求读锁和写锁时,请求写锁的线程优先获得锁) PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP int pthread_rwlockattr_getkind_np(const pthread_rwlockattr_t* attr, int* pref); // 设置写锁优先示例 pthread_rwlockattr_t attr; pthread_rwlockattr_init(&attr); //设置成请求写锁优先 pthread_rwlockattr_setkind_np(&attr, PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP); pthread_rwlock_init(&myrwlock, &attr);
- 读写锁的使用示例:8个线程操作同一个全局变量,3个线程不定时写同一全局资源,5 个线程不定时读同一全局资源。
#include <pthread.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int number = 0;
// 定义读写锁
pthread_rwlock_t rwlock;
void* writeNum(void* arg) {
while(1) {
pthread_rwlock_wrlock(&rwlock);
int cur = number;
cur ++;
number = cur;
printf("++写操作完毕, number : %d, tid = %ld\n", number, pthread_self());
pthread_rwlock_unlock(&rwlock);
usleep(rand() % 100);
}
}
void* readNum(void* arg) {
while(1) {
pthread_rwlock_rdlock(&rwlock);
printf("--全局变量number = %d, tid = %ld\n", number, pthread_self());
pthread_rwlock_unlock(&rwlock);
usleep(rand() % 100);
}
}
int main() {
// 初始化读写锁
pthread_rwlock_init(&rwlock, NULL);
pthread_t wthreads[5], rthreads[3];
for (int i = 0; i < 5; i++) {
pthread_create(&wthreads[i], NULL, writeNum, NULL);
}
for (int i = 0; i < 3; i++) {
pthread_create(&rthreads[i], NULL, readNum, NULL);
}
for (int i = 0; i < 5; i++) {
pthread_join(wthreads[i], NULL);
}
for (int i = 0; i < 3; i++) {
pthread_join(rthreads[i], NULL);
}
// 释放读写锁的资源
pthread_rwlock_destroy(&rwlock);
return 0;
}
4.条件变量
- 条件变量:在多线程程序中只使用条件变量无法实现线程的同步,必须要配合互斥锁来使用。条件变量的主要作用不是处理线程同步,而是进行线程的阻塞。条件变量只有在满足指定条件下才会阻塞线程。
- 条件变量的类型为:pthread_cond_t
- 条件变量的相关操作函数如下:
- 初始化条件变量
方式1: int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr); 方式2: 使用PTHREAD_COND_INITIALIZER
- 销毁条件变量
int pthread_cond_destroy (pthread_cond_t *cond)
- 条件变量等待被唤醒:当条件不满足,wait系列函数将会阻塞调用线程,并且释放持有的互斥锁。因此在调用wait系列函数前会有一个上锁操作;同理,当收到条件信号,当前阻塞的线程被唤醒,pthread_cond_wait调用将会返回并对互斥锁进行上锁。因此在调用wait系列函数后会有一个解锁操作。
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex); 函数参数: mutex:在阻塞线程时候,如果线程已经对互斥锁 mutex 上锁,那么会将这把锁打开,这样做是为了避免死锁 // 将线程阻塞一定的时间长度, 时间到达之后, 线程就解除阻塞了 int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);
- 唤醒阻塞在条件变量上的线程
// 至少有一个被解除阻塞 int pthread_cond_signal (pthread_cond_t *cond) // 被阻塞的线程全部解除阻塞 int pthread_cond_broadcast(pthread_cond_t *cond);
- 虚假唤醒问题:pthread_cond_signal或者pthread_cond_broadcast唤醒多个wait的线程。产生虚假唤醒的一个原因是一个原因是wait系列函数是futex系统调用,属于阻塞型的系统调用,当系统调用被信号中断的时候,会返回-1,并且把errno置为EINTR。很多这种系统调用为了防止被信号中断都会重启系统调用,但是在wait系列函数返回之后到重新调用之前,条件信号已经到达,一旦错失可能导致wait系列调用永久阻塞下去。因此为了避免这种情况,宁可虚假唤醒,也不能再次调用wait系列函数,以免wait系列调用永久阻塞下去。
- 示例:使用条件变量结合互斥锁实现生产者、消费者模型,其中五个生产者,五个消费者
#include <pthread.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> // 链表的结点 typedef struct Node { int number; struct Node* next; }Node; // 指向头节点的指针 struct Node* head = NULL; // 条件变量 pthread_cond_t cond; // 互斥锁 pthread_mutex_t mutex; void* producer(void* arg) { while (1) { pthread_mutex_lock(&mutex); Node* node = (Node*)malloc(sizeof(Node)); node->number = rand() % 1000; node->next = head; head = node; printf("生产者线程id:%ld,number值:%d\n", pthread_self(), node->number); pthread_mutex_unlock(&mutex); // 通知所有消费者消费 pthread_cond_broadcast(&cond); // 生产慢一点 sleep(rand() % 3); } } void* consumer(void* arg) { while (1) { pthread_mutex_lock(&mutex); // 使用while而不用if while (head == NULL) { pthread_cond_wait(&cond, &mutex); } Node* node = head; head = node->next; printf("消费者线程id:%ld,number值:%d\n", pthread_self(), node->number); if (node) free(node); pthread_mutex_unlock(&mutex); sleep(rand() % 3); } } int main() { pthread_cond_init(&cond, NULL); pthread_mutex_init(&mutex, NULL); pthread_t proThreads[5], conThreads[5]; for (int i = 0; i < 5; i++) { pthread_create(&proThreads[i], NULL, producer, NULL); pthread_create(&conThreads[i], NULL, consumer, NULL); } for (int i = 0; i < 5; i++) { pthread_join(proThreads[i], NULL); pthread_join(conThreads[i], NULL); } pthread_cond_destroy(&cond); pthread_mutex_destroy(&mutex); return 0; }
- 上例中的生产者消费者模型中,如果在consumer函数中使用if会出现虚假唤醒问题:使用pthread_cond_broadcast唤醒了不该醒过来的消费者线程,因为执行的是if判断,不用继续判断任务队列是否为空,所以进行消费,此时可能由于任务队列已经为空,所以将产生运行时的段错误。 而使用while进行判断,后续的消费者线程还会进行任务队列是否为空的判断。
- 为什么条件变量需要和互斥锁结合?因为释放互斥锁和条件变量等待唤醒必须是一个原子操作。如下所示,假设不是原子操作,线程A执行完第三行代码后CPU时间片被剥夺,此时另外一个线程B获得该互斥锁m,然后发送条件信号通知等待此条件信号的线程A。等线程A重新获得时间片后,由于该信号已经被错过了,这样可能会导致线程A在第6行pthread_cond_wait(&cv);无限阻塞下去。在pthread_cond_wait相关函数实现中就将代码3和6执行的功能放在一个原子操作中。
//m的类型是已初始化的互斥锁,cv是条件变量
1 pthread_mutex_lock(&m)
2 while(condition_is_false) {
3 pthread_mutex_unlock(&m);
4 //解锁之后,等待之前,可能条件已经满足5,信号已经发出,但是该信号可能会被错过
6 pthread_cond_wait(&cv);
7 pthread_mutex_lock(&m);
8 }
- 条件变量信号丢失的问题:A线程通过调用pthread_cond_signal或者pthread_cond_broadcast唤醒阻塞在条件变量上的其他线程时,如果没有相关的线程调用wait系列函数捕捉该信号,则该信号就丢失了,再次调用pthread_cond_wait函数将阻塞。
5.信号量(信号灯)
- 信号量(信号灯)与互斥锁和条件变量的主要不同在于”灯”的概念,灯亮则意味着资源可用,灯灭则意味着不可用。信号量主要阻塞线程,不能完全保证线程安全,如果要保证线程安全,需要信号量和互斥锁一起使用。
- 信号的类型:sem_t
- 信号量操作函数:
- 信号量的初始化:
int sem_init (sem_t *__sem, int __pshared, unsigned int __value) 函数参数: sem:信号量变量地址 pshared: 0表示在同一个进程的线程之间共享 非0表示在多个进程之间共享 value:初始化当前信号量拥有的资源数,如果资源数为0,线程就被阻塞了。
- sem 中的资源数>0,线程不会阻塞,线程会占用sem中的一个资源,因此资源数- 1,直到 sem 中的资源数减为0时,资源被耗尽,因此线程也就被阻塞
int sem_wait(sem_t *sem);
- 和sem_wait函数一样,但是当sem中的资源数减为0时,资源被耗尽,线程不会被阻塞,直接返回错误号EAGAIN。
int sem_trywait(sem_t *sem);
- 和sem_wait函数一样,但是当资源数为0时,线程阻塞,在阻塞abs_timeout对应的时长之后,解除阻塞,超时返回。
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
- 给sem中的资源数+1
// 调用这个函数将使得给sem中的资源数加一 // 因为调用sem_wait、sem_trywait、sem_timedwait处于阻塞的线程将解除阻塞。 int sem_post(sem_t *sem);
- 查看信号量sem中的整型数的当前值
int sem_getvalue(sem_t *sem, int *sval); 函数参数: sval:传出参数
- 销毁信号量:
int sem_destroy(sem_t *sem);
- 使用信号量实现生产者消费者模型
- 信号量总资源数为1的情况:不会出现生产者线程和消费者线程同时访问共享资源的情况,不管生产者和消费者线程有多少个,它们都是顺序执行的。
#include <pthread.h> #include <semaphore.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> // for sleep typedef struct Node { int number; struct Node* next; }Node; // 指向头节点的指针 Node* head = NULL; // 生产者线程信号量 sem_t proSem; // 消费者线程信号量 sem_t conSem; void* producer(void* arg) { while (1) { sem_wait(&proSem); Node* node = (Node*)malloc(sizeof(Node)); node->next = head; node->number = rand() % 1000; printf("生产者线程id:%ld,number的值:%d\n", pthread_self(), node->number); head = node; // 通知消费者线程消费,给消费者增加信号灯 sem_post(&conSem); // 生产慢一点 sleep(rand() % 3); } } void* consumer(void* arg) { while (1) { sem_wait(&conSem); Node* node = head; printf("消费者线程id:%ld,number的值:%d\n", pthread_self(), node->number); head = node->next; free(node); // 通知生产者生产,给生产者增加信号灯 sem_post(&proSem); sleep(rand() % 3); } } int main() { sem_init(&proSem, 0, 1); sem_init(&conSem, 0, 0); pthread_t proThreads[5]; pthread_t conThreads[5]; for (int i = 0; i < 5; i++) { pthread_create(&proThreads[i], NULL, producer, NULL); pthread_create(&conThreads[i], NULL, consumer, NULL); } for (int i = 0; i < 5; i++) { pthread_join(proThreads[i], NULL); pthread_join(conThreads[i], NULL); } sem_destroy(&proSem); sem_destroy(&conSem); return 0; }
- 总资源数大于1:可能会出现多个线程访问共享资源的情况,如果想防止共享资源出现数据混乱,那么就需要使用互斥锁进行同步
#include <pthread.h> #include <semaphore.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> // for sleep typedef struct Node { int number; struct Node* next; }Node; // 指向头节点的指针 Node* head = NULL; // 生产者线程信号量 sem_t proSem; // 消费者线程信号量 sem_t conSem; // 互斥锁 pthread_mutex_t mutex; void* producer(void* arg) { while (1) { // 注意下面两行代码的调用次序,防止死锁 sem_wait(&proSem); pthread_mutex_lock(&mutex); Node* node = (Node*)malloc(sizeof(Node)); node->next = head; node->number = rand() % 1000; printf("生产者线程id:%ld,number的值:%d\n", pthread_self(), node->number); head = node; // 通知消费者线程消费,给消费者增加信号灯 sem_post(&conSem); pthread_mutex_unlock(&mutex); // 生产慢一点 sleep(rand() % 3); } } void* consumer(void* arg) { while (1) { // 注意下面两行代码的调用次序,防止死锁 sem_wait(&conSem); pthread_mutex_lock(&mutex); Node* node = head; printf("消费者线程id:%ld,number的值:%d\n", pthread_self(), node->number); head = node->next; if (node) free(node); // 通知生产者生产,给生产者增加信号灯 sem_post(&proSem); pthread_mutex_unlock(&mutex); sleep(rand() % 3); } } int main() { pthread_mutex_init(&mutex, NULL); sem_init(&proSem, 0, 5); sem_init(&conSem, 0, 0); pthread_t proThreads[5]; pthread_t conThreads[5]; for (int i = 0; i < 5; i++) { pthread_create(&proThreads[i], NULL, producer, NULL); pthread_create(&conThreads[i], NULL, consumer, NULL); } for (int i = 0; i < 5; i++) { pthread_join(proThreads[i], NULL); pthread_join(conThreads[i], NULL); } sem_destroy(&proSem); sem_destroy(&conSem); pthread_mutex_destroy(&mutex); return 0; }
- 上个程序中,注意代码的调用次序,否则可能产生死锁。考虑这么一种情况:某个消费者线程先运行,对互斥锁枷锁成功,然后调用sem_wait(),由于没有资源将被阻塞。而后生产者和其余的消费者进行加锁也将被阻塞。
线程局部存储
为了数据操作的安全,多个线程访问位于共享内存区域的共享资源,需要线程同步技术。那么如果各个线程将数据置于线程局部存储区又当如何呢?
- 线程局部存储(Thread Local Storage,TLS):对于一个包含多个线程的进程来说,每个线程可以操作自己的这份数据,这样的数据称之为线程局部存储,对应的存储区域叫做线程局部存储区。
- 方式1:Linux提供了一套函数接口来实现线程局部存储
- 函数接口
// 创建键,所有线程都可以通过键获取各自线程局部存储区上的数据 int pthread_key_create(pthread_key_t* key, void (*destructor)(void*)); 函数参数: key:这个key应当是所有线程都可以访问到的 destructor:线程终止时,如果key关联的值不是NULL,那么会自动执行定义的destructor 函数 // 通过键删除数据 int pthread_key_delete(pthread_key_t key); // 通过键值设置数据 int pthread_setspecific(pthread_key_t key, const void* value); // 通过键获取数据 void* pthread_getspecific(pthread_key_t key);
- Linux gcc编译器提供了一个关键字
__thread
定义线程局部变量。
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
// 定义线程局部变量
__thread int g_numer;
void* read(void* arg) {
while (1) {
printf("g_numer:%d, tid:%ld\n", g_numer, pthread_self());
sleep(1);
}
}
void* write(void* arg) {
while (1) {
g_numer++;
printf("g_numer:%d, tid:%ld\n", g_numer, pthread_self());
sleep(1);
}
}
int main() {
pthread_t tids[2];
pthread_create(&tids[0], nullptr, read, nullptr);
pthread_create(&tids[1], nullptr, write, nullptr);
for (int i = 0; i < 2; ++i) {
pthread_join(tids[i], nullptr);
}
return 0;
}
死锁
- 死锁:当多个线程访问共享资源,需要加锁,如果锁使用不当,就会造成死锁这种现象。如果线程死锁造成的后果是:所有的线程都被阻塞,并且线程的阻塞是无法解开的
- 造成死锁的场景如下:
- 加锁之后忘记解锁
#include <pthread.h> #include <stdio.h> pthread_mutex_t mutex; void* func(void* arg) { // 上锁但是没有解锁,则下一个抢到CPU时间片的线程将阻塞在这 pthread_mutex_lock(&mutex); } int main() { pthread_mutex_init(&mutex, NULL); pthread_t threads[2]; for (int i = 0; i < 2; i++) { pthread_create(&threads[i], NULL, func, NULL); } for (int i = 0; i < 2; i++) { pthread_join(threads[i], NULL); } pthread_mutex_destroy(&mutex); return 0; }
- 重复加非递归锁,造成死锁
void* func(void* arg) { pthread_mutex_lock(&mutex); // 重复加锁 pthread_mutex_lock(&mutex); pthread_mutex_unlock(&mutex); }
- 在程序中有多个共享资源,因此存在很多把锁。有的时候锁的次序推进不当将导致互相被阻塞。
#include <pthread.h> #include <stdio.h> #include <unistd.h> pthread_mutex_t mutexA = PTHREAD_MUTEX_INITIALIZER; pthread_mutex_t mutexB = PTHREAD_MUTEX_INITIALIZER; void* funcA(void* arg) { pthread_mutex_lock(&mutexA); // 保证线程t2获得锁mutexB sleep(1); pthread_mutex_lock(&mutexB); pthread_mutex_unlock(&mutexB); pthread_mutex_unlock(&mutexA); pthread_exit(NULL); } void* funcB(void* arg) { pthread_mutex_lock(&mutexB); pthread_mutex_lock(&mutexA); pthread_mutex_unlock(&mutexA); pthread_mutex_unlock(&mutexB); pthread_exit(NULL); } int main() { pthread_t t1, t2; pthread_create(&t1, NULL, funcA, NULL); pthread_create(&t2, NULL, funcB, NULL); pthread_join(t1, NULL); pthread_join(t2, NULL); return 0; }
- 在使用多线程的时候,如何避免死锁
- 对共享资源访问完毕后,一定要解锁。或者在加锁的时候使用trylock
- 如果程序中包含多把锁,可以控制对锁的访问顺序。也可以在对其他互斥锁做加锁操作前,先释放当前线程拥有的互斥锁。
- 在项目中引入专门用于死锁检测的模块
- 项目开发中能不使用锁就尽量不要使用锁,无锁队列了解一下hhh
- 实际运行的项目可能出现死锁,如何排查?