2024年1月

设计模式是通用的、可复用的代码设计方案,也可以说是针对某类问题的解决方案,因此,掌握好设计模式,可以帮助我们编写更健壮的代码。

wiki中将
设计模式
分为四类,分别是:

  • 创建模式(creational patterns)
  • 结构模式(structural patterns)
  • 行为模式(behavioral patterns)
  • 并发模式(concurrency patterns)

策略模式和状态模式属于其中的行为模式,行为模式——从名称上就可以看出——与动作、操作有关。

这两种模式我接触下来,感觉存在一定的相似性。状态模式中通常会存在一个内部状态,状态改变时行为也会发生改变,而策略模式是针对不同条件下的行为进行封装。总的来说,两者都是在不同条件下有不同的行为。接下来我们分别来看一下。

策略模式

首先看策略模式,根据针对它的概述,貌似就是
一系列算法的封装

Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients that use it.

当然策略模式不止关于算法的定义,还有对算法的调用。关于这一点我们在策略模式对应的
wiki:Strategy pattern
页面也能看到相应的描述。

Instead of implementing a single algorithm directly, code receives run-time instructions as to which in a family of algorithms to use.

这句话的大概意思是:代码在运行时接收指令,决定使用一系列算法中的哪一种。在这里一种算法就是对应一个策略。

这个描述很容易让人联想到代码中常见的条件语句
if-elseif-else
,在条件分支根据不同的指令执行不同的操作。但是很显然,既然是一系列的算法,那就说明可能会有很多、甚至是大量的条件,那么可想而知,如果我们直接使用
if-else
语句来编写执行代码的话,这部分代码会非常长,并且这会破坏软件设计原则中的单一功能原则,这段代码除了判断条件,还要根据不同的条件执行不同的细节操作。

策略模式中的“策略”,其实指的就是算法。然后条件判断作为一个入口,去调用对应的“策略”。所以我们看到wiki中有下面这段描述:

Typically, the strategy pattern stores a reference to some code in a data structure and retrieves it. This can be achieved by mechanisms such as the native function pointer, the first-class function, classes or class instances in object-oriented programming languages, or accessing the language implementation's internal storage of code via reflection.

这段话的意思是:策略模式会在数据结构中存储对某些代码的引用,并对其进行检索。这可以通过本地函数指针、一级函数、面向对象编程语言中的类或类实例,或通过反射 访问 语言实现的代码内部存储等机制来实现。

简单来理解,就是把一系列相关操作封装成函数,一个函数就对应一个算法的实现。

策略模式与开闭原则

我们知道在软件设计原则中有一条是:对扩展开放,对修改封闭。策略模式与开闭原则是一致的。

According to the strategy pattern, the behaviors of a class should not be inherited. Instead, they should be encapsulated using interfaces.

根据策略模式,类的行为不应被继承,它们应使用接口进行封装。也就是说,我们最好不要对父类本身做修改,而是使用接口对子类的行为进行扩展。在wiki中使用了Java来举例子,在JavaScript中也可以做类似的处理,比如不把对应的策略函数加在对象自身,而是统一放在一个地方进行调用,也就是上面所说的
在数据结构中存储对某些代码的引用
。比如下面这个例子:

某商场中的商品在不同阶段的价格满足固定的逻辑,做了以下封装:

const priceProcessor = {
  pre(originPrice) {
    if (originPrice >= 100) {
      return originPrice - 20;
    }
    return originPrice * 0.9;
  },
  onSale(originPrice) {
    if(originPrice >= 100) {
      return originPrice - 30;
    }
    return originPrice * 0.8;
  },
  back(originPrice) {
    if(originPrice >= 200) {
      return originPrice - 50;
    }
    return originPrice;
  },
  fresh(originPrice) {
    return originPrice * 0.5;
  },
};

pre、onSale、back、fresh分别代表了在预热、大促、返场、尝鲜四种阶段下的价格处理。

在上述代码中,我们在
priceProcessor
这个数据结构中存储了针对不同阶段下对价格的处理逻辑,也就是各种封装的函数。我们可以调用这些引用,从而实现在不同条件下执行不同的算法。

这样,当我们策划新的促销活动时,只需要在
priceProcessor
这个结构中增加新的处理逻辑,而不需要影响商品对象和其他的处理逻辑,并且这样子处理后,测试流程中就只需要测试新的处理逻辑,而不需要回归测试整体功能。

每个处理逻辑有单独的函数实现,这也方便不同条件下的算法替换,比如在某次商场大促,想要使用返场的价格,就可以直接调用
priceProcessor.back
方法,而不需要编写重复冗余的代码。

状态模式

