Linux网络编程3——多线程编程(改良版)
一.多线程
1.什么是线程
要了解线程,首先需要知道进程。一个进程指的是一个正在执行的应用程序。线程对应的英文名称为“thread
”,它的功能是执行应用程序中的某个具体任务,比如一段程序、一个函数等。
线程和进程之间的关系,类似于工厂和工人之间的关系,进程好比是工厂,线程就如同工厂中的工人。一个工厂可以容纳多个工人,工厂负责为所有工人提供必要的资源(电力、产品原料、食堂、厕所等),所有工人共享这些资源,每个工人负责完成一项具体的任务,他们相互配合,共同保证整个工厂的平稳运行。
每个进程执行前,操作系统都会为其分配所需的资源,包括要执行的程序代码、数据、内存空间、文件资源等。一个进程至少包含 1 个线程,可以包含多个线程,所有线程共享进程的资源,各个线程也可以拥有属于自己的私有资源。
进程仅负责为各个线程提供所需的资源,真正执行任务的是线程,而不是进程。
2.什么是多线程
所谓多线程,即一个进程中拥有多(≥2)个线程,线程之间相互协作、共同执行一个应用程序。
当进程中仅包含 1 个执行程序指令的线程时,该线程又称“主线程”,这样的进程称为“单线程进程”。
二.线程基础函数
1.pthread_self函数
作用:获取线程的ID,起作用对应于进程的getpid()
函数
函数原型:
#include <pthread.h>pthread_t pthread_self(void);
- 线程ID:
pthread_t
类型,本质是Linux下的无符号整数 - 线程ID是进程内部识别的标志(两个进程间,线程ID允许相同)
2.pthread_create函数
作用:用于创建新线程
函数原型:
#include <pthread.h>int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void*), void *arg);
参数说明:
pthread_t *thread
: 指向pthread_t
类型的指针,用于存储新创建线程的线程 ID。const pthread_attr_t *attr
: 指向pthread_attr_t
结构的指针,该结构包含了新线程的属性。如果不需要设置特定属性,可以传递NULL
。void *(*start_routine)(void*)
: 新线程的入口函数(回调函数应用),即线程开始执行时调用的函数。这个函数必须有返回值类型void*
并接受一个void*
类型的参数。void *arg
: 传递给(回调函数)线程入口函数start_routine
的参数。
个人理解补充:
- 该函数的第一个参数 pthread_t *thread:传递一个 pthread_t 类型的指针变量,也可以直接传递某个 pthread_t 类型变量的地址。**pthread_t 是一种用于表示线程的数据类型,每一个 pthread_t 类型的变量都可以表示一个线程。**例如 int 是一种表示整数的数据类型,每个 int 类型的变量都可以表示一个整数,它们都是数据类型的一种。
- 该函数的第三个参数是正在创建的该线程需要执行的函数,需要注意的是这里是以函数指针的方式指明新建线程需要执行的函数,该函数的形参和返回值都必须为
void*
类型。void*
类型又称空指针类型,表明指针所指数据的类型是未知的(如果不理解,类比于结构体指针一样理解)。使用此类型指针时,我们通常需要先对其进行强制类型转换,然后才能正常访问指针指向的数据。- 返回值:如果成功创建线程,
pthread_create()
函数返回数字 0,反之返回非零值。各个非零值都对应着不同的宏,指明创建失败的原因,这里可以自己去了解。
void* thread_function(void *arg) {// 线程要执行的代码return NULL;
}int main() {pthread_t thread_id;int result = pthread_create(&thread_id, NULL, thread_function, NULL);if (result) {// 错误处理}// 其他代码return 0;
}
3.pthread_exit函数
用于终止调用线程。在 C 语言中,该函数的原型定义在 <pthread.h>
头文件中。函数原型如下:
void pthread_exit(void *retval);
参数说明:
retval
:指向一个指针的指针,用于返回一个值给其他线程。如果调用线程不关心返回值,可以传递NULL
。
当线程调用 pthread_exit
时,它将立即终止。如果有其他线程使用 pthread_join
等待该线程的结束,并且提供了一个接收返回值的指针,那么这个返回值将会被存储在提供的指针所指向的位置。
需要注意的是,pthread_exit
并不会清理线程的栈,也不会执行任何线程的清理处理程序。如果需要执行清理操作,应该使用 pthread_cleanup_push
和 pthread_cleanup_pop
函数来注册和注销清理函数。
该函数用来终止线程执行。多线程程序中,终止线程执行的方式本来有 3 种,分别是:
-
线程执行完成后,自行终止;
-
线程执行过程中遇到了 pthread_exit() 或者 return,也会终止执行;
-
线程执行过程中,接收到其它线程发送的“终止执行”的信号,然后终止执行。
第一种的理解就是什么也不管,线程执行完会自己终止;第二种就是本部分要用的pthread_exit()函数,return也好理解,返回即终止;第三种方法,本来要使用pthread_cancel()函数,但在使用这个函数时会出现其他一系列的问题,解决起来非常麻烦,所以除非特殊情况,我们一般使用第二种方式。
补充:pthread_exit()和return的区别:首先,return 语句和 pthread_exit() 函数的含义不同,return 的含义是返回,它不仅可以用于线程执行的函数,普通函数也可以使用;pthread_exit() 函数的含义是线程退出,它专门用于结束某个线程的执行。实际使用中,我们终止子线程一般都使用pthread_exit()函数,不建议使用return。
4.pthread_join函数
pthread_join
函数是 POSIX 线程库中的一个函数,用于等待一个线程结束。这个函数允许一个线程(通常称为“主线程”)等待另一个线程(即被等待的线程)完成执行。在 C 语言中,该函数的原型定义在 <pthread.h>
头文件中。函数原型如下:
int pthread_join(pthread_t thread, void **retval);
参数说明:
thread
:标识要等待的线程的线程 ID。retval
:一个指向指针的指针,用于存储被等待线程的返回值。如果不需要获取返回值,可以传递NULL
。
返回值:
- 成功时,
pthread_join
函数返回 0。 - 失败时,返回一个错误编号,具体错误编号可以在
<errno.h>
头文件中找到。
当 pthread_join
被调用时,如果 thread
参数指定的线程已经结束,那么 pthread_join
会立即返回。如果指定的线程还没有结束,调用 pthread_join
的线程将阻塞,直到被等待的线程退出。一旦被等待的线程退出,retval
指向的变量将被设置为该线程的返回值(如果有的话)。
5.pthread_cancel函数
pthread_cancel
函数是 POSIX 线程(pthread)库中的一个函数,用于请求取消(终止)一个指定的线程。当调用这个函数时,目标线程将收到一个取消信号,并且通常会执行一些清理操作后终止。这个函数的原型定义在 <pthread.h>
头文件中,如下所示:
int pthread_cancel(pthread_t thread);
参数说明:
thread
:需要被取消的线程的标识符。
返回值:
- 成功时,返回 0。
- 失败时,返回一个错误编号,具体错误编号可以在
<errno.h>
头文件中找到。
需要注意的是,pthread_cancel
函数只是发起了一个取消请求,并不保证目标线程会立即停止执行。目标线程可能会在完成当前操作或者达到一个安全点后才开始处理取消请求。此外,目标线程可以安装取消处理函数(使用 pthread_setcanceltype
和 pthread_cleanup_push
/pthread_cleanup_pop
),以便在被取消时执行一些清理工作。
6.pthread_detach函数
pthread_detach
函数用于将一个线程标记为分离状态(detached)。当线程被创建时,默认情况下它是“joinable”,这意味着在它结束执行之前,其他线程可以使用 pthread_join
函数来等待它结束。如果一个线程被标记为分离状态,那么一旦线程结束执行,它的资源将自动被释放,不需要其他线程显式调用 pthread_join
。
pthread_detach
函数的原型定义在 <pthread.h>
头文件中,如下所示:
int pthread_detach(pthread_t thread);
参数说明:
thread
:需要被标记为分离状态的线程的标识符。
返回值:
- 成功时,返回 0。
- 失败时,返回一个错误编号,具体错误编号可以在
<errno.h>
头文件中找到。
三.线程同步
1.缘由
多线程程序中各个线程除了可以使用自己的私有资源(局部变量、函数形参等)外,还可以共享全局变量、静态变量、堆内存、打开的文件等资源。我们通常将“多个线程同时访问某一公共资源”的现象称为“线程间产生了资源竞争”或者“线程间抢夺公共资源”,线程间竞争资源往往会导致程序的运行结果出现异常,我们常常采用同步机制来解决这种问题。
2.实现方法
实现线程同步的常用方法有 4 种,分别称为互斥锁、信号量、条件变量和读写锁。
-
互斥锁(
Mutex
)又称互斥量或者互斥体,是最简单也最有效地一种线程同步机制。互斥锁的用法和实际生活中的锁非常类似,当一个线程访问公共资源时,会及时地“锁上”该资源,阻止其它线程访问;访问结束后再进行“解锁”操作,将该资源让给其它线程访问。 -
信号量又称“信号灯”,主要用于控制同时访问公共资源的线程数量,当线程数量控制在 ≤1 时,该信号量又称二元信号量,功能和互斥锁非常类似;当线程数量控制在 N(≥2)个时,该信号量又称多元信号量,指的是同一时刻最多只能有 N 个线程访问该资源。
-
条件变量的功能类似于实际生活中的门,门有“打开”和“关闭”两种状态,分别对应条件变量的“成立”状态和“不成立”状态。当条件变量“不成立”时,任何线程都无法访问资源,只能等待条件变量成立;一旦条件变量成立,所有等待的线程都会恢复执行,访问目标资源。为了防止各个线程竞争资源,条件变量总是和互斥锁搭配使用。
-
多线程程序中,如果大多数线程都是对公共资源执行读取操作,仅有少量的线程对公共资源进行修改,这种情况下可以使用读写锁解决线程同步问题。
这里我们使用最简单的也是最常用的方法:互斥锁。
3.互斥锁
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>//共享资源,打印机
void printer(char *str)
{while(*str != '\0'){putchar(*str);fflush(stdout);str++;sleep(1);}
}//线程1
void *thread_fun_1(void *arg)
{char *str = "hello";printer(str);
}//线程2
void *thread_fun_2(void *arg)
{char *str = "HELLO";printer(str);
}int main()
{pthread_t tid1,tid2;//创建两个线程pthread_create(&tid1,NULL,thread_fun_1,NULL);pthread_create(&tid2,NULL,thread_fun_2,NULL);//等待线程执行结束,回收其资源pthread_join(tid1,NULL);pthread_join(tid2,NULL);return 0;
}
这里是一个共享打印机的程序案例,此刻还没有使用互斥锁。
下面是几个互斥锁常用的API
函数:
1.初始化线程函数
#include <pthread.h>int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
-
功能:初始化一个互斥锁。
-
参数:
mutex
:互斥锁地址。类型是pthread_mutex_t
。attr
:设置互斥量的属性,通常可采用默认属性,即可将attr
设为 NULL。
-
可以使用宏
PTHREAD_MUTEX_INITIALIZER
静态初始化互斥锁,比如:pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
这种方法等价于使用 NULL 指定的attr
参数调用pthread_mutex_init()
来完成动态初始化,不同之处在于PTHREAD_MUTEX_INITIALIZER
宏不进行错误检查。 -
返回值:
- 成功:0,成功申请的锁默认是打开的。
- 失败:非 0 错误码
2.销毁线程函数
#include <pthread.h>
int pthread_mutex_destroy(pthread_mutex_t *mutex);
功能:销毁指定的一个互斥锁。互斥锁在使用完毕后,必须要对互斥锁进行销毁,以释放资源。
参数:mutex:互斥锁地址。
返回值:成功:0失败:非 0 错误码
3.加锁函数
#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);
功能:对互斥锁上锁,若互斥锁已经上锁,则调用者阻塞,直到互斥锁解锁后再上锁。
参数:mutex:互斥锁地址。
返回值:成功:0失败:非 0 错误码
int pthread_mutex_trylock(pthread_mutex_t *mutex);
调用该函数时,若互斥锁未加锁,则上锁,返回 0;
若互斥锁已加锁,则函数直接返回失败,即 EBUSY。
4.解锁函数
#include <pthread.h>
int pthread_mutex_unlock(pthread_mutex_t *mutex);
功能:对指定的互斥锁解锁。
参数:mutex:互斥锁地址。
返回值:成功:0失败:非0错误码
使用互斥锁修改上述案例:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>pthread_mutex_t mutex; //互斥锁void printer(char *str)
{pthread_mutex_lock(&mutex); //上锁while(*str != '\0'){putchar(*str);fflush(stdout);str++;sleep(1);}pthread_mutex_unlock(&mutex); //解锁
}//线程1
void *thread_fun_1(void *arg)
{char *str = "hello";printer(str);
}//线程2
void *thread_fun_2(void *arg)
{char *str = "HELLO";printer(str);
}int main()
{pthread_t tid1,tid2;//创建两个线程pthread_create(&tid1,NULL,thread_fun_1,NULL);pthread_create(&tid2,NULL,thread_fun_2,NULL);//等待线程执行结束,回收其资源pthread_join(tid1,NULL);pthread_join(tid2,NULL);return 0;
}
4.读写锁
当有一个线程已经持有互斥锁时,互斥锁将所有试图进入临界区的线程都阻塞住。但是考虑一种情形,当前持有互斥锁的线程只是要读访问共享资源,而同时有其它几个线程也想读取这个共享资源,但是由于互斥锁的排它性,所有其它线程都无法获取锁,也就无法读访问共享资源了,但是实际上多个线程同时读访问共享资源并不会导致问题。
在对数据的读写操作中,更多的是读操作,写操作较少,例如对数据库数据的读写应用。为了满足当前能够允许多个读出,但只允许一个写入的需求,线程提供了读写锁来实现。
读写锁的特点如下:
1)如果有其它线程读数据,则允许其它线程执行读操作,但不允许写操作。
2)如果有其它线程写数据,则其它线程都不允许读、写操作。
读写锁分为读锁和写锁,规则如下:
1)如果某线程申请了读锁,其它线程可以再申请读锁,但不能申请写锁。
2)如果某线程申请了写锁,其它线程不能申请读锁,也不能申请写锁。
POSIX
定义的读写锁的数据类型是: pthread_rwlock_t
。
读写锁常规API函数:
初始化读写锁:
#include <pthread.h>
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,const pthread_rwlockattr_t *restrict attr);
功能:用来初始化 rwlock 所指向的读写锁。
参数:rwlock:指向要初始化的读写锁指针。attr:读写锁的属性指针。如果 attr 为 NULL 则会使用默认的属性初始化读写锁,否则使用指定的 attr 初始化读写锁。
可以使用宏 PTHREAD_RWLOCK_INITIALIZER 静态初始化读写锁,比如:pthread_rwlock_t my_rwlock = PTHREAD_RWLOCK_INITIALIZER;
这种方法等价于使用 NULL 指定的 attr 参数调用 pthread_rwlock_init() 来完成动态初始化,不同之处在于PTHREAD_RWLOCK_INITIALIZER 宏不进行错误检查。
返回值:成功:0,读写锁的状态将成为已初始化和已解锁。失败:非 0 错误码。
销毁读写锁:
#include <pthread.h>
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
功能:用于销毁一个读写锁,并释放所有相关联的资源(所谓的所有指的是由 pthread_rwlock_init() 自动申请的资源) 。
参数:rwlock:读写锁指针。
返回值:成功:0失败:非 0 错误码
添加读锁函数:
#include <pthread.h>
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
功能:以阻塞方式在读写锁上获取读锁(读锁定)。如果没有写者持有该锁,并且没有写者阻塞在该锁上,则调用线程会获取读锁。如果调用线程未获取读锁,则它将阻塞直到它获取了该锁。一个线程可以在一个读写锁上多次执行读锁定。线程可以成功调用 pthread_rwlock_rdlock() 函数 n 次,但是之后该线程必须调用 pthread_rwlock_unlock() 函数 n 次才能解除锁定。
参数:rwlock:读写锁指针。
返回值:成功:0失败:非 0 错误码
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
用于尝试以非阻塞的方式来在读写锁上获取读锁。
如果有任何的写者持有该锁或有写者阻塞在该读写锁上,则立即失败返回。
添加写锁函数“
#include <pthread.h>
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
功能:在读写锁上获取写锁(写锁定)。如果没有写者持有该锁,并且没有写者读者持有该锁,则调用线程会获取写锁。如果调用线程未获取写锁,则它将阻塞直到它获取了该锁。
参数:rwlock:读写锁指针。
返回值:成功:0失败:非 0 错误码
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
用于尝试以非阻塞的方式来在读写锁上获取写锁。
如果有任何的读者或写者持有该锁,则立即失败返回。
解锁函数:
#include <pthread.h>
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
功能:无论是读锁或写锁,都可以通过此函数解锁。
参数:rwlock:读写锁指针。
返回值:成功:0失败:非 0 错误码
读写锁示例:
pthread_rwlock_t rwlock; //读写锁
int num = 1;
//读操作,其他线程允许读操作,却不允许写操作
void *fun1(void *arg)
{while (1){pthread_rwlock_rdlock(&rwlock);printf("read num first===%d\n", num);pthread_rwlock_unlock(&rwlock);sleep(1);}
}
//读操作,其他线程允许读操作,却不允许写操作
void *fun2(void *arg)
{while (1){pthread_rwlock_rdlock(&rwlock);printf("read num second===%d\n", num);pthread_rwlock_unlock(&rwlock);sleep(2);}
}
//写操作,其它线程都不允许读或写操作
void *fun3(void *arg)
{while (1){
pthread_rwlock_wrlock(&rwlock);num++;printf("write thread first\n");pthread_rwlock_unlock(&rwlock);sleep(2);}
}
//写操作,其它线程都不允许读或写操作
void *fun4(void *arg)
{while (1){
pthread_rwlock_wrlock(&rwlock);num++;printf("write thread second\n");pthread_rwlock_unlock(&rwlock);sleep(1);}
}
int main()
{pthread_t ptd1, ptd2, ptd3, ptd4;
pthread_rwlock_init(&rwlock, NULL);//初始化一个读写锁
//创建线程pthread_create(&ptd1, NULL, fun1, NULL);pthread_create(&ptd2, NULL, fun2, NULL);pthread_create(&ptd3, NULL, fun3, NULL);pthread_create(&ptd4, NULL, fun4, NULL);
//等待线程结束,回收其资源pthread_join(ptd1, NULL);pthread_join(ptd2, NULL);pthread_join(ptd3, NULL);pthread_join(ptd4, NULL);
pthread_rwlock_destroy(&rwlock);//销毁读写锁
return 0;
}
5.条件变量
与互斥锁不同,条件变量是用来等待而不是用来上锁的,条件变量本身不是锁!
条件变量用来自动阻塞一个线程,直到某特殊情况发生为止。通常条件变量和互斥锁同时使用。
条件变量的两个动作:
- 条件不满, 阻塞线程
- 当条件满足, 通知阻塞的线程开始工作
条件变量的类型: pthread_cond_t
。
初始化条件变量函数:
#include <pthread.h>
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
功能:初始化一个条件变量
参数:cond:指向要初始化的条件变量指针。attr:条件变量属性,通常为默认值,传NULL即可也可以使用静态初始化的方法,初始化条件变量:pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
返回值:成功:0失败:非0错误号
销毁条件变量函数:
#include <pthread.h>
int pthread_cond_destroy(pthread_cond_t *cond);
功能:销毁一个条件变量
参数:cond:指向要初始化的条件变量指针
返回值:成功:0失败:非0错误号
阻塞等待条件变量函数:
#include <pthread.h>
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
功能:阻塞等待一个条件变量a) 阻塞等待条件变量cond(参1)满足b) 释放已掌握的互斥锁(解锁互斥量)相当于pthread_mutex_unlock(&mutex);a) b) 两步为一个原子操作。c) 当被唤醒,pthread_cond_wait函数返回时,解除阻塞并重新申请获取互斥锁pthread_mutex_lock(&mutex);
参数:cond:指向要初始化的条件变量指针mutex:互斥锁
返回值:成功:0失败:非0错误号
int pthread_cond_timedwait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex,const struct.*restrict abstime);
功能:限时等待一个条件变量
参数:cond:指向要初始化的条件变量指针mutex:互斥锁abstime:绝对时间
返回值:成功:0失败:非0错误号
abstime
补充说明:
struct timespec {time_t tv_sec; /* seconds */ // 秒long tv_nsec; /* nanosecondes*/ // 纳秒
}time_t cur = time(NULL); //获取当前时间。
struct timespec t; //定义timespec 结构体变量t
t.tv_sec = cur + 1; // 定时1秒
pthread_cond_timedwait(&cond, &t);
唤醒至阻塞在条件变量上的线程:
#include <pthread.h>
int pthread_cond_signal(pthread_cond_t *cond);
功能:唤醒至少一个阻塞在条件变量上的线程
参数:cond:指向要初始化的条件变量指针
返回值:成功:0失败:非0错误号
int pthread_cond_broadcast(pthread_cond_t *cond);
功能:唤醒全部阻塞在条件变量上的线程
参数:cond:指向要初始化的条件变量指针
返回值:成功:0失败:非0错误号
综合互斥锁和条件变量案例联系:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>int flag = 0;
pthread_mutex_t mutex; //互斥锁
pthread_cond_t cond; //条件变量//改变条件的线程
void *thread_fun_1(void *arg)
{while(1){//加锁pthread_mutex_lock(&mutex);flag = 1;//解锁pthread_mutex_unlock(&mutex);//唤醒因为条件而阻塞的线程pthread_cond_signal(&cond);sleep(2);}
}//线程2
void *thread_fun_2(void *arg)
{while(1){//加锁pthread_mutex_lock(&mutex);//条件不成立,阻塞,等待条件为真if(0 == flag){pthread_cond_wait(&cond,&mutex);}printf("条件为真,开始修改值\n");flag = 0;//解锁pthread_mutex_unlock(&mutex);}
}int main()
{int ret = -1;pthread_t tid1,tid2;//初始化条件变量ret = pthread_cond_init(&cond,NULL);if(0 != ret){printf("初始化条件变量失败\n");return 1;}//初始化互斥锁ret = pthread_mutex_init(&mutex,NULL);if(0 != ret){printf("初始化互斥锁失败\n");}//创建两个线程pthread_create(&tid1,NULL,thread_fun_1,NULL);pthread_create(&tid2,NULL,thread_fun_2,NULL);//等待线程执行结束,回收其资源pthread_join(tid1,NULL);pthread_join(tid2,NULL);//销毁互斥锁pthread_mutex_destroy(&mutex);//销毁条件变量pthread_cond_destroy(&cond);return 0;
}
6.生产者消费者模型
生产消费者模型:
// 节点结构
typedef struct node
{int data;struct node* next;
}Node;
// 永远指向链表头部的指针
Node* head = NULL;
// 线程同步 - 互斥锁
pthread_mutex_t mutex;
// 阻塞线程 - 条件变量类型的变量
pthread_cond_t cond;
// 生产者
void* producer(void* arg)
{while (1){// 创建一个链表的节点Node* pnew = (Node*)malloc(sizeof(Node));// 节点的初始化pnew->data = rand() % 1000; // 0-999
// 使用互斥锁保护共享数据pthread_mutex_lock(&mutex);// 指针域pnew->next = head;head = pnew;printf("====== produce: %lu, %d\n", pthread_self(), pnew->data);pthread_mutex_unlock(&mutex);
// 通知阻塞的消费者线程,解除阻塞pthread_cond_signal(&cond);
sleep(rand() % 3);}return NULL;
}
void* customer(void* arg)
{while (1){pthread_mutex_lock(&mutex);// 判断链表是否为空if (head == NULL){// 线程阻塞// 该函数会对互斥锁解锁pthread_cond_wait(&cond, &mutex);// 解除阻塞之后,对互斥锁做加锁操作}// 链表不为空 - 删掉一个节点 - 删除头结点Node* pdel = head;head = head->next;printf("------ customer: %lu, %d\n", pthread_self(), pdel->data);free(pdel);pthread_mutex_unlock(&mutex);}return NULL;
}
int main(int argc, const char* argv[])
{pthread_t p1, p2;// initpthread_mutex_init(&mutex, NULL);pthread_cond_init(&cond, NULL);
// 创建生产者线程pthread_create(&p1, NULL, producer, NULL);// 创建消费者线程pthread_create(&p2, NULL, customer, NULL);
// 阻塞回收子线程pthread_join(p1, NULL);pthread_join(p2, NULL);
pthread_mutex_destroy(&mutex);pthread_cond_destroy(&cond);
return 0;
}
7.信号量
信号量广泛用于进程或线程间的同步和互斥,信号量本质上是一个非负的整数计数器,它被用来控制对公共资源的访问。
编程时可根据操作信号量值的结果判断是否对公共资源具有访问的权限,当信号量值大于 0 时,则可以访问,否则将阻塞。
PV
原语是对信号量的操作,一次 P 操作使信号量减1,一次 V 操作使信号量加1。
信号量主要用于进程或线程间的同步和互斥这两种典型情况。
信号量数据类型为:sem_t
。
信号量用于互斥:
信号量用于同步:
信号量的常见API函数:
1.初始化信号量函数
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
功能:创建一个信号量并初始化它的值。一个无名信号量在被使用前必须先初始化。
参数:sem:信号量的地址。pshared:等于 0,信号量在线程间共享(常用);不等于0,信号量在进程间共享。value:信号量的初始值。
返回值:成功:0失败: - 1
2.销毁信号量
#include <semaphore.h>
int sem_destroy(sem_t *sem);
功能:删除 sem 标识的信号量。
参数:sem:信号量地址。
返回值:成功:0失败: - 1
3.信号量P操作(占用1个资源)
#include <semaphore.h>
int sem_wait(sem_t *sem);
功能:将信号量的值减 1。操作前,先检查信号量(sem)的值是否为 0,若信号量为 0,此函数会阻塞,直到信号量大于 0 时才进行减 1 操作。
参数:sem:信号量的地址。
返回值:成功:0失败: - 1
int sem_trywait(sem_t *sem);
以非阻塞的方式来对信号量进行减 1 操作。
若操作前,信号量的值等于 0,则对信号量的操作失败,函数立即返回。
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
限时尝试将信号量的值减 1
abs_timeout:绝对时间
abs_timeout补充说明:
struct timespec {time_t tv_sec; /* seconds */ // 秒long tv_nsec; /* nanosecondes*/ // 纳秒
}time_t cur = time(NULL); //获取当前时间。
struct timespec t; //定义timespec 结构体变量t
t.tv_sec = cur + 1; // 定时1秒
sem_timedwait(&cond, &t);
4.信号量V操作(释放1个资源)
#include <semaphore.h>
int sem_post(sem_t *sem);
功能:将信号量的值加 1 并发出信号唤醒等待线程(sem_wait())。
参数:sem:信号量的地址。
返回值:成功:0失败:-1
5.获取信号量的值
#include <semaphore.h>
int sem_getvalue(sem_t *sem, int *sval);
功能:获取 sem 标识的信号量的值,保存在 sval 中。
参数:sem:信号量地址。sval:保存信号量值的地址。
返回值:成功:0失败:-1
信号量案例:
sem_t sem; //信号量
void printer(char *str)
{sem_wait(&sem);//减一while (*str){putchar(*str);fflush(stdout);str++;sleep(1);}printf("\n");
sem_post(&sem);//加一
}
void *thread_fun1(void *arg)
{char *str1 = "hello";printer(str1);
}
void *thread_fun2(void *arg)
{char *str2 = "world";printer(str2);
}
int main(void)
{pthread_t tid1, tid2;
sem_init(&sem, 0, 1); //初始化信号量,初始值为 1
//创建 2 个线程pthread_create(&tid1, NULL, thread_fun1, NULL);pthread_create(&tid2, NULL, thread_fun2, NULL);
//等待线程结束,回收其资源pthread_join(tid1, NULL);pthread_join(tid2, NULL);
sem_destroy(&sem); //销毁信号量
return 0;
}
相关文章:
Linux网络编程3——多线程编程(改良版)
一.多线程 1.什么是线程 要了解线程,首先需要知道进程。一个进程指的是一个正在执行的应用程序。线程对应的英文名称为“thread”,它的功能是执行应用程序中的某个具体任务,比如一段程序、一个函数等。 线程和进程之间的关系,类…...
LeetCode每日三题(六)数组
一、最大子数组和 自己答案: class Solution {public int maxSubArray(int[] nums) {int begin0;int end0;if(numsnull){//如果数组非空return 0;}else if(nums.length1){//如果数组只有一个元素return nums[0];}//初值选为数组的第一个值int resultnums[0];int i…...
安装了python,环境变量也设置了,但是输入python不报错也没反应是为什么?window的锅!
目录 问题 结论总结 衍生问题 1 第1步:小白python安装,不要埋头一直点下一步!!! 2 第2步:可以选择删了之前的,重新安装python 3 第3步:如果你不想或不能删了重装python&#…...
Vue.js组件开发-使用KeepAlive缓存特定组件
在Vue中,<keep-alive> 组件是一个非常有用的工具,可以用来缓存那些不希望每次切换时都重新渲染的组件。要缓存特定组件,可以使用 <keep-alive> 的 include 和 exclude 属性,这两个属性都接受字符串、正则表达式或数组…...
配置hive支持中文注释
hive元数据metastore默认的字符集是latin1,所以中文注释会乱码。但是不能将metastore库的字符集更改为utf-8,只能对特定表、特定列修改为utf-8。配置hive支持中文注释,主要在两个方面: 1、在Hive元数据存储的Mysql数据库中&#…...
Windows配置IE浏览器不自动跳转到Edge
一:使用 IE 浏览器自身设置(部分情况有效) 打开 IE 浏览器设置:启动 IE 浏览器,点击右上角的 “工具”(齿轮形状)图标,选择 “Internet 选项”。设置启动选项:在 “Inte…...
BGP特性实验
实验拓扑 实验需求及解法 本实验模拟大规模BGP网络部署,使用4字节AS号,传递IPv6路由。 预配说明: sysname R1 ospfv3 1router-id 1.1.1.1 # firewall zone Localpriority 15 # interface Serial1/0/0link-protocol pppipv6 enable ipv6 ad…...
【多模态】从零学习多模态——2024学习笔记总结
从零学习多模态——2024学习笔记总结 前言1. preliminary2. Transformer和NLP基础3. 多模态模型原理和架构学习4. 动手实验多模态模型第一步尝试Swift框架使用数据验证 5. 总结 前言 2024快结束啦,半年抽空学了学多模态还挺好玩的,学习和踩坑记录记一下&…...
Meta AI 提出大型概念模型(LCMs):语义超越基于令牌的语言建模
在自然语言处理(Natural Language Processing, NLP)的领域,大型语言模型(Large Language Models, LLMs)已经取得了令人瞩目的成就,它们使得文本生成、摘要和问答等应用成为现实。但是,这些模型依…...
【优先算法】滑动窗口 --(结合例题讲解解题思路)(C++)
目录 编辑 1.什么是滑动窗口? 2. 滑动窗口例题 2.1 例题1:长度最小的子数组 2.1.1 解题思路 2.1.2 方法一:暴力枚举出所有的子数组的和 2.1.3 方法二:使用 “同向双指针” 也就是滑动窗口来进行优化 2.2 例题2:无重…...
Linux下PostgreSQL-12.0安装部署详细步骤
一、安装环境 postgresql-12.0 CentOS-7.6 注意:确认linux系统可以正常连接网络,因为在后面需要添加依赖包。 二、pg数据库安装包下载 下载地址:PostgreSQL: File Browser 选择要安装的版本进行下载: 三、安装依赖包 在要安…...
Delphi历史版本对照及主要版本特性
Delphi编程的关键特性包括: 可视化开发:Delphi以其独特的开发方法而闻名,它允许开发者通过直观的表单设计器来创建用户界面。这种快速应用程序开发(RAD)的方法大大简化并加速了图形用户界面(GUI)…...
java基础知识22 java的反射机制
一 java反射机制 1.1 概述 Java反射,程序在运行时,动态获取类信息(类元数据),获取字段属性,动态创建对象和调用方法。Spring框架正是基于反射机制,通过我们的配置文件,在项目运行时…...
多因子模型连载
多因子模型 (Multi-Factor Model)是量化投资中的一种重要工具,用于解释和预测股票收益。它通过将多个不同的因子(如市值、动量、价值、质量等)结合起来,构建一个综合的模型来评估股票的表现。多因子模型不仅能够捕捉单个因子的影响…...
服务器广播算法
服务器广播算法(Server Broadcasting Algorithm)是一种在分布式系统中用于高效地将信息从一个服务器传播到整个网络的算法。它被广泛用于分布式计算、数据中心、内容分发网络(CDN)和消息队列系统中。以下是常见的服务器广播算法的…...
具身智能 - Vision-language-action models for robot manipulation
具身智能大模型简介_哔哩哔哩_bilibili 受到自然语言处理、计算机视觉领域中基础模型的成功的启发,具身智能领域也在尝试设计、训练、微调大模型来解决现实生活中最普遍存在的机器人操作问题。“Vision-language-action models for robot manipulation”࿰…...
计算机组成原理的学习笔记(12) -- 总线和I/O系统
学习笔记 前言 本文主要是对于b站尚硅谷的计算机组成原理的学习笔记,仅用于学习交流。 总线 1. 组成 总线主要由三种信号线组成: 数据线:用于传输实际的数据,宽度决定了数据传输的并行性。 地址线:传输内存或I/…...
ReactiveStreams、Reactor、SpringWebFlux
注意: 本文内容于 2024-12-28 21:22:12 创建,可能不会在此平台上进行更新。如果您希望查看最新版本或更多相关内容,请访问原文地址:ReactiveStreams、Reactor、SpringWebFlux。感谢您的关注与支持! ReactiveStreams是…...
【潜意识Java】探寻Java子类构造器的神秘面纱与独特魅力,深度学习子类构造器特点
目录 一、子类构造器的诞生背景 (一)为啥要有子类构造器? (二)子类与父类构造器的关系 二、子类构造器的调用规则 (一)默认调用父类无参构造器 (二)显式调用父类构…...
OpenCV调整图像亮度和对比度
【欢迎关注编码小哥,学习更多实用的编程方法和技巧】 1、基本方法---线性变换 // 亮度和对比度调整 cv::Mat adjustBrightnessContrast(const cv::Mat& src, double alpha, int beta) {cv::Mat dst;src.convertTo(dst, -1, alpha, beta);return dst; }// 使用…...
HarmonyOs DevEco studio小技巧40--应用名称、图标与启动动画修改全攻略
一、引言 随着 HarmonyOS 的日益普及,越来越多的开发者投身于这个充满潜力的生态之中。而 DevEco Studio 作为 HarmonyOS 官方推出的集成开发环境,为开发者提供了一站式的开发体验。在应用开发过程中,一些细节上的设置,如应用名称…...
ESP-IDF学习记录(2)ESP-IDF 扩展的简单使用
傻瓜式记录一个示例的打开,编译,运行。后面我再一个个运行简单分析每个demo的内容。 1.打开示例代码 2.选择项目,文件夹 3.选择串口 4.选择调试方式 5.根据硬件GPIO口配置menuconfig 6.构建项目 7.烧录设备,选择串口UART方式 运行…...
2024 年最新 windows 操作系统搭建部署 nginx 服务器应用详细教程(更新中)
nginx 服务器概述 Nginx 是一款高性能的 HTTP 和 反向代理 服务器,同时是一个 IMAP / POP3 / SMTP 代理服务器。Nginx 凭借其高性能、稳定性、丰富的功能集、简单的配置和低资源消耗而闻名。 浏览 nginx 官网:https://nginx.org/ Nginx 应用场景 静态…...
基于ArcGIS Pro的SWAT模型在流域水循环、水生态模拟中的应用及案例分析;SWAT模型安装、运行到结果读取全流程指导
目前,流域水资源和水生态问题逐渐成为制约社会经济和环境可持续发展的重要因素。SWAT模型是一种基于物理机制的分布式流域水文与生态模拟模型,能够对流域的水循环过程、污染物迁移等过程进行精细模拟和量化分析。SWAT模型目前广泛应用于流域水文过程研究…...
Quartz任务调度框架实现任务动态执行
说明:之前使用Quartz,都是写好Job,指定一个时间点,到点执行。最近有个需求,需要根据前端用户设置的时间点去执行,也就是说任务执行的时间点是动态变化的。本文介绍如何用Quartz任务调度框架实现任务动态执行…...
10.MySQL事务
目录 什么是事务为什么有事务存在事务的版本支持事务的提交方式事务常见的操作方式事务异常验证与产出结论事务隔离性理论事务隔离级别的设置与查看事务隔离级别 - 读未提交事务隔离级别 - 读提交事务隔离级别 - 可重复读事务隔离级别 - 串行化MVCC机制3个记录隐藏字段undo日志…...
1.若依介绍
若依框架 好处: 1.快速构建 2.通用模块(登录、权限分配和校验、操作日志功能) 3.代码生成(定义好数据库表的结构,就能自动生成前后端对应的代码) 位置:系统工具-> 代码生成 若依版本 R…...
计算机网络实验室建设方案
一、计算机网络实验室拓扑结构 计算机网络综合实验室解决方案,是面向高校网络相关专业开展教学实训的综合实训基地解决方案。教学实训系统采用 B/S架构,通过公有云教学实训平台在线学习模式,轻松实现网络系统建设与运维技术的教学…...
【Rust自学】6.4. 简单的控制流-if let
喜欢的话别忘了点赞、收藏加关注哦,对接下来的教程有兴趣的可以关注专栏。谢谢喵!(・ω・) 6.4.1. 什么是if let if let语法允许将if和let组合成一种不太冗长的方式来处理与一种模式匹配的值,同时忽略其余模式。 可以…...
4-1 输出一组成绩中的最高分和最低分
第一行输入人数n,第二行输入每个人的成绩,用空格分开。输出所有成绩中的最高分和最低分。 输入格式: 第一行输入n,大于0的整数;第二行输入n个大于等于0,小于等于100的整数,用空格分开。 输出格式: 最高…...
数据结构:二叉树部分接口(链式)
目录 二叉树的遍历 1.通过前序遍历的数据构造二叉树 2.二叉树销毁 3. 二叉树节点个数 4. 二叉树叶子节点的个数 5.二叉树第k层节点个数 6.二叉树查找值为x的节点 7.二叉树的前/中/后序遍历 8.层序遍历 9.判断二叉树是否是完全二叉树 二叉树的遍历 前序、中序以及后序…...
音视频入门基础:MPEG2-PS专题(1)——MPEG2-PS官方文档下载
一、引言 MPEG2-PS(又称PS,Program Stream,程序流,节目流)是一种多路复用数字音频、视频等的封装容器。MPEG2-PS将一个或多个分组但有共同的时间基准的基本数据流 (PES)合并成一个整体流。它是…...
overleaf中文生僻字显示不正确,显示双线F
我是不想换全文字体的,只是一个生僻字显示不出来,就想要像word一样,把这个生僻字用包含这个生僻字的字体来显示就好了。 解决步骤: 1、使用如下宏包: \usepackage{xeCJK} %声明宏包,主要用于支持在XeTeX…...
代理arp(proxy arp)原理 及配置
openwrt下打开 arp代理方法 proxy arp概念打开方法openwrt下打开 arp代理方法proxy arp概念 定义 Proxy ARP(代理地址解析协议)是一种网络技术,它允许一个设备(通常是路由器)代表另一个设备来回应 ARP(地址解析协议)请求。工作原理 ARP 回顾:在正常的 ARP 过程中,当主…...
torch.tensor
torch.tensor 通过复制数据构造一个张量 (构造出的张量是一个没有自动微分(autograd )历史的张量,也称为叶张量,参考Autograd mechanics)。 torch.tensor(data, *, dtypeNone, deviceNone, requires_gra…...
Lucene 漏洞历险记:修复损坏的索引异常
作者:来自 Elastic Benjamin Trent 有时,一行代码需要几天的时间才能写完。在这里,我们可以看到工程师在多日内调试代码以修复潜在的 Apache Lucene 索引损坏的痛苦。 做好准备 这篇博客与往常不同。它不是对新功能或教程的解释。这是关于花…...
Github优质项目推荐(第十期)
文章目录 Github优质项目推荐(第十期)一、【postiz-app】,14.6k stars - 您的终极 AI 社交媒体调度工具二、【lobe-chat】,50.1k stars - AI 聊天框架三、【cobalt】,22.1k stars - 媒体下载器四、【build-your-own-x】…...
【已解决】“Content-Security-Policy”头缺失
1、作用 简称CSP,意为内容安全策略,通过设置约束指定可信的内容来源,降低异源文件攻击,例如:js/css/image等 2、相关设置值 指令名 demo 说明 default-src self cdn.example.com 默认策略,可以应用于js文件/图片…...
【每日学点鸿蒙知识】Web高度适配、变量声明规范、动画取消、签名文件、包体积优化相关
1、HarmonyOS Web页面高度适配? 在Web页面设置高度100%时,发现和Web控件的高度不一致,这个需要设置什么可以达到页面高度和Web容器高度一致 目前只支持两种web布局模式,分别为Web布局跟随系统WebLayoutMode.NONE和Web基于页面大…...
呼叫中心中间件免费体验测试和freeswitch部署方案
文章目录 前言联系我们部署freeswitch常见问题汇总 前言 大部分的用户想体验呼叫中心中间件的功能,却没有门路。这里可以分享呼叫中心中间件的部署链接,可供用户们免费体验测试。 联系我们 有意向了解呼叫中心中间件的用户,点击该链接可添加…...
游戏开发线性空间下PS工作流程
前言 使用基于物理的渲染,为了保证光照计算的准确,需要使用线性空间; 使用线性空间会带来一个问题,ui 在游戏引擎中的渲染结果与 PS 中的不一致: PS(颜色空间默认是sRGB伽马空间):…...
Mono里运行C#脚本7—MonoImageStorage结构解析
Mono里运行C#脚本7—MonoImageStorage结构解析 定义一个结构来保存EXE文件加载到内存的表示。 typedef struct { MonoRefCount ref; //引用计数,如果这个文件引用计数为0就可以删除。 /* key used for lookups. owned by this image storage. */ char *key; //HASH…...
Mac 查询IP配置,网络代理
常用命令 1.查询IP ifconfig | grep "inet" 2.ping查询 ping 172.18.54.19(自己IP) 3.取消代理,通过在终端执行以下命令,可以取消 Git 的代理设置 git config --global --unset http.proxy git config --global …...
WebRTC 环境搭建
主题 本文主要描述webrtc开发过程中所需的环境搭建 环境: 运行环境:ubuntu20.04 Node.js环境搭建 安装编译 Node.js 所需的依赖包: sudo apt-get updatesudo apt-get install -y build-essential libssl-dev下载 Node.js 源码: curl -sL https://…...
Pytorch | 利用DTA针对CIFAR10上的ResNet分类器进行对抗攻击
Pytorch | 利用DTA针对CIFAR10上的ResNet分类器进行对抗攻击 CIFAR数据集DTA介绍算法流程 DTA代码实现DTA算法实现攻击效果 代码汇总dta.pytrain.pyadvtest.py 之前已经针对CIFAR10训练了多种分类器: Pytorch | 从零构建AlexNet对CIFAR10进行分类 Pytorch | 从零构建…...
基于STM32的智能路灯系统控制的Proteus仿真
文章目录 一、智能路灯系统控制1.题目要求2.思路3.电路仿真3.1 未仿真3.2 开始仿真,显示屏显示初始化连接界面后,转为正常显示界面3.3 按下模式按键,切换为AUTO1模式3.4 再次按下模式按键,切换为HAND模式3.5 切换为时间设置界面&a…...
Kivy App开发遇到的问题
Python的安装 如图示,不要安装在带空格的路径下,Program Files 错误,后面安装kivy部件时导致找不到路径, 只能卸载重装. Python重装后将之前kivy的安装拷贝到新的目录下,不用重新安装 安装kivy,kivy的库都会安装在python的目录下,所以kivy项目设置编译器指向python 安装…...
如何在 ONLYOFFICE 中使用智谱 AI 人工智能插件以及其它实用插件来写文章
如何在 ONLYOFFICE 中使用智谱 AI 人工智能插件以及其它实用插件来写文章 书接上文: 为什么 F-35 拥有更大推力的引擎,只能达到 1.6 马赫速度,然而 F-16 却能达到 2.0 马赫? 这一片其实是我和人工智能一起合写的东西࿰…...
Clickhouse使用基础
# 查看操作系统版本 cat /etc/os-release# clickhouse版本 clickhouse -V# 登录clickhouse客户端 clickhouse-client -u xxx --password xxx -m # -m 或 --multiline:进入客户端后,运行输入多行sql语句建表 # 创建数据库 CREATE DATABASE IF NOT EXIST…...
Docker基础知识 Docker命令、镜像、容器、数据卷、自定义镜像、使用Docker部署Java应用、部署前端代码、DockerCompose一键部署
目录 1.Docker 2.镜像和容器 2.1 定义 2.2 开机自动启动容器 3.docker命令 3.1 docker run 参数说明 3.2 常见命令 3.3 命令演示 3.4 命令别名 4.Docker命令详解 5.数据卷 5.1 定义 5.2 数据卷的相关命令 5.3 数据卷命令 5.4 挂载本地目录或文件 5.4.1 定义 5.4.2 mysql容器目录…...