文章目录
- 线程同步
- 信号量
- 互斥量
- 条件变量
线程同步
- 线程同步其实实现的是线程排队。
- 防止线程同步访问共享资源造成冲突。
- 多个线程访问共享资源的代码有可能是同一份代码,也有可能是不同的代码;无论是否执行同一份代码,只要这些线程的代码访问同一份可变的共享资源,这些线程之间就需要同步。
1. 问题
- 同一个进程内的各个线程,共享该进程内的全局变量
- 如果多个线程同时对某个全局变量进行访问时,有可能达不到预期效果。
2. 信号量和互斥量的选择。
- 互斥量:为协调共同对一个共享资源的单独访问而设计的;因为进入内核模式,所以性能比临界区差;跨进程,可用于防止程序重复打开运行。
- 信号量:为控制一个具有有限数量用户资源而设计,互斥锁可以理解为1个用户资源的信号量。
- 使用时,选择更符合语义的手段:
- 如果要求最多只允许一个线程进入临界区,则使用互斥量
- 如果要求多个线程之间的执行顺序满足某个约束,则使用信号量
- 使用时,选择更符合语义的手段:
条件变量:条件变量是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:一个线程等待"条件变量的条件成立"而挂起;另一个线程使"条件成立"(给出条件成立信号)。
信号量
1)什么是信号量
-
此时所指的“信号量”是指用于同一个进程内多个线程之间的信号量。即POSIX信号量,而不是System V信号量(用于进程之间的同步)
-
用于线程的信号量的原理,与用于进程之间的信号量的原理相同。都有P操作、V操作。
-
信号量的表示:sem_t 类型
2) 信号量的初始化
原型:int sem_init (sem_t *sem,int pshared, unsigned int value); 功能:对信号量进行初始化 参数:sem, 指向被初始化的信号量 pshared, 0:表示该信号量是该进程内使用的“局部信号量”, 不再被其它进程共享。 非0:该信号量可被其他进程共享,Linux不支持这种信号量 value, 信号量的初值。>= 0 返回值:成功,返回0 失败, 返回错误码
3) 信号量的P操作
原型:int sem_wait (sem_t *sem); 返回值:成功,返回0 失败, 返回错误码
4) 信号量的V操作
原型:int sem_post (sem_t *sem); 返回值:成功,返回0 失败, 返回错误码
5) 信号量的删除
原型:int sem_destroy (sem_t *sem); 返回值:成功,返回0 失败, 返回错误码
6) 实例
主线程循环输入字符串,把字符串存放到一个全局缓存中。新线程从全局缓存中读取字符串,统计该字符串的长度。直到用户输入end
main.c
预期结果:主线程每接收终端输入的一个字符串,子线程就打印字符串和输出字符串长度
- 信号量被初始化为0,主线程接受一个字符串的时候,执行V操作(信号量+1),此时信号量大于1,子线程执行P操作(-1),然后对字符串做出相应的操作
#include <pthread.h>
#include <semaphore.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#define BUFF_SIZE 80
// 全局变量可以让多个线程访问
char buff[BUFF_SIZE];
sem_t sem;
static void* str_thread_handle(void *arg)
{
while(1) {
//P(sem) -1
if (sem_wait(&sem) != 0) {
printf("sem_wait failed!\n");
exit(1);
}
printf("string is: %slen=%u\n", buff, (unsigned int)strlen(buff));
if (strncmp(buff, "end", 3) == 0) {
break;
}
}
}
int main(void)
{
int ret;
pthread_t str_thread;
void *thread_return;
// 参数:被初始化信号量 0 给信号量的赋值
ret = sem_init(&sem, 0, 0);
if (ret != 0) {
printf("sem_init failed!\n");
exit(1);
}
// 创建线程
ret = pthread_create(&str_thread, 0, str_thread_handle, 0);
if (ret != 0) {
printf("pthread_create failed!\n");
exit(1);
}
while (1) {
// 从终端获取一行输入
fgets(buff, sizeof(buff), stdin);
//V(sem) +1
// 0->1 那么线程就可以执行P操作 1->0
if (sem_post(&sem) != 0) {
printf("sem_post failed!\n");
exit(1);
}
if (strncmp(buff, "end", 3) == 0) {
break;
}
}
ret = pthread_join(str_thread, &thread_return);
if (ret != 0) {
printf("pthread_join failed!\n");
exit(1);
}
ret = sem_destroy(&sem);
if (ret != 0) {
printf("sem_destroy failed!\n");
exit(1);
}
return 0;
}
练习
创建2个线程(共有主线程、线程1、线程2共3个线程)
主线程阻塞式等待用户输入字符串
主线程每接收到一个字符串之后, 线程1就马上对该字符串进行处理。
线程1的处理逻辑为:统计该字符串的个数,并记录当时的时间。
线程1把该字符串处理完后,线程2马上就把处理结果写入文件result.txt
直到用户输入exit.
multi_pthread.c
互斥量
1)什么是互斥量
效果上等同于初值为1的信号量
互斥量的使用:类型为 pthread_mutex_t
2)互斥量的初始化
原型:int pthread_mutex_init(pthread_mutex_t *mutex,
pthread_mutexattr_t *attr);
参数:mutex, 指向被初始化的互斥量
attr, 指向互斥量的属性
一般取默认属性(当一个线程已获取互斥量后,该线程再次获取该信号量,将导致死锁!)
3) 互斥量的获取
原型:int pthread_mutex_lock (pthread_mutex_t *mutex);
4)互斥量的释放
原型:int pthread_mutex_unlock (pthread_mutex_t *mutex);
5)互斥量的删除
int pthread_mutex_destroy (pthread_mutex_t *mutex);
- 实例
main3.c
预期实现效果:主线程和子线程依次把全局变量 -1 ,依次往终端输出结果
- 当没有用互斥量的时候,同时运行主线程和子线程,对全局变量进行 - 1
#include <pthread.h>
#include <semaphore.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#define BUFF_SIZE 80
// 全局
int global_value = 1000;
pthread_mutex_t lock;
static void* str_thread_handle(void *arg)
{
int i = 0;
for (i=0; i<10; i++) {
//pthread_mutex_lock(&lock);
if (global_value > 0) {
// work
sleep(1);
printf("soled ticket(%d) to ChildStation(%d)\n",
global_value, i+1);
}
global_value--;
//pthread_mutex_unlock(&lock);
sleep(1);
}
}
int main(void)
{
int ret;
pthread_t str_thread;
void *thread_return;
int i;
ret = pthread_mutex_init(&lock, 0);
if (ret != 0) {
printf("pthread_mutex_init failed!\n");
exit(1);
}
ret = pthread_create(&str_thread, 0, str_thread_handle, 0);
if (ret != 0) {
printf("pthread_create failed!\n");
exit(1);
}
for (i=0; i<10; i++) {
//pthread_mutex_lock(&lock);
if (global_value > 0) {
// work
sleep(1);
printf("soled ticket(%d) to MainStation(%d)\n",
global_value, i+1);
}
global_value--;
//pthread_mutex_unlock(&lock);
sleep(1);
}
ret = pthread_join(str_thread, &thread_return);
if (ret != 0) {
printf("pthread_join failed!\n");
exit(1);
}
ret = pthread_mutex_destroy(&lock);
if (ret != 0) {
printf("pthread_mutex_destroy failed!\n");
exit(1);
}
return 0;
}
- 主线程 -10 , 子线程 -10 最后应该是 981 所以不符合预期
- 原因 主线程和子线程同时对全局变量进行了 -1
使用信号量后 - 上了两把锁
- 给主线程和子线程对全局变量进行操作的部分分别上锁
#include <pthread.h>
#include <semaphore.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#define BUFF_SIZE 80
// 全局
int global_value = 1000;
pthread_mutex_t lock;
static void* str_thread_handle(void *arg)
{
int i = 0;
for (i=0; i<10; i++) {
/*****上锁*****/
pthread_mutex_lock(&lock);
if (global_value > 0) {
// work
sleep(1);
printf("soled ticket(%d) to ChildStation(%d)\n",
global_value, i+1);
}
global_value--;
/*****开锁*****/
pthread_mutex_unlock(&lock);
sleep(1);
}
}
int main(void)
{
int ret;
pthread_t str_thread;
void *thread_return;
int i;
ret = pthread_mutex_init(&lock, 0);
if (ret != 0) {
printf("pthread_mutex_init failed!\n");
exit(1);
}
ret = pthread_create(&str_thread, 0, str_thread_handle, 0);
if (ret != 0) {
printf("pthread_create failed!\n");
exit(1);
}
for (i=0; i<10; i++) {
/*****上锁*****/
pthread_mutex_lock(&lock);
if (global_value > 0) {
// work
sleep(1);
printf("soled ticket(%d) to MainStation(%d)\n",
global_value, i+1);
}
global_value--;
/*****开锁*****/
pthread_mutex_unlock(&lock);
sleep(1);
}
ret = pthread_join(str_thread, &thread_return);
if (ret != 0) {
printf("pthread_join failed!\n");
exit(1);
}
ret = pthread_mutex_destroy(&lock);
if (ret != 0) {
printf("pthread_mutex_destroy failed!\n");
exit(1);
}
return 0;
}
- 结果分析:相比于上次,每一次输出全局变量就 -1 最后结果为 981
条件变量
1.什么是线程条件变量
与互斥锁不同,条件变量是用来等待而不是用来上锁的。条件变量用来自动阻塞一个线程,直到某特殊情况发生为止。通常条件变量和互斥锁同时使用。
条件变量使我们可以睡眠等待某种条件出现。条件变量是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:一个线程等待"条件变量的条件成立"而挂起;另一个线程使"条件成立"(给出条件成立信号)。
2. 条件变量初始化
原型:int pthread_cond_init (pthread_cond_t *cond, const pthread_condattr_t *attr);
参数:cond: 条件变量指针
attr:条件变量高级属性
3. 唤醒一个等待线程
原型: int pthread_cond_signal (pthread_cond_t *cond);
参数:cond:条件变量指针
4.唤醒所有等待该条件变量的线程
原型: int pthread_cond_broadcast (pthread_cond_t *cond);
参数:cond, 条件变量指针
5.等待条件变量/超时被唤醒
原型: int pthread_cond_timedwait (pthread_cond_t *cond, pthread_mutex_t *mutex,
const struct timespec *abstime);
参数:cond, 条件变量指针
pthread_mutex_t *mutex 互斥量
const struct timespec *abstime 等待被唤醒的绝对超时时间
6.等待条件变量被唤醒(一般使用这个)
原型: int pthread_cond_wait (pthread_cond_t *cond, pthread_mutex_t *mutex);
参数:cond, 条件变量指针
pthread_mutex_t *mutex 互斥量
常见错误码: [EINVAL] cond或mutex无效,
[EINVAL] 同时等待不同的互斥量
[EINVAL] 主调线程没有占有互斥量
7. 释放/销毁条件变量
pthread_cond_destroy 待销毁的条件变量
原型: int pthread_cond_destroy (pthread_cond_t *cond);
参数:cond, 条件变量指针
main.c
效果:触发信号,子线程向终端打印数据
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
// 注意这里一定要定义为全局变量
pthread_mutex_t mutex;
pthread_cond_t cond;
void *thread1(void *arg)
{
while (1) {
printf("thread1 is running\n");
// 加锁
pthread_mutex_lock(&mutex);
printf("thread1 lock..\n");
// 解锁-阻塞等待信号-信号来了-加锁-执行任务
pthread_cond_wait(&cond, &mutex);
// 执行任务
printf("thread1 applied the condition\n");
// 解锁
printf("thread1 unlock..\n");
pthread_mutex_unlock(&mutex);
sleep(4);
}
}
void *thread2(void *arg)
{
while (1) {
printf("thread2 is running\n");
pthread_mutex_lock(&mutex);
printf("thread2 lock..\n");
pthread_cond_wait(&cond, &mutex);
printf("thread2 applied the condition\n");
printf("thread2 unlock..\n");
pthread_mutex_unlock(&mutex);
sleep(2);
}
}
int main()
{
pthread_t thid1, thid2;
printf("condition variable study!\n");
// 初始化互斥锁 效果上等同于初值为1的信号量
// 初始化条件变量
pthread_mutex_init(&mutex, NULL);
pthread_cond_init(&cond, NULL);
// 创建两个线程
pthread_create(&thid1, NULL, (void *)thread1, NULL);
pthread_create(&thid2, NULL, (void *)thread2, NULL);
// 不断发送信号,唤醒一个线程
do {
sleep(10);
pthread_cond_signal(&cond);
} while (1);
return 0;
}
结果分析:为什么两个线程可以同时上锁?
因为 pthread_cond_wait(&cond, &mutex); 执行过程中有一个解锁的过程,所以是解锁后,另一个线程拿到锁。但是如果此线程被信号触发,那么也会立即上锁,执行任务。