策略模式的核心很简单,就是单一功能的函数封装。接下来我们继续看状态模式。

The state pattern is a behavioral software design pattern that allows an object to alter its behavior when its internal state changes.

状态模式也很简单,就是允许对象在内部状态发生变化时改变其行为。

状态模式的“状态”,就是指对象内部的状态,也就是说,这个模式针对的是存在内部状态的对象。

The state pattern can be interpreted as a strategy pattern, which is able to switch a strategy through invocations of methods defined in the pattern's interface.

我们可以看到wiki这部分也有说,状态模式可以解释为一种策略模式。其实上也就是说,根据不同的状态切换策略;在策略模式下,是根据不同的条件切换不同策略,这个是广泛意义下的条件,而状态模式中,不同条件就特定为不同的内部状态。

这样处理后,就不需要使用条件语句了,可以直接通过不同状态映射不同的行为。

在状态模式的
wiki
页面中,也列举了它所解决的主要问题:

The state pattern is set to solve two main problems:[
4]

  • An object should change its behavior when its internal state changes.
  • State-specific behavior should be defined independently. That is, adding new states should not affect the behavior of existing states.

在某类场景中,第一,对象应根据其内部状态的改变来改变其行为。

第二,特定于状态的行为应独立定义。也就是说,添加新状态不应影响现有状态的行为。

这里看第二点,其实和策略模式的场景很类似。

对应这两个问题,状态模式描述了以下解决方案:

In this, the pattern describes two solutions:

  • Define separate (state) objects that encapsulate state-specific behavior for each state. That is, define an interface (state) for performing state-specific behavior, and define classes that implement the interface for each state.
  • A class delegates state-specific behavior to its current state object instead of implementing state-specific behavior directly.

第一,是定义独立的状态对象,为每个状态封装特定于状态的行为。

第二,类将特定于状态的行为委托给其当前的状态对象,而不是直接实现特定于状态的行为。

因此,
状态模式中的关键就在于对状态对象的实现
。比如下面这个例子:

一个养生壶有不同的功能,当切换不同的功能时我们可以认为它处于不同的工作状态。

class HealthPot {
  constructor() {
    this.state = new State();
  }

  changeState(status) {
    this.state.status = status;
    // 若状态不存在,则返回
    if(!this.state.statusToProcessor[status]) {
      return;
    }
    this.state.statusToProcessor[status]();
   }
}

class State {
  constructor() {
    this.status = '';
  }
  statusToProcessor = {
    water() {
        console.log('煮开水');
    },
    flowersTea() {
        console.log('煮花草茶');
    },
    fruitsTea() {
        console.log('煮水果茶');
    },
    keepWarm() {
        console.log('保温');
    }
  }
}

const hp = new HealthPot();
hp.changeState('flowersTea');

在上述代码中,如何实现特定状态的行为与养生壶本身无关,只与状态对象有关。养生壶的行为只是改变状态,并调用对应方法,这样如果后续有新的状态增加,也不用去修改养生壶这个具体的对象,相当于一种拆分行为。

这其实就有点类似于vue中的状态管理工具vuex。

总结

策略模式和状态模式两者存在一定的相似性,但是策略模式封装的函数其独立性会更高,而状态模式中封装的函数依赖于主体的状态,具体操作代码也可能依赖主体的其他属性,比如养生壶例子中,执行各种功能时,需要保证壶中有水,并且要判断是否通电中等等。

简单来说就是一种拆分、封装的行为,满足软件设计原则中的单一职责和开闭原则。

一般在应用开发初期,由于功能简单,开发者可能不会特别在意拆分,并且通常而言不太提倡提前优化,所以会在之后的维护和迭代中,应用这些模式来优化和重构代码;但是有时设计良好的代码,会更便于代码的维护。

本文介绍查看
Visual Studio
软件
_MSC_VER
值的方法。

_MSC_VER
是微软公司推出的
C/C++
编译器——
MSVC
编译器的一个内置宏,其值表示当前
Visual Studio
软件中
MSVC
编译器的具体版本。不同的
Visual Studio
软件版本对应着不同的
MSVC
编译器版本——无论是不同发布年份的版本(例如
Visual Studio 2017

Visual Studio 2019
),还是同一发布年份的不同版本(例如
Visual Studio 2017 version 15.8

Visual Studio 2017 version 15.9
),其
MSVC
编译器版本都有差异,因此其
_MSC_VER
值各不相同。而在代码开发过程中,我们有时会需要获取、用到当前版本
Visual Studio
软件对应的
_MSC_VER
值;因此就需要找到一种获取
_MSC_VER
值的方法。


