首页 > 其他分享 >线程

线程

时间:2024-04-14 15:55:24浏览次数:18  
标签:int void mutex pthread 线程 NULL

什么是线程:进程里面的一条执行流程

img

为什么要引入线程

这就不得说说进程的缺点了:

  • 进程间的切换,会导致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_tunsigned 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

img

线程的基本操作

  • 线程的创建
  • 线程的终止
  • 线程的等待
  • 线程的清理
  • 线程的游离

在正式介绍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;
}

img

可以看到,主线程和子线程的执行顺序是不确定的。

在第一种情况只打印了主线程的信息,这是因为主线程在结束时,进程就会终止(所有子线程都会终止)

这就不得不提一个在线程编程中的惯用法:主线程通常用于接收任务(或请求),然后将这些任务分配给其他子线程执行。主线程会等待所有子线程执行完毕后再结束,从而实现有序的退出。在实现线程的等待需要使用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;
}
img

当传递多个参数时,需要封装到一个数组(同类型)或结构体中。下面是一个简单的实例,但会出现一些问题,我们先运行一下。

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;
}
img

可以看到,子进程可以正常获取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;
}

imgimg

通过对比可以发现当使用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);

其中oldstateoldtype是传出参数,用于返回旧的状态和旧的取消类型。

取消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

什么时候执行线程清理函数呢?通过上面的例子不难发现:

  1. 使用pthread_exit退出
  2. 响应取消请求时,都会执行线程清理函数

注意:

  1. clear_push 和 clearup_pop一定要成对出现
  2. 从start_routine 返回不会执行线程清理函数
  3. 调用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);

怎样上锁

一个上锁的简单步骤就是:

  1. 需要先判断出临界区
  2. 在临界区前上锁
  3. 临界区后释放锁

为保证程序的逻辑,

下面是一个使用使用静态初始化方式上锁的例子:


请完善下面程序:

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

假设两个线程在执行时出现下面这种情况,导致竞争状态的产生,因此需要上锁。

img

下面,我们以银行的为例,来详细介绍一下锁的使用

锁的使用

实现银行取钱功能

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)则可能发生死锁,

img

程序的执行流程如下,由于tid1会一直等待tid2释放锁,tid2也会一直等待tid1释放锁,所以程序处于死锁状态。而主线程则在join处等待子线程结束。

img

死锁

死锁(deadlock)是指多个进程或线程在执行过程中造成的一种相互等待的现象,若无外力干涉,将无法向前推进。

死锁出现的原因:

  1. 互斥:至少有一个资源处于非共享模式。即,在一段时间内只有一个进程可以使用资源。如果另外一个进程请求该资源,请求者只能等待,直到资源被释放。
  2. 持有并等待:一个进程至少占有一个资源,并正在等待获取被其他进程持有的资源。
  3. 不能抢占:资源不能被抢占。一旦资源被占有,在它被使用完成并资源释放之前,不能被强行夺取
  4. 循环等待:存在一条进程资源的循环链,链中的每一个进程至少占有一个资源,该资源被链中的下一个进程锁请求。
img

以上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

相关文章

  • UE4 iOS打印出所有线程的调用栈
    在Xcode15.2中调试UE4游戏(Development包),执行btall打印出所有线程(共116个线程)的调用堆栈*thread#1,queue='com.apple.main-thread',stopreason=signalSIGSTOP*frame#0:0x00000001f9c7d178libsystem_kernel.dylib`mach_msg2_trap+8frame#1:0x00000001f......
  • Godot UI线程,Task异步和消息弹窗通知
    目录前言线程安全全局消息IOC注入消息窗口搭建最简单的消息提示简单使用仿ElementUIElementUI效果简单的Label样式如何快速加载多个相同节点修改一下,IOC按钮事件注册总结前言最近我在研究Godot的全局消息,然后发现Godot也是有UI线程限制的,只能在主线程的子线程里面修改UI。线......
  • Csharp线程
    CSharpe线程 目录CSharpe线程C#如何操作线程Thread1.Thread如何开启一个线程呢?2.Thread中常见的API3.thread的扩展封装threadpool一、.NETFramework2.0时代:出现了一个线程池ThreadPool二、线程池如何申请一个线程呢?三、线程等待四、线程池如何控制线......
  • C#开发AutoCAD插件多线程问题2种解决方法
    后台线程不允许操作界面,解决方案委托主线程来操作,在winform中用控件的Invoke方法。CAD插件里,可以用下面两种方法来实现: 方法一(推荐)://主线程:System.Threading.SynchronizationContextctx=null;ctx=Autodesk.AutoCAD.Runtime.SynchronizationContext.Current;if(ctx==......
  • 多线程-多个子线程执行结果插入List集合
    业务场景:将多个子线程的执行结果存入List,但是总会出现List集合的长度小于子线程的执行数的情况1、错误示例(多个线程同时操作同一个List对象,List是线程不安全)packageunitTest;importorg.assertj.core.util.Lists;importjava.util.List;importjava.util.concurrent.Coun......
  • ThreadPoolExecutor线程池解析
    ThreadPoolExecutor线程池解析一、ThreadPoolExecutor常见参数jdk中Executors提供了几种常用的线程池,底层都是ThreadPoolExecutor。publicThreadPoolExecutor(intcorePoolSize,//核心线程数intmaximumPoolSize,//最大线程数......
  • 多线程知识点
     1.多线程基本概念1)概念:多线程简单来说是一个程序具备同时执行多个功能的能力。在多线程中,这些功能被称为线程,每个线程都有自己的执行路径,它们可以并行(xíng)运行,同时共享程序的资源与内存。而在传统的单线程程序中,代码会顺序执行,一个任务完成后才会开始下一个任......
  • 单线程Reactor模型
    1.如何理解reactorreactor是一种设计模式。用于处理事件驱动的系统。reactor模式,主要有两个组件:reactor反应器:负责监听所有事件,当事件发生时,调用相应的处理程序。reactor本身时一个事件循环,负责处理I/O事件。handler处理程序:处理特点类型的事件。当reactor接收......
  • 多线程下写全局变量时,可借助sleep(0)让出cpu
    目录一个demo(对全局变量++)-->反汇编阅读cpu指令多个线程都去对全局变量++线程不挂起sleep(0)使线程挂起,让出cpu总结一下为啥不到10W?加锁版本近期在重读APUE,对unix下多线程有了新的理解用一个小demo来说明多线程下写全局变量时,让出cpu(使线程挂起)的重要性一个demo(对全局变量++)-......
  • 多线程(进阶篇&小白易懂版)
    文章目录多线程为什么要有多线程多线程案例线程通讯分传主线程通讯主传分关闭线程线程锁多线程概念:多线程就是多个线程同时工作的过程,我们可以将线程看作是程序的执行路径,每个线程都定义了一个独特的控制流,用来完成特定的任务。如果您的应用程序涉及到复杂且耗时的......