什么是线程:进程里面的一条执行流程
为什么要引入线程
这就不得说说进程的缺点了:
- 进程间的切换,会导致TLB、CPU的Cache失效
- 进程之间是隔离的,进程间的通信需要打破隔离的壁障
而相较于进程而言,
-
线程的创建和销毁是轻量级的。
-
同一进程的线程之间的切换,不会导致
TLB
失效、也不会导致CPUcache
失效. -
线程之间共享进程的所有资源,所以线程之间通信的代价小
获取进程的标识
进程:getpid()
, getppid()
线程:pthread_self()
NAME
pthread_self - obtain ID of the calling thread
SYNOPSIS
#include <pthread.h>
pthread_t pthread_self(void);
Compile and link with -pthread.
下面通过一个简单的例子来了解一下pthread_self()
怎么使用。
在使用之前,我们需要先知道pthread_t
是什么类型,可以通过下面的命令获取
gcc -E pthread_self.c | grep -nE "pthread_t"
1305:typedef unsigned long int pthread_t;
可以看到pthread_t
是unsigned long
类型
int main(int argc, char* argv[])
{
printf("pid = %d, ppid = %d\n", getpid(), getppid());
pthread_t tid = pthread_self();
printf("tid = %lu\n", tid);
return 0;
}
需要注意的是,为保证可移植性在编译时需要再Makefile
文件中加入-pthread
线程的基本操作
- 线程的创建
- 线程的终止
- 线程的等待
- 线程的清理
- 线程的游离
在正式介绍pthead
系列函数时,需要了解一个pthread
的设计原则:
- 成功:返回0
- 失败:返回错误码,不会设置errno
线程的创建
在创建线程时,使用到pthread_creat()
函数。
NAME
pthread_create - create a new thread
SYNOPSIS
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
Compile and link with -pthread.
thread
: 作为传出参数,用于传出创建新线程的id
,注意是指针类型。
attr
: 线程的属性,传入参数,一般填NULL,表示采用默认属性
start_routine
: 线程的入口函数,类型和参数都是void*
类型,在C语言中指通用指针,可以传递或返回任意类型的值。
arg
:入口函数的参数,没有参数传NULL
通过一个简单的例子来了解一下thread_create()
函数的使用
#include <func.h>
void printf_ids(char* prefix) {
printf("%s pid = %d, ppid = %d, thread_id = %lu\n",
prefix, getpid(), getppid(), pthread_self());
}
void* start_routine(void* arg){
printf_ids("new thread");
return NULL;
}
int main(int argc, char* argv[])
{
// 创建线程
pthread_t tid;
int err = pthread_create(&tid, NULL, start_routine, NULL);
if (err) {
error(1, err, "pthread_creat");
}
// 主线程
printf("main pthread\n");
return 0;
}
可以看到,主线程和子线程的执行顺序是不确定的。
在第一种情况只打印了主线程的信息,这是因为主线程在结束时,进程就会终止(所有子线程都会终止)。
这就不得不提一个在线程编程中的惯用法:主线程通常用于接收任务(或请求),然后将这些任务分配给其他子线程执行。主线程会等待所有子线程执行完毕后再结束,从而实现有序的退出。在实现线程的等待需要使用pthread_join()
函数,我们在后面介绍。
需要注意的是:主线程的执行流程是从main
函数开始,而子线程的执行流程从入口函数开始。
在这,提供一个技巧,在64位计算机中,如果传递的参数不超过8个字节,可以将其分装到一个指针中传递。下面是一个实例:
// pthread_create2.c
void printf_ids(char* prefix) {
printf("%s pid = %d, ppid = %d, thread_id = %lu\n",
prefix, getpid(), getppid(), pthread_self());
}
void* start_routine(void* arg){
int num = (int)arg;
printf_ids("new thread");
printf("num = %d\n", num);
return NULL;
}
int main(int argc, char* argv[])
{
// 创建线程
pthread_t tid;
int err = pthread_create(&tid, NULL, start_routine, (void*)4096);
if (err) {
error(1, err, "pthread_creat");
}
// 主线程
printf("main pthread\n");
sleep(2);
return 0;
}
当传递多个参数时,需要封装到一个数组(同类型)或结构体中。下面是一个简单的实例,但会出现一些问题,我们先运行一下。
typedef struct {
int a;
double b;
char* message;
} Paras;
void printf_ids(char* prefix) {
printf("%s pid = %d, ppid = %d, thread_id = %lu\n",
prefix, getpid(), getppid(), pthread_self());
}
void* start_routine(void* arg){
Paras* arguments = (Paras*)arg;
printf_ids("new thread");
printf("a = %d, b = %lf, message = %s\n",
arguments->a, arguments->b, arguments->message);
return NULL;
}
int main(int argc, char* argv[])
{
// 创建线程
pthread_t tid;
Paras arguments = {1, 3.14, "Hello"};
int err = pthread_create(&tid, NULL, start_routine, &arguments);
if (err) {
error(1, err, "pthread_creat");
}
// 主线程
printf("main pthread\n");
sleep(2);
return 0;
}
可以看到,子进程可以正常获取arguments
的值。这是因为arguments
保存在主线程的栈上,由于线程之间资源共享,因此子线程可以成功获取到arguments
的值
但需要注意的是,不要轻易访问其他线程的栈空间。因为当访问的线程终止时,其对应的堆空间也会被释放。
若要在线程之间共享数据,可以放到进程的堆空间或进程的代码段和数据段。
若要存放到堆空间,需确保堆空间有且只能被其中一个进程free
.
线程的等待
要实现等待某线程完成,需要调用pthread_join()
函数
SYNOPSIS
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
Compile and link with -pthread.
第一个参数:thread
指定要等待线程的tid
第二个参数retval
:是void**
类型,是传出参数,接收返回值(void*
,任意类型),不想接收返回值置为NULL。
下面是一个简单的例子
#include <func.h>
void* start_routine(void* arg) {
printf("new thread start\n");
sleep(3); // 让子线程sleep(3),看主线程是否提前结束
printf("new thread end\n");
return NULL;
}
int main()
{
pthread_t tid;
pthread_create(&tid, NULL, start_routine, NULL);
// 等待tid终止,如果tid没有终止,主线程就会一直阻塞
pthread_join(tid, NULL);
return 0;
}
执行结果
new thread start
new thread end
可以发现,当强制让子线程sleep(3)
时,主线程会一直等待子线程结束,否则会一直阻塞。
线程的终止
引起进程终止的事件有:从main
返回、调用exit()
、使用信号量机制kill -SIGKILL pid
引起线程终止的事件有:
-
从
start_routine
返回 -
调用
pthread_exit()
、 -
调用
pthread_cancel()
,一个线程给另一个线程发送取消请求,若响应则终止
pthread_exit
NAME
pthread_exit - terminate calling thread
SYNOPSIS
#include <pthread.h>
void pthread_exit(void *retval);
Compile and link with -pthread.
可以看到pthread_exit()
的参数是void*
类型,是一个传出参数。通常是线程的退出状态或其他一些有用的结果。在同一进程中的其他线程,可以使用pthread_join()
接收。
下面是一个分别是从start_routine
返回以及使用pthread_exit()
退出的简单示例。
// 从start_routine返回
void* start_routine(void* arg){
printf("new thread start\n");
printf("new thread end\n");
return NULL;
}
int main(int argc, char* argv[])
{
// 创建线程
pthread_t tid;
int err = pthread_create(&tid, NULL, start_routine, NULL);
if (err) {
error(1, err, "pthread_creat");
}
// 主线程
printf("main pthread: create new ptherad\n");
sleep(3); // 等待子线程结束
return 0;
}
// 调用pthread_exit()
void* start_routine(void* arg){
printf("new thread start\n");
pthread_exit(1);
printf("new thread end\n");
//return NULL;
}
int main(int argc, char* argv[])
{
// 创建线程
pthread_t tid;
int err = pthread_create(&tid, NULL, start_routine, NULL);
if (err) {
error(1, err, "pthread_creat");
}
// 主线程
printf("main pthread: create new ptherad\n");
sleep(3); // 等待子线程结束
return 0;
}
通过对比可以发现当使用pthread_eixt()
显示地退出线程时,调用即退出。而使用start_routine
返回,线程可以通过其他线程退出。
pthread_cancel
(了解)
SYNOPSIS
#include <pthread.h>
int pthread_cancel(pthread_t thread);
Compile and link with -pthread.
通过下面的例子来简单了解一下这个函数
#include <func.h>
void* start_routine(void* arg) {
for(;;) {
}
return NULL;
}
int main(int argc, char* argv[])
{
pthread_t tid;
int err = pthread_create(&tid, NULL, start_routine, NULL);
if (err) {
error(1, err, "pthread_create");
}
// 主线程发送取消请求给子线程
err = pthread_cancel(tid);
if (err) {
error(1, err, "pthread_cancel");
}
// 等待子线程终止
pthread_join(tid, NULL);
return 0;
}
在上面的例子中,由于子线程的入口函数没有响应主线程的终止信号,因此主线程会在pthread_join(tid, NULL)
处阻塞等待子线程结束。
但如果将子线程的入口函数修改成下面的代码,会结束子线程的执行。
void* start_routine(void* arg) {
for(;;) {
sleep(1);
}
return NULL;
}
这是由于sleep(1)
是一个取消点。通过阅读pthread_cancel
的man手册,可以看到是否响应,以及何时响应取决于线程的属性。
The pthread_cancel() function sends a cancellation request to the thread thread.
Whether and when the target thread reacts to the cancellation request depends on two
attributes that are under the control of that thread: its cancelability state and
type.
- 是否响应:取消
state
- 何时响应:取消
type
修改者两个属性可以通过下面两个函数进行。
#include <pthread.h>
int pthread_setcancelstate(int state, int *oldstate);
int pthread_setcanceltype(int type, int *oldtype);
其中oldstate
和oldtype
是传出参数,用于返回旧的状态和旧的取消类型。
取消stste
:的取值有下面两个
PTHREAD_CANCEL_ENABLE (默认) 响应取消状态
PTHREAD_CANCEL_DISABLE 不响应取消状态
取消type
: 的取值有:
PTHREAD_CANCEL_DEFERRED (默认) 在取消点响应
PTHREAD_CANCEL_ASYNCHRONOUS 可以在任意点响应
取消点可以查看man手册man 7 pthread
线程的清理
要实现线程的清理,需要先通过下面两个函数注册线程清理函数
#include <pthread.h>
void pthread_cleanup_push(void (*routine)(void *), void *arg);
第一个参数:要执行的清理函数
第二个参数:传递给清理函数的参数
void pthread_cleanup_pop(int execute); 移除最近添加的清理处理程序。如果它的参数是非零值,则它还会执行清理处理程序
execute:是一个标志,用于指示是否执行清理函数。
0: 不执行
1: 执行
通过一个简单例子,来了解一些线程的清理
void cleanup(void* arg){
char* msg = (char*)arg;
puts(msg);
}
void* start_routine(void* arg) {
// 注册线程清理函数
pthread_cleanup_push(cleanup, "111");
pthread_cleanup_push(cleanup, "222");
// 2. 执行线程逻辑
printf("new thread push\n");
sleep(1);
printf("new thread pop\n");
// 3. 线程退出
//pthread_exit(NULL);
//return NULL;
pthread_cleanup_pop(0);
pthread_cleanup_pop(0);
}
int main(int argc, char* argv[])
{
pthread_t tid;
int err = pthread_create(&tid, NULL, start_routine, NULL);
// 主线程发送取消请求给子线程
err = pthread_cancel(tid);
// 等待子线程终止
pthread_join(tid, NULL);
return 0;
}
使用pthread_exit()
退出子线程的执行结果
new thread push
new thread pop
222
111
使用return NULL
退出子线程的执行结果
new thread push
new thread pop
使用pthread_cancel()
在退出点退出子线程的执行结果
new thread push
222
111
什么时候执行线程清理函数呢?通过上面的例子不难发现:
- 使用
pthread_exit
退出 - 响应取消请求时,都会执行线程清理函数
注意:
- clear_push 和 clearup_pop一定要成对出现
- 从start_routine 返回不会执行线程清理函数
- 调用pthread_clearnup_pop(1)时,不会造成线程终止
线程的游离
线程的游离是指:断开线程之间的attached
,使线程处于游离状态。
#include <pthread.h>
int pthread_detach(pthread_t thread);
typedef struct{
int id;
char name[20];
char gender;
} Student;
void print_stu_info(Student* s){
printf("%d %s %c",
s->id,
s->name,
s->gender);
}
void* start_routine(void* arg) {
printf("new thread start\n");
printf("new thread end\n");
Student* s = (Student*)malloc(sizeof(Student));
s->id = 1;
strcpy(s->name, "hello");
s->gender = 'f';
pthread_exit(s);
}
int main(int argc, char* argv[])
{
pthread_t tid;
int err = pthread_create(&tid, NULL, start_routine, NULL);
if (err) {
error(1, err, "pthread_create");
}
// detach 线程
err = pthread_detach(tid);
if (err) {
error(1, err, "pthread_detach");
}
Student* retval;
// 等待子线程终止
err = pthread_join(tid, (void**)&retval);
if (err) {
error(1, err, "pthread_join");
}
// 打印retval
print_stu_info(retval);
free(retval);
return 0;
}
在打印之前已经游离了线程,因此会join失败。
./pthread_detach: pthread_join: Invalid argument
同步、异步、并发、并行
程序的运行方式:异步、同步、并发、并行
编程范式:贯通式,面向对象、函数式、范型
异步:任务之间相互独立,不需要等待前一个任务完成就可以开始执行下一个任务,异步模式下事件的执行顺序一般是随机的,一个任务的执行不会阻塞其他任务的进行。
同步:时间之间的执行顺序是确定的,每一个任务需要等待前一个任务完成才可以开始执行。可以看作它们共同遵循一定的规则,可以让程序有秩序的执行。同步的基础是通信,通信的基础是共享资源。
并发:并发是一种现象。两个执行流程在一段时间内可以交替执行。
并行:是一种技术。指的是在同一个时间点可以执行多个任务。
由于线程间的异步执行,从而导致竟态条件的产生。
竟态条件
竟态条件是指:有多个执行流程同时访问共享资源,从而导致执行的结果由执行流程访问共享资源的先后顺序决定。
临界区
为避免竟态条件的额产生,提出了临界区的概念。
临界区:访问共享资源的一段代码,资源通常是一个变量或数据结构
锁
为了实现并发编程,我们希望原子式执行一系列指令,但由于单处理器上的中断(或多个线程在多处理器上并发执行),很难实现。
因此,锁(lock)直接解决这个问题。
什么是锁
锁是一个变量,因此需要声明一个某种类型的锁变量(lock variable)才能使用。
锁变量保存了锁在某一时刻的状态。
通过给临界区加锁,可以保证临界区内只有一个线程活跃,从而保证对临界区的访问是原子的。锁将原本由操作系统调度的混乱状态变为可控。
互斥锁
SYNOPSIS
#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
lock
: 上锁。先尝试获取锁,若锁被占有,会一直阻塞,直到获取锁
unlock
: 释放锁
trylock
: 尝试上锁。尝试获取锁,获取不成功立即返回。
初始化锁
在使用锁之前,需要正确初始化锁。
SYNOPSIS
#include <pthread.h>
// 销毁锁
int pthread_mutex_destroy(pthread_mutex_t *mutex);
// 动态初始化锁
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr);
// 静态初始化锁
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
动态初始化锁
mutex
: 指向将要被初始化的互斥锁变量的指针attr
: 可选属性。如果设置为NULL
,初始化为默认属性
需要注意的是,若采用动态初始化,需要使用pthread_mutex_destory()
销毁锁
销毁锁
int pthread_mutex_destroy(pthread_mutex_t *mutex);
怎样上锁
一个上锁的简单步骤就是:
- 需要先判断出临界区
- 在临界区前上锁
- 临界区后释放锁
为保证程序的逻辑,
下面是一个使用使用静态初始化方式上锁的例子:
请完善下面程序:
int main(void) {
long long* value = (long long*) calloc(1, sizeof(long long));
// 创建两个线程
// 第一个线程执行 (*value)++ 10000000次
// 第二个线程叶执行 (*value)++ 10000000次
// 主线乘等待两个子线程结束。并打印 *value 的值。
}
// 静态初始化
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
// 线程函数
void* start_routine(void* arg) {
long long* value = (long long*)arg;
for (int i = 0; i < 10000000; i++) {
// 上锁
pthread_mutex_lock(&mutex);
(*value)++;
// 释放锁
pthread_mutex_unlock(&mutex);
}
return NULL;
}
int main(void) {
long long* value = (long long*) calloc(1, sizeof(long long));
// 创建两个线程
pthread_t tid1, tid2;
pthread_create(&tid1, NULL, start_routine, value);
pthread_create(&tid2, NULL, start_routine, value);
// 主线乘等待两个子线程结束。并打印 *value 的值。
err = pthread_join(tid1, NULL);
err = pthread_join(tid2, NULL);
// 输出最终结果。
printf("*value = %lld\n", *value);
free(value);
}
执行结果如下:
*value=2000000
如果不上锁呢?
多次执行:
*value = 11256461
*value = 11863935
*value = 11400809
可以发现,每次执行结果都不同,发生的原因是什么呢?
查看一下执行+1操作的汇编代码:
mov 0x8049a1c, %eax
add $0x1, %eax
mov %eax, 0x8049a1c
假设两个线程在执行时出现下面这种情况,导致竞争状态的产生,因此需要上锁。
下面,我们以银行的为例,来详细介绍一下锁的使用
锁的使用
实现银行取钱功能
typedef struct {
int id;
int balance;
} Account;
Account acct1 = {1, 100};
pthread_t mutex = PTHREAD_MUTEX_INITIALIZER;
// 取钱
int withdraw(Account* acct, int money) {
pthread_mutex_lock(&mutex);
// 检验
if (acct->balance < money) {
return 0;
}
// 取钱
acct->balance -= money;
pthread_mutex_unlock(&mutex);
return money;
}
void* start_routine(void* arg) {
int money = (int)arg;
int n = withdraw(&acct1, money);
printf("%lu withdraw $%d\n", pthread_self(), n);
return NULL;
}
void* start_routine1(void* arg) {
int money = (int)arg;
int n = withdraw(&acct1, money);
printf("%lu withdraw $%d\n", pthread_self(), n);
return NULL;
}
int main(int argc, char* argv[])
{
pthread_t tid1, tid2;
pthread_create(&tid1, NULL, start_routine, (void*)100);
pthread_create(&tid2, NULL, start_routine1, (void*)100);
// 主线程等待子线程
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
return 0;
}
这样就可以使取钱操作正确进行。
但存在一个问题:只有一把锁,因此在同一时刻只能有一个人可以取钱,导致并发量很低,在实际环境中很不适用。
在实际环境中,应该是每个用户都有自己的锁,自己取钱时不影响其他人。可以修改代码
typedef struct {
int id;
int balance;
pthread_mutex_t mutex;
} Account;
Account acct1 = {1, 100};
//pthread_t mutex = PTHREAD_MUTEX_INITIALIZER;
// 取钱
int withdraw(Account* acct, int money) {
pthread_mutex_lock(&acct->mutex);
// 检验
if (acct->balance < money) {
pthread_mutex_unlock(&acct->mutex);
return 0;
}
// 取钱
acct->balance -= money;
pthread_mutex_unlock(&acct->mutex);
return money;
}
下面实现一个简单的转账功能。
// 转账
int transfer(Account* acctA, Account* acctB, int money) {
pthread_mutex_lock(&acctA->mutex);
pthread_mutex_lock(&acctB->mutex);
if (acctA->balance < money) {
pthread_mutex_unlock(&acctA->mutex);
pthread_mutex_unlock(&acctB->mutex);
return 0;
}
acctA->balance -= money;
acctB->balance += money;
pthread_mutex_unlock(&acctA->mutex);
pthread_mutex_unlock(&acctB->mutex);
return money;
}
但如果在线程A执行期间强行sleep(1)则可能发生死锁,
程序的执行流程如下,由于tid1会一直等待tid2释放锁,tid2也会一直等待tid1释放锁,所以程序处于死锁状态。而主线程则在join处等待子线程结束。
死锁
死锁(deadlock)是指多个进程或线程在执行过程中造成的一种相互等待的现象,若无外力干涉,将无法向前推进。
死锁出现的原因:
- 互斥:至少有一个资源处于非共享模式。即,在一段时间内只有一个进程可以使用资源。如果另外一个进程请求该资源,请求者只能等待,直到资源被释放。
- 持有并等待:一个进程至少占有一个资源,并正在等待获取被其他进程持有的资源。
- 不能抢占:资源不能被抢占。一旦资源被占有,在它被使用完成并资源释放之前,不能被强行夺取
- 循环等待:存在一条进程资源的循环链,链中的每一个进程至少占有一个资源,该资源被链中的下一个进程锁请求。
以上4个条件缺一不可。因此破除死锁只需破坏其中一个条件既可。
破坏循环等待
如何破坏循环等待,最常用的方式是按照一定顺序上锁。修改转账代码如下。
// 转账
int transfer(Account* acctA, Account* acctB, int money) {
if (acctA->id < acctB->id) {
pthread_mutex_lock(&acctA->mutex);
sleep(1); // 切换
pthread_mutex_lock(&acctB->mutex);
} else {
pthread_mutex_lock(&acctB->mutex);
sleep(2); // 切换
pthread_mutex_lock(&acctA->mutex);
}
if (acctA->balance < money) {
pthread_mutex_unlock(&acctA->mutex);
pthread_mutex_unlock(&acctB->mutex);
return 0;
}
acctA->balance -= money;
acctB->balance += money;
pthread_mutex_unlock(&acctA->mutex);
pthread_mutex_unlock(&acctB->mutex);
return money;
}
破坏不能抢占
// 破坏不能抢占
start:
pthread_mutex_lock(&acctA->mutex);
sleep(1);
int err = pthread_mutex_trylock(&acctB->mutex);
if (err) {
pthread_mutex_unlock(&acctA->mutex);
// 停留一个随机时间
int nsec = rand() % 5;
sleep(nsec);
goto start;
}
破坏持有并等待
破坏持有并等待,可以通过要么一次获得所有锁,要么一次也不获取。
因此,可以定义一个全局锁,将所有获取锁的操作变为原子操作
// 2. 持有并等待
pthread_mutex_lock(&protection);
pthread_mutex_lock(&acctA->mutex);
sleep(1); // 切换
pthread_mutex_lock(&acctB->mutex);
pthread_mutex_unlock(&protection);
破坏互斥
破坏互斥,需要硬件的支持。
条件变量
锁并不是并发程序设计所需的唯一原语。详细来说,在很多情况下,线程需要检查某一条件满足之后,才会继续运行。
如何等待一个条件满足呢?
简单的方法是自旋直到条件满足,下面是一个简单例子。
volatile int done = 0;
void* start_continue(void* arg) {
printf("child\n");
done = 1;
return NULL;
}
int main(int argc, char* argv[]) {
printf("parent begin\n");
pthread_t tid;
pthread_create(&tid, NULL, child, NULL); // create child
while (done == 0)
; // 自旋
printf("parent end\n");
return 0;
}
线程也可以使用条件变量来等待一个条件变为真。使用条件变量,需要等待和唤醒机制。
int pthread_cond_broadcast(pthread_cond_t *cond); // 等待
int pthread_cond_signal(pthread_cond_t *cond); // 唤醒
在使用条件变量时,必须有另外一个与此条件相关的锁,在使用pthread_cond_wait()
或pthread_cond_signal()
函数时,必须拥有该锁。
典型的用法如下:
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
pthread_mutex_lock(&lock);
while (ready == 0) {
pthread_cond_wait(&cond, &lock);
}
pthread_mutex_unlock(&lock);
在初始化相关的锁和条件之后,一个线程检查变量 ready 是否准备好。如果没有,那么线程只是简单地调用等待函数以便休眠,直到其他线程唤醒它。
唤醒线程的代码可以运行在另外某个线程中
Pthread_mutex_lock(&lock);
ready = 1;
Pthread_cond_signal(&cond);
Pthread_mutex_unlock(&lock);
等待调用将锁(互斥锁)作为其第二个参数,而信号调用仅需要一个条件。
这是因为,等待调用除了使调用线程进入休眠状态外,还会让调用者在睡眠时释放锁。如果不这样,其他线程就不会获得锁将其唤醒。
但是,在被唤醒之后返回之前,pthread_cond_wait()会重新获取该锁
条件变量的初始化和销毁
在使用条件变量之前需要对其进行初始化
SYNOPSIS
#include <pthread.h>
int pthread_cond_destroy(pthread_cond_t *cond);
// 动态初始化
int pthread_cond_init(pthread_cond_t *restrict cond,
const pthread_condattr_t *restrict attr);
// 静态初始胡
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
通知机制(条件满足)
当条件满足时,通知等待该条件成立的线程
int pthread_cond_signal(pthread_cond_t *cond);
当cond
满足时,会唤醒一个等待该条件的线程。
主要注意的是:内核在实现时,为了性能考虑,可能会唤醒多个等待的线程
int pthread_cond_broadcast(pthread_cond_t *cond);
会唤醒所有等待该条件的线程。
等待机制(条件不满足)
条件一直不成立,就会一直等待
int pthread_cond_wait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex);
pthread_cond_t *restrict cond
: 指向条件变量的指针。
pthread_mutex_t *restrict mutex
: 互斥锁
生产者/消费者(有界缓冲区)问题
假设一个或多个生产者线程和一个或多个消费者线程。生产者将生产的数据项放入缓冲区;消费者从缓冲区中取走数据项,以某种方式消费。
因为缓冲区是共享资源,所以必须通过同步机制来进行访问。以免产生竟态条件。
cond_t cond;
mutex_t mutex;
void *producer(void *arg) {
int i;
for (i = 0; i < loops; i++) {
Pthread_mutex_lock(&mutex); // p1
if (count == 1) // p2
Pthread_cond_wait(&cond, &mutex); // p3
put(i); // p4
Pthread_cond_signal(&cond); // p5
Pthread_mutex_unlock(&mutex); // p6
}
}
void *consumer(void *arg) {
int i;
for (i = 0; i < loops; i++) {
Pthread_mutex_lock(&mutex); // c1
if (count == 0) // c2
Pthread_cond_wait(&cond, &mutex); // c3
int tmp = get(); // c4
Pthread_cond_signal(&cond); // c5
Pthread_mutex_unlock(&mutex); // c6
printf("%d\n", tmp);
}
}
标签:int,void,mutex,pthread,线程,NULL
From: https://www.cnblogs.com/notob/p/18129936