Visual Studio
软件中,我们可以通过顶部菜单栏中的“
帮助
”→“
关于 Microsoft Visual Studio
”选项,获取
Visual Studio
软件的版本信息。

其中,获得的
Visual Studio
软件版本信息如下图所示。

可以看到,其中仅仅显示了
Visual Studio
软件的发布年份、当前版本,以及
.NET Framework
的版本,还有已安装的产品的版本;而并没有
MSVC
编译器的版本。因此,这就需要我们通过以下方法来获得
MSVC
编译器的版本。

方法也很简单,只需要运行如下所示的代码即可。

#include <iostream>
using namespace std;

int main() {
    int num = _MSC_VER; // get the version
    cout << "My MSVC version is: " << num << endl;
    return 0;
}

其中,如果是第一次接触
Visual Studio
软件,大家可以参考文章
安装Visual Studio的详细流程
后半部分提到的操作方法,新建项目并运行上述代码。

上述代码运行后,将会出现如下所示的界面,从而获取
MSVC
编译器的版本。本文中
1933
就是我这里的
MSVC
编译器版本。

至此,大功告成。

前言

上一篇说过,系统会为线程mmap一块内存,每个线程有自己的私有栈,使用局部变量没啥问题。但是实际场景中不可避免的需要线程之间共享数据,这就需要确保每个线程看到的数据是一样的,如果大家都只需要读这块数据没有问题,但是当有了修改共享区域的需求时就会出现数据不一致的问题。甚至线程2的任务在执行到某个地方的时候,需要线程1先做好准备工作,出现顺序依赖的情况。为了解决这些问题,Linux提供了多种API来适用于不同的场景。

互斥量 mutex

排他的访问共享数据,锁竞争激烈的场景使用。锁竞争不激烈的情况可以使用自旋锁(忙等)

当我们用trace -f 去追踪多线程的时候会看到执行加锁解锁的调用是futex,glibc通过futex(fast user space mutex)实现互斥量。通过FUTEX_WAIT_PRIVATE标志的futex调用内核的futex_wait挂起线程,通过FUTEX_WAKE_PRIVATE的futex调用内核的futex_wake来唤醒等待的线程。这之中glibc做了优化:

  • 加锁时,当前mutex没有被加锁,则直接加锁,不做系统调用,自然不需要做上下文切换。如果已经加锁则需要系统调用futex_wait让内核将线程挂起到等待队列
  • 解锁时,没有其他线程在等待该mutex,直接解锁,不做系统调用。如果有其他线程在等待,则通过系统调用futex_wake唤醒等待队列中的一个线程

初始化互斥量

#include <pthread.h>
// 动态初始化并设置互斥量属性,用完需要销毁
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
                       const pthread_mutexattr_t *restrict attr);
// attr 设置mutex的属性,NULL为使用默认属性
// 返回值:成功返回0,失败返回错误编号

// 静态初始化,无需销毁
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

销毁互斥量

// 销毁互斥量
int pthread_mutex_destroy(pthread_mutex_t *mutex);
// 返回值:成功返回0,失败返回错误编号。
// 		如果互斥量是锁定状态,或者正在和条件变量共同使用,销毁会返回EBUSY

加锁和解锁

  1. 使用pthread_mutex_lock加锁
#include <pthread.h>
// 阻塞
int pthread_mutex_lock(pthread_mutex_t *mutex);
// 返回值:成功返回0,失败返回错误编号

// 非阻塞
int pthread_mutex_trylock(pthread_mutex_t *mutex);
// 返回值:加锁成功直接返回0,加锁失败返回EBUSY

int pthread_mutex_unlock(pthread_mutex_t *mutex);
// 返回值:成功返回0,失败返回错误编号

调用状态:

  • 调用时互斥量未锁定,该函数所在线程争取到mutex,返回。
  • 调用时已有其他线程对mutex加锁,则阻塞等待mutex被释放后重新尝试加锁

重复调用问题,即本线程已经对mutex加锁,再次调用加锁操作时,根据互斥量的类型不同会有不同表现:

  • PTHREAD_MUTEX_TIMED_NP:重复加锁导致死锁,该调用线程永久阻塞,并且其他线程无法申请到该mutex
  • PTHREAD_MUTEX_ERRORCHECK_NP:内部记录着调用线程,重复加锁返回EDEADLK,如果解锁的线程不是锁记录的线程,返回EPERM
  • PTHREAD_MUTEX_RECURSIVE_NP:允许重复加锁,锁内部维护着引用计数和调用线程。如果解锁的线程不是锁记录的线程,返回EPERM
  • PTHREAD_MUTEX_ADAPTIVE_NP(自适应锁):先自旋一段时间,自旋的时间由__spins和MAX_ADAPTIVE_COUNT共同决定,自动调整__spin的大小但是不会超过MAX_ADAPTIVE_COUNT。超过自旋时间让出CPU等待,比自旋锁温柔,比normal mutex激进。

设置mutex属性

// 设置mutex为ADAPTER模式
pthread_mutexattr_t mutexattr;
pthread_mutexattr_init(&mutexattr);
pthread_mutexattr_settype(&mutexattr, PTHREAD_MUTEX_ADAPTIVE_NP);

// 获取mutex模式
int kind;
pthread_mutexattr_gettype(&mutexattr, &kind);
if (kind == PTHREAD_MUTEX_ADAPTIVE_NP) {
printf("mutex type is %s", "PTHREAD_MUTEX_ADAPTIVE_NP\n");
}

带有超时的mutex

int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);
// abstime表示在该时间之前阻塞,不是时间间隔
// 成功返回0,失败返回错误编号,超时返回ETIMIEOUT

demo

对已经加锁的mutex继续使用timedlock加锁,timedlock超时返回,之后mutex解锁

#define _DEFAULT_SOURCE 1
#include <errno.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/time.h>

char* now_time(char buf[]) {
  struct timespec abstime;
  abstime.tv_sec = time(0);
  strftime(buf, 1024, "%r", localtime(&abstime.tv_sec));
  return buf;
}

int main() {
  char buf[1024];
  pthread_mutex_t mutex;
  struct timespec abstime;
  pthread_mutex_init(&mutex, NULL);
  pthread_mutex_lock(&mutex);
  char* now = now_time(buf);
  printf("mutex locked, now: %s\n", buf);
  // 设置超时的绝对时间,不设置tv_nsec会返回22,EINVAL
  abstime.tv_sec = time(0) + 10;
  abstime.tv_nsec = 0;
  int ret = pthread_mutex_timedlock(&mutex, &abstime);
  fprintf(stderr, "error %d\n", ret);
  if (ret == ETIMEDOUT) {
    printf("lock mutex timeout\n");
  } else if (ret == 0) {
    printf("lock mutex successfully\n");
  } else if (ret == EINVAL) {
    printf("timedlock param invalid!\n");
  } else {
    printf("other error\n");
  }
  pthread_mutex_unlock(&mutex);
  memset(buf, '\0', 1024);
  now = now_time(buf);
  printf("mutex unlocked, now: %s\n", buf);
  pthread_mutex_destroy(&mutex);
  return 0;
}

// -----------------------------
root@yielde:~/workspace/code-container/cpp/blog_demo# ./test 
mutex locked, now: 08:18:34 PM
error 110
lock mutex timeout
mutex unlocked, now: 08:18:44 PM

读写锁

读写锁适用于临界区很大并且在大多数情况下读取共享资源,极少数情况下需要写的场景

  1. 未加锁:加读、写锁都可以
  2. 加读锁:再次尝试加读锁成功,写锁阻塞
  3. 加写锁:再次尝试加读、写锁阻塞

常用接口与mutex类似,用的时候查
https://man7.org/linux/man-pages/dir_section_3.html
,读写锁有两种策略:

PTHREAD_RWLOCK_PREFER_READER_NP, // 读者优先
PTHREAD_RWLOCK_PREFER_WRITER_NP, // 读者优先
PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP, // 写者优先
PTHREAD_RWLOCK_DEFAULT_NP = PTHREAD_RWLOCK_PREFER_READER_NP

// 通过以下函数设置
int pthread_rwlockattr_setkind_np(pthread_rwlockattr_t *attr, int pref);
int pthread_rwlockattr_getkind_np(const pthread_rwlockattr_t *attr, int *pref);

读写锁存在的问题:

  1. 如果临界区小,锁内部维护的数据结构多于mutex,性能不如mutex
  2. 因为有读优先和写优先的策略,使用不当会出现读或写线程饿死的现象
  3. 如果是写策略优先,线程1持有读锁,线程2等待加写锁,线程1再次加读锁,就出现了死锁情况

demo

启动5个线程共同对一个变量累加1,使用读写锁让线程并发,用自适应锁对共享变量加锁。

/*
  5个线程对total加1执行指定次数
*/

#define _DEFAULT_SOURCE 1  // 处理vscode 未定义 pthread_rwlock_t
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#define THREAD_COUNT 5

int total = 0;                                      // 最终和
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;  // 初始化互斥量
pthread_rwlock_t rwlock;                            // 读写锁变量
typedef struct param {                              // 线程参数类型
  int count;
  int id;
} param;

void *handler(void *arg) {
  struct param *pa = (struct param *)arg;
  pthread_rwlock_rdlock(&rwlock);  // 当主线程不unlock写锁时,会阻塞在这里
  for (int i = 0; i < pa->count; ++i) {
    pthread_mutex_lock(&mutex);  // 加互斥锁
    ++total;
    pthread_mutex_unlock(&mutex);
  }
  pthread_rwlock_unlock(&rwlock);
  printf("thread %d complete\n", pa->id);
  return NULL;
}

int main(int argc, char *argv[]) {
  if (argc != 2) {
    printf("usage: %s per_thread_loop_count\n", argv[0]);
    return 1;
  }
  // 设置mutex为ADAPTER模式
  pthread_mutexattr_t mutexattr;
  pthread_mutexattr_init(&mutexattr);
  pthread_mutexattr_settype(&mutexattr, PTHREAD_MUTEX_ADAPTIVE_NP);
  // 给handler传参
  int loop_count = atoi(argv[1]);
  // 存放线程id的数组
  pthread_t tid[THREAD_COUNT];
  param pa[THREAD_COUNT];

  pthread_rwlock_init(&rwlock, NULL);  // 动态初始化读写锁
  pthread_rwlock_wrlock(&rwlock);  // 给写加锁,等所有线程创建好后解锁,线程执行
  for (int i = 0; i < THREAD_COUNT; ++i) {  // 创建5个线程
    pa[i].count = loop_count;
    pa[i].id = i;
    pthread_create(&tid[i], NULL, handler, &pa[i]);
  }

  pthread_rwlock_unlock(&rwlock);
  for (int i = 0; i < THREAD_COUNT; ++i) {
    pthread_join(tid[i], NULL);
  }
  pthread_rwlock_destroy(&rwlock);
  printf("thread count: %d\n", THREAD_COUNT);
  printf("per thread loop count: %d\n", loop_count);
  printf("total except: %d\n", loop_count * 5);
  printf("total result: %d\n", total);

  int kind;
  pthread_mutexattr_gettype(&mutexattr, &kind);
  if (kind == PTHREAD_MUTEX_ADAPTIVE_NP) {
    printf("mutex type is %s", "PTHREAD_MUTEX_ADAPTIVE_NP\n");
  }
  return 0;
}

// --------------------------------
root@yielde:~/workspace/code-container/cpp/blog_demo# ./test 2000
thread 2 complete
thread 1 complete
thread 0 complete
thread 3 complete
thread 4 complete
thread count: 5
per thread loop count: 2000
total except: 10000
total result: 10000
mutex type is PTHREAD_MUTEX_ADAPTIVE_NP

自旋锁

等待锁的时候不会通知内个将线程挂起,而是忙等。适用于临界区很小,锁被持有的时间很短的情况,相比于互斥锁,节省了上下文切换的开销

线程同步-屏障

barrier可以同步多个线程,允许任意数量的线程等待,直到所有的线程完成工作,然后继续执行

#include <pthread.h>

int pthread_barrier_destroy(pthread_barrier_t *barrier);
// 返回值:成功返回0,失败返回错误号
int pthread_barrier_init(pthread_barrier_t *restrict barrier,
   	const pthread_barrierattr_t *restrict attr, unsigned count);
// count指定有多少个线程到达屏障后再继续执行下去
// 返回值:成功返回0,失败返回错误号

int pthread_barrier_wait(pthread_barrier_t *barrier);
// 成功:给一个线程返回PTHREAD_BARRIER_SERIAL_THREAD,其他线程返回0
// 失败返回错误号

demo

使用4个线程,每个线程计算1+1+..+1=10,将结果放入数组的一个位置,完成后到达barrier。主线程创建好线程后到达barrier,等四个线程全部完成后,由主线程合计结果

#define _DEFAULT_SOURCE
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#define COUNT 10
#define THR_NUM 4

pthread_barrier_t barrier;
long total_arr[THR_NUM] = {0};

void *handler(void *arg) {
  long idx = (long)arg;
  long tmp = 0;
  for (int i = 0; i < COUNT; ++i) {
    ++tmp;
    sleep(1);
  }
  total_arr[idx] = tmp;
  printf("thread %ld complete, count %ld\n", idx, tmp);
  pthread_barrier_wait(&barrier); // 等待在barrier
  return NULL;
}

int main() {
  pthread_t tids[THR_NUM];
  unsigned long total = 0;

  pthread_barrier_init(&barrier, NULL, THR_NUM + 1);  // 包含主线程
  for (long i = 0; i < THR_NUM; ++i) {
    pthread_create(&tids[i], NULL, handler, (void *)i);
  }
  pthread_barrier_wait(&barrier); // 到达barrier
  for (int i = 0; i < THR_NUM; ++i) {
    total += total_arr[i];
  }

  for (int i = 0; i < THR_NUM; ++i) {
    pthread_join(tids[i], NULL);
  }
  pthread_barrier_destroy(&barrier); // 销毁barrier
  printf("total: %lu\n", total);
}

// ---------------------
root@yielde:~/workspace/code-container/cpp/blog_demo# time ./test
thread 2 complete, count 10
thread 0 complete, count 10
thread 3 complete, count 10
thread 1 complete, count 10
total: 40

real    0m10.027s
user    0m0.005s
sys     0m0.003s

线程同步-条件变量

如果条件不满足,线程会等待在条件变量上,并且让出mutex,等待其他线程来执行。其他线程执行到条件满足后会发信号唤醒等待的线程。

// 销毁条件变量
int pthread_cond_destroy(pthread_cond_t *cond);

// 初始化条件变量
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);

// 等待条件变量
int pthread_cond_timedwait(pthread_cond_t *restrict cond,
   		pthread_mutex_t *restrict mutex,
   		const struct timespec *restrict abstime);
int pthread_cond_wait(pthread_cond_t *restrict cond,
   		pthread_mutex_t *restrict mutex);

// 通知条件变量满足
int pthread_cond_broadcast(pthread_cond_t *cond); // 唤醒所有线程
int pthread_cond_signal(pthread_cond_t *cond); // 至少唤醒1个线程
//返回值成功返回0,失败返回错误号

对于 cond_wait,传递mutex保护条件变量,调用线程将锁住的mutex传给函数,函数将调用线程挂起到等待队列上,解锁互斥量。当函数返回时,互斥量再次被锁住。

demo

handler_hello往buf里输入字符串,由handler_print打印

#include <pthread.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; // 初始化互斥量
pthread_cond_t cond = PTHREAD_COND_INITIALIZER; // 初始化条件变量

char buf[8] = {0};

void *handler_hello(void *arg) {
  for (;;) {
    sleep(2);
    pthread_mutex_lock(&mutex);
    sprintf(buf, "%s", "hello !");
    pthread_mutex_unlock(&mutex);
    pthread_cond_signal(&cond); // 唤醒wait的线程
  }

  return NULL;
}

void *handler_print(void *arg) {
  for (;;) {
    pthread_mutex_lock(&mutex);
    while (buf[0] == 0) {
        // 如果buf没有内容就等待,此处将线程挂入队列,然后解锁mutex,等收到handler_hello的signal后返回,加锁mutex
        // 
      pthread_cond_wait(&cond, &mutex); 
    }
    fprintf(stderr, "%s", buf);
    memset(buf, '\0', 8);
    pthread_mutex_unlock(&mutex);
  }
  return NULL;
}

int main() {
  pthread_t tid1, tid2;
  pthread_create(&tid1, NULL, handler_hello, NULL);
  pthread_create(&tid2, NULL, handler_print, NULL);

  pthread_join(tid1, NULL);
  pthread_join(tid2, NULL);

  printf("%s", buf);
  return 0;
}


// ------------------------
root@yielde:~/workspace/code-container/cpp/blog_demo# ./test
hello !hello !hello !hello !^C

学习自:
《UNIX环境高级编程》
《Linux环境编程从应用到内核》高峰 李彬 著

本章介绍电子秤模块的示例功能以及二次开发称重驱动的代码,二次开发以顶尖OS2型号驱动为示例,实现方式与
物联网浏览器(IoTBrowser)-顶尖OS2电子秤协议实现
类似,不同的是电子秤只需要采集重量不需要写入串口数据,实现一个称重抽象类即可。

一、电子秤示例功能

示例代码文件:\src\app_x64\Html\examples\weight\index.html

1.选择型号

2.打开,接收并展示重量

二、开发称重驱动

示例代码文件:\Plugins\DDS.IoT.DJ\Os2Weight.cs

1.添加类

2. 添加引用DDS.IoT.Weight.exe

3.集成.WeightBase基类

4.编译并部署到IoTBrowser

部署路径支持:\app_x64\Plugins\weight或\app_x64\Plugins

5.自动识别到新的驱动型号

项目开源地址:https://gitee.com/yizhuqing/IoTBrowser

在《
WebAssembly入门笔记[2]
》中,我们介绍了如何利用Memory在作为宿主的JavaScript应用和wasm模块之间传递数据,但是Memory面向单纯二进制字节的读写在使用起来还是不太方便,此时我们会更多地用到另一个重要的对象Table。Table利用用来存储一组指定类型的对象,说得准确一点是对象的引用,所以可以读取出来直接消费。

一、利用Table存储wasm函数引用
二、执行call_indirect执行函数
三、利用Table存储JavaScript函数引用

一、利用Table存储wasm函数引用

就目前的版本来说,Table只支持
funcref

externref
两种引用类型,前者表示wasm原生函数,后者则用来存储宿主程序提供的任何JavaScript对象,所以如果存储JavaScript函数,Table元素的类型必需指定为externref。下面的实例演示了这样的场景:wasm模块将自身定义的函数存储在导出的Table中供宿主程序使用。

如下所示的采用WebAssembly Text(WAT)格式定义的app.wat文件的定义。我们定义了用来执行加、减、乘、除运算的四个函数,并将它们存储在导出的Table中。由于存储的是wasm函数,所以Table定义语句
(table (
export
"
table
") funcref (elem $add $sub $mul $div))

将元素类型设置为funcref。我们利用elem语句将四个函数的引用填充到Table中。(
源代码

(module
   (func $add (param $op1 i32) (param $op2 i32) (result i32)
     (local.get $op1)
     (local.get $op2)
     (i32.add)
   )
   (func $sub (param $op1 i32) (param $op2 i32) (result i32)
     (local.get $op1)
     (local.get $op2)
     (i32.sub)
   )
   (func $mul (param $op1 i32) (param $op2 i32) (result i32)
     (local.get $op1)
     (local.get $op2)
     (i32.mul)
   )
   (func $div (param $op1 i32) (param $op2 i32) (result i32)
     (local.get $op1)
     (local.get $op2)
     (i32.div_u)
   )
  (table (export "table") funcref (elem $add $sub $mul $div))
)

上面的定义主要是为了解释wasm基于“堆栈”的参数传递方式,代码相对繁琐。如果切换如下所示的“嵌套模式”,就会简洁很多。(
源代码

(module
   (func $add (param $op1 i32) (param $op2 i32) (result i32)
        (i32.add (local.get $op1) (local.get $op2))
   )
   (func $sub (param $op1 i32) (param $op2 i32) (result i32)
        (i32.sub (local.get $op1) (local.get $op2))
   )
   (func $mul (param $op1 i32) (param $op2 i32) (result i32)
        (i32.mul (local.get $op1) (local.get $op2))
   )
   (func $div (param $op1 i32) (param $op2 i32) (result i32)
        (i32.div_u (local.get $op1) (local.get $op2))
   )
  (table (export "table") funcref (elem $add $sub $mul $div))
)

在承载宿主应用的index.html中,在得到导出的Table对象之后,我们将存储(0-3)的位置作为参数调用其get方法得到对应的wasm函数。我们传入相同的参数(2,1)调用这四个函数。

<html>
    <head></head>
    <body>
        <div id="container"></div>
        <script>
            WebAssembly
                .instantiateStreaming(fetch("app.wasm"))
                .then(results => {
                    var table = results.instance.exports.table;
                    document.getElementById("container").innerHTML =
                    `<p>2 + 1 = ${table.get(0)(2,1)}</p>`+
                    `<p>2 - 1 = ${table.get(1)(2,1)}</p>`+
                    `<p>2 * 1 = ${table.get(2)(2,1)}</p>`+
                    `<p>2 / 1 = ${table.get(3)(2,1)}</p>`;
                });
        </script>
    </body>
</html>

我们将包含结果的运算表达式格式化成HTML,所以页面加载后将会呈现出如下的输出。

image

二、执行call_indirect执行函数

对于存储在Table中的wasm函数,我们还可以按照如下的方式执行call_indirect指令间接执行它。执行call_indirect指定时需要以”类型“的形式确定待执行函数的签名,由于四个函数的签名都是一致的(两个参数和返回值类型均为i32类型),所以我们定义了一个名为$i32_i32_i32的函数类型。(
源代码

(module
   (func $add (param $op1 i32) (param $op2 i32) (result i32)
        (i32.add (local.get $op1) (local.get $op2))
   )
   (func $sub (param $op1 i32) (param $op2 i32) (result i32)
        (i32.sub (local.get $op1) (local.get $op2))
   )
   (func $mul (param $op1 i32) (param $op2 i32) (result i32)
        (i32.mul (local.get $op1) (local.get $op2))
   )
   (func $div (param $op1 i32) (param $op2 i32) (result i32)
        (i32.div_u (local.get $op1) (local.get $op2))
   )
   (table funcref (elem $add $sub $mul $div))

   (type $i32_i32_i32 (func (param i32) (param  i32) (result i32)))
   (func (export "calc") (param $index i32) (param $op1 i32) (param $op2 i32) (result i32)
       (call_indirect (type $i32_i32_i32)  (local.get $op1) (local.get $op2) (local.get $index))
   )
)

我们定义了名为calc的导出函数执行存储在Table中的函数,该函数的第一个参数$index表示函数在Table中的位置,后续两个参数才是算数操作数。传入call_indirect指令的4个参数分别是函数类型、传入目标函数的参数和函数在Table中的位置。index.html中的JavaScript代码以如下的方式调用导出函数calc,所以页面会呈现出与上面相同的输出。

<html>
    <head></head>
    <body>
        <div id="container"></div>
        <script>
            WebAssembly
                .instantiateStreaming(fetch("app.wasm"))
                .then(results => {
                    var calc = results.instance.exports.calc;
                    document.getElementById("container").innerHTML =
                    `<p>2 + 1 = ${calc(0, 2 ,1)}</p>`+
                    `<p>2 - 1 = ${calc(1, 2 ,1)}</p>`+
                    `<p>2 * 1 = ${calc(2, 2 ,1)}</p>`+
                    `<p>2 / 1 = ${calc(3, 2 ,1)}</p>`;
                });
        </script>
    </body>
</html>

三、利用Table存储JavaScript函数引用

第一个实例演示了将wasm函数存储在Table中供JavaScript应用调用,那么是否可以反其道而行之,将JavaScript函数存储在Table中传入wasm模块中执行呢?答案是不可能,至少目前不可以。我们在前面提到过,包含函数在内的JavaScript对象只能以externref的形式存在在Table中,对于externref,wasm模块无法对其”解引用“,自然也不能直接对它进行消费。

但是我们可以将它们回传到作为宿主的JavaScript应用中执行,下面的代码很好地演示了这一点。这次我们选择在JavaScript应用中创建Table,并将其导入到wasm模块。如下面的代码片段所示,一并导入的还有一个被命名为$apply函数。这个函数具有三个参数,第一个参数类型为externref,表示存储在Table中的JavaScript函数,后面两个参数运算操作上。(
源代码

(module
    (import "imports" "table" (table 4 externref))
    (func $apply (import "imports" "apply") (param externref) (param i32) (param i32))
    (func $calc (param $index i32) (param $op1 i32) (param $op2 i32)
         (call $apply (table.get (local.get $index)) (local.get $op1) (local.get $op2))
    )
    (func (export "calculate") (param $op1 i32) (param $op2 i32)
       (call $calc (i32.const 0) (local.get $op1) (local.get $op2))
       (call $calc (i32.const 1) (local.get $op1) (local.get $op2))
       (call $calc (i32.const 2) (local.get $op1) (local.get $op2))
       (call $calc (i32.const 3) (local.get $op1) (local.get $op2))
    )
)

JavaScript函数的执行实现在$calc函数中,它的第一个参数表示函数在Table中的位置。我们通过执行table.get指令得到存储在Table以externref形式存在的JavaScript函数,并将它和两个操作数作为参数调用导入的$apply函数
。导出函数calculate调用$calc函数完成针对4中运算的执行。

导入的apply函数在index.html中以如下的形式定义。我们调用构造函数WebAssembly.Table创建了一个Table对象,并将初始化大小和元素类型设置为4和externref。我们调用Table对象的set方法将四个JavaScript函数存储在这个Table中,四个函数会执行加、减、乘、除运算并将表达式拼接在html字符串上,后者将会作为<div>的innerHTML,所以页面程序的输出还是与上面一致。

<html>
    <head></head>
    <body>
        <div id="container"></div>
        <script>
            var html = "";
            var apply = (func, op1, op2)=> func(op1, op2);
            const table = new WebAssembly.Table({ initial: 4, element: "externref" });

            table.set(0, (op1, op2)=> html += `<p>${op1} + ${op2} = ${op1 + op2}</p>`);
            table.set(1, (op1, op2)=> html += `<p>${op1} - ${op2} = ${op1 - op2}</p>`);
            table.set(2, (op1, op2)=> html += `<p>${op1} * ${op2} = ${op1 * op2}</p>`);
            table.set(3, (op1, op2)=> html += `<p>${op1} / ${op2} = ${op1 / op2}</p>`);

            WebAssembly
                .instantiateStreaming(fetch("app.wasm"), {"imports":{"table":table, "apply":apply}})
                .then(results => {
                    html = "";
                    results.instance.exports.calculate(4,2);
                    document.getElementById("container").innerHTML = html;
                });
        </script>
    </body>
</html>

WebAssembly入门笔记[1]:与JavaScript的交互
WebAssembly入门笔记[2]:利用Memory传递字节数据
WebAssembly入门笔记[3]:利用Table传递引用