2024年9月

商业模式

商业模式是帮助企业成功的“秘诀”,它通过整合企业内外部的多种要素,构建起一个全面、高效且具有独特竞争优势的运营体系。这一体系的目的是满足市场的需求,实现各利益相关者价值最大化,并确保企业的长期盈利能力。

商业模式的核心架构由三个紧密相连的环节构成:创造价值、传递价值和获取价值。

  • 创造价值:这一环节围绕客户的需求展开,提供具有吸引力的价值提案。
  • 传递价值:通过优化资源配置和实施相关策略,确保价值的有效传递。
  • 获取价值:通过精心设计的盈利机制,实现企业收益的持续增长。

这三个环节相互依赖,共同构成了商业模式的框架,它们协同工作,帮助企业在竞争激烈的市场中脱颖而出。

商业模式举例

因为互联网和数字化的发展,公司正在努力改变传统的商业模式,把生产、销售、交易、物流和支付等多个部分整合在一起,不受时间和空间的限制。

通过使用移动互联网、大数据、人工智能和云计算等新技术,公司正在改变消费者的购物习惯和生活方式。新零售的改变特别明显,线下实体门店向线上转型,而线上电商也在寻找扩大实体店市场机会。下面是一些典型的电商商业模式的例子:

  • B2C(商家对消费者):这是电商领域最为常见的模式,商家直接向消费者销售产品和服务。天猫、京东等平台是这一模式的典型代表。
  • B2B(商家对商家):在这一模式下,供应方和采购方通过电商平台完成交易,有效解决了供应链上游至中游的问题。1688等平台是这一模式的杰出代表。
  • C2C(消费者对消费者):在这种模式中,消费者可以直接与其他消费者交易。它强调产品的个性化和质量,提供类似于B2C的服务,但更注重服务。淘宝、微店等平台就是这种模式的例子。
  • C2M(消费者对工厂):这是一种消费者与制造商直接对接的模式,去除了中间环节,提供定制化的生产和消费,强调定制服务和增值服务。淘宝特价版、拼多多等平台采用了这一模式。
  • O2O(线上到线下):这一模式融合了线上信息获取和线下购买体验,是新零售商业模式的典型代表。许多传统企业正在积极探索O2O模式,以增强市场竞争力。

商业模式并非一成不变,它随着企业经营策略和市场环境的变化而变化。

商业模式画布

商业模式画布是一种广泛使用的规划商业模式的工具,由亚历山大·奥斯特瓦德(Alexander Osterwalder)首创。通过九宫格框架,企业能够将商业模式形象化,全面审视和分析其商业运作。

商业模式画布的核心要素:

  • 价值主张:企业向客户提供哪些独特的产品、服务或价值,并解决客户的哪些问题?价值主张是企业与竞争对手区分开的关键,通过创新性、性能、定制化、质量、设计、品牌、定价、成本效益、风险降低、便捷性和可用性等元素来提供价值。
  • 客户关系:企业打算与客户建立什么关系?可能的关系类型包括个性化服务、专属服务、自助服务、自动化服务、社区互动和共同创造等。
  • 客户细分:企业的目标客户群体是谁?通过识别和理解客户的不同需求及特征,企业可以对客户群体进行精准细分,如大众市场、小众市场、多边市场、区隔化市场等,更好地满足特定客户群体的需求。
  • 核心资源:企业为了支持商业活动,有哪些重要的资源?这些可能是实体资产、知识、员工、或者金融资产等。
  • 关键活动:企业需要进行哪些主要活动,来确保产品或服务的顺利运作?这些活动可能包括产品制造、问题解决、平台构建和服务网络搭建等。
  • 渠道通路:企业通过哪些途径将产品或服务传递给客户?渠道通路的构建涉及五个阶段:认知、评估、购买、交付和售后。渠道类型包括自有渠道(如实体店)、合作伙伴渠道(如分销商),同时还需考虑线上、线下和O2O等新零售渠道。
  • 合作伙伴:企业需要与哪些上游或下游的企业建立深度合作关系?合作伙伴关系可能包括战略联盟、竞争合作、新业务合作以及供应商与购买方关系。合作的实质在于资源共享和互利共赢。
  • 成本结构:企业在商业运作中是否充分考虑了成本因素?成本结构可以是成本驱动型或价值驱动型,需要考虑固定成本、变动成本、规模经济和范围经济等。
  • 收入来源:企业的主要收入途径是什么?收入的生成方式包括产品销售、使用费、订阅费、租赁费、授权费、交易费和广告费等。

下图所示为滴滴企业的商业模式画布:

价值流

价值流的相关概念包括:价值主张、价值流、价值流阶段。

价值主张

价值主张概念在商业模式画布部分已解释过。

价值主张处于商业画布的中心位置,当企业决定是否投资某个产品或服务前,首先需明确它为哪个客户群体服务?提供什么价值?以及目标客户群体能否承受产品或服务的价格?

价值流

价值流的定义:为客户创造结果的端到端活动的集合,客户可能是价值流的最终客户,也可能内部使用用户。

价值流更专注于特定的目标客户和价值主张,目标明确。同时,价值流更强调结果导向和价值增长。

通过价值流分析,我们可以很容易地看出哪些环节是增值的,哪些是不增值的。理论上,我们可以消除或减弱没有价值增长的环节,这可以避免流程过于繁重,效果不明显的情况。

价值流阶段

价值流可以进一步细分为不同的价值流阶段,其中的每个价值流阶段,都会贡献相应的价值增量,以确保客户所需整体价值的逐步实现和完整交付。价值流阶段有以下几个特征:

  • 每个流程阶段都有相应的“价值”。如果某个阶段不能增加或贡献到客户需要的价值,理论上可以放弃这个阶段。
  • 每个阶段都有进入条件。只有满足特定条件才能进入下一步。确认和保障这些条件,有利于阶段顺利实现目标。
  • 每个价值流阶段都有完成条件。设定明确的完成条件,可以快速检查是否已完成该阶段的任务,并开始下一个阶段。

我们以一个门店自提服务为例子:

  • 价值主张:让顾客享受优质的酒店服务
  • 价值流阶段:整个价值链由5个阶段组成,分别为商品浏览、下单支付、备货并通知、自提、售后。

业务能力

业务能力指的是企业开展其业务活动所必需的一系列核心技能和资源。这些能力是从业务的角度出发,为了达成特定的目标或结果,构建的特定技能或生产能力。

企业的业务能力与商业模式和价值流密切相关,因为它们直接影响了企业的业绩和价值创造。它们确保了企业战略的实施,并与客户旅程和市场环境保持一致。此外,业务能力也协调了业务需求和IT系统。

业务能力的范畴较为宏观,它有助于企业从多角度进行战略规划和业务发展。在TOGAF(开放组织架构框架)中,业务能力的实现涉及角色、流程、信息和工具的综合运用。

在其他企业管理理论中,业务能力同样被视为企业架构中的一个重要组成部分,它包括人员、组织机构、功能、流程、业务服务、数据信息、应用系统和基础设施等多个要素,并与企业的各种项目和解决方案紧密相关。

业务能力提供了一种独立于现有组织结构、业务流程、应用系统、产品/服务的业务视角,有助于企业从更高层次上理解和管理其业务。

在业务架构体系中,业务能力的关键在于系统化地表现企业的核心业务功能。

以电商业务为例,常见的业务能力包括店铺管理、商品管理、会员营销、订单处理、物流、支付结算、售后服务等方面的综合管理。这些管理活动汇集而成的能力,构成了企业高层业务能力,并且可以进一步细分为多个子业务能力。

例如,订单处理能力可以细分为平台订单管理、自营订单管理、第三方订单管理、订单来源追踪、订单分拆处理等子能力。

业务流程

业务流程是一系列逻辑上相关联的业务活动,它们组合起来以达成特定的业务结果。在业务架构的设计阶段,业务流程扮演着至关重要的角色,它不仅关系到企业资源的有效利用,也直接影响到企业IT架构中应用功能的设计和系统整合的具体需求。

业务流程是价值流概念的进一步展开,它将价值流中的概念细化为可操作的流程。

业务流程与业务能力的区别:

  • 业务能力:关注企业核心业务的能力和结果,不涉及具体的流程分解。
  • 业务流程:专注于流程本身,面向特定场景,通过活动组合解决具体问题,是企业日常运作的关键。
  • 业务流程涵盖关键业务活动,如销售、市场推广、生产、采购和客户服务等,以及执行这些活动的角色和他们之间的互动。同时,业务流程还需遵循行业规范、专业标准和企业内部规章。

业务流程可以进一步细化为不同层级,包括主流程和子流程。它们是连接不同业务部门的纽带,端到端流程往往跨越多个部门或业务能力领域,实现价值的增加。

以电商系统为例,用户交易流程是一个相对标准化的流程,通常包括以下环节:

  • 商品选择
    :用户浏览电商平台,选择想要购买的商品,并添加到购物车。
  • 购物车确认
    :用户查看购物车中的商品,可以修改数量或删除不想购买的商品。
  • 结算
    :用户选择“结算”选项,准备进行支付。在此步骤,用户还可以选择或添加配送地址。
  • 支付
    :用户选择支付方式(如信用卡、支付宝、微信支付等),输入必要的支付信息进行支付。
  • 订单确认
    :支付完成后,系统生成订单,确认购买的商品和支付详情,并通常通过邮件或短信形式发送订单确认信息给用户。
  • 物流处理
    :订单信息传递给仓库,开始打包和发货流程。
  • 发货与跟踪
    :商品发货后,用户可以通过订单系统跟踪物流状态。
  • 确认收货
    :用户收到货物,并确认收货。

业务流程可以进一步细化为不同层次,提供更具体的管理和执行指南。通过分层方法,企业能够确保业务流程设计与价值流的每个环节紧密相连,从而在不同层级上实现价值的最大化。通常包括以下几个层次:

  • 流程类别
    :大类别,如采购、销售、生产等。
  • 流程组
    :在同一类别下的相关流程集合,如订单处理流程组可能包括订单接收、订单确认、订单履行等。
  • 流程
    :具体的操作步骤,例如订单确认流程可能包括接收订单、审核订单、确认库存、生成配送单等步骤。
  • 子流程
    :流程中的更详细步骤,如审核订单可能细化为验证客户资料、检查支付状态等。
  • 任务
    :最基本的操作单元,具体到个人的具体工作,如输入客户订单数据、打印配送单等。

组织架构

组织架构是按照企业战略来设定和安排部门和岗位,形成稳定且科学的管理体系。这个体系保证企业能适应业务需求并支持企业发展。

组织架构对于业务架构至关重要。在梳理业务流程时,必须根据业务流程的运作规律和处理逻辑,在流程的各个节点上安排合适的人员,确保组织的灵活性和明确的责任分配。

同时,业务架构也需要考虑组织的业务需求和发展,对部门的岗位设置、人员配置、角色定义、权限分配、职责明确以及考核机制进行清晰的规划,保障业务流程中每个环节的顺利运作。

下图所示为一个中小连锁企业的组织架构图。

本文已收录于,我的技术网站:
tangshiye.cn
里面有,算法Leetcode详解,面试八股文、BAT面试真题、简历模版、架构设计,等经验分享。

前言

人工智能时代,人脸识别技术已成为安全验证、身份识别和用户交互的关键工具。

给大家推荐一款.NET 开源提供了强大的人脸识别 API,工具不仅易于集成,还具备高效处理能力。

本文将介绍一款如何利用这些API,为我们的项目添加智能识别的亮点。

项目介绍

GitHub 上拥有 1.2k 星标的 C# 面部识别 API 项目:FaceRecognitionDotNet。该项目功能强大,开箱即用,并支持跨平台。

它使用了 OpenCVSharp 和 face_recognition 开源库,并提供了 NuGet 包,方便集成到项目中。

项目是 face_recognition 的 C# 移植版本。 face_recognition 是一个基于 Python 的人脸识别库,它提供了简单易用的接口来进行人脸检测、人脸识别和人脸特征提取等功能。

这个库基于dlib和OpenCV开发,并且提供了一个高级的人脸识别接口,可以用于识别图像或视频中的人脸,并且可以识别出不同人物之间的相似度。

项目特点

  • 预测年龄
  • 情绪识别
  • 性别判断
  • 脸部标记
  • 眨眼检测

项目说明

支持跨平台,包括 Windows、Linux 和 macOS!

支持的API

项目展示

1、面部识别

2、年龄和性别

3、脸部标记

4、情绪识别

项目地址

文档:
https://taktak.jp/FaceRecognitionDotNet/

face_recognition:
https://github.com/ageitgey/face_recognition

Github:
https://github.com/takuya-takeuchi/FaceRecognitionDotNet

最后

如果你觉得这篇文章对你有帮助,不妨点个赞支持一下!你的支持是我继续分享知识的动力。如果有任何疑问或需要进一步的帮助,欢迎随时留言。

也可以加入微信公众号
[DotNet技术匠]
社区,与其他热爱技术的同行一起交流心得,共同成长!
优秀是一种习惯,欢迎大家留言学习!

原创文章,欢迎转载,转载请注明出处,谢谢。


0. 前言


第四讲
我们介绍了 main goroutine 是如何运行的。其中针对 main goroutine 介绍了调度函数 schedule 是怎么工作的,对于整个调度器的调度策略并没有介绍,这点是不完整的,这一讲会完善调度器的调度策略部分。

1. 调度时间点

runtime.schedule
实现了调度器的调度策略。那么对于调度时间点,查看哪些函数调用的
runtime.schedule
即可顺藤摸瓜理出调度器的调度时间点,如下图:

image

调度时间点不是本讲的重点,这里有兴趣的同学可以顺藤摸瓜,摸摸触发调度时间点的路径,这里就跳过了。

2. 调度策略

调度策略才是我们的重点,进到
runtime.schedule

// One round of scheduler: find a runnable goroutine and execute it.
// Never returns.
func schedule() {
    mp := getg().m                  // 获取当前执行线程

top:
	pp := mp.p.ptr()                // 获取执行线程绑定的 P
	pp.preempt = false

    // Safety check: if we are spinning, the run queue should be empty.
	// Check this before calling checkTimers, as that might call
	// goready to put a ready goroutine on the local run queue.
    if mp.spinning && (pp.runnext != 0 || pp.runqhead != pp.runqtail) {
		throw("schedule: spinning with local work")
	}

    gp, inheritTime, tryWakeP := findRunnable() // blocks until work is available

    ...
    execute(gp, inheritTime)        // 执行找到的 goroutine
}

runtime.schedule
的重点在
findRunnable()

findRunnable()
函数很长,为避免影响可读性,这里对大部分流程做了注释,后面在有重点的加以介绍。进入
findRunnable()

// Finds a runnable goroutine to execute.
// Tries to steal from other P's, get g from local or global queue, poll network.
// tryWakeP indicates that the returned goroutine is not normal (GC worker, trace
// reader) so the caller should try to wake a P.
func findRunnable() (gp *g, inheritTime, tryWakeP bool) {
	mp := getg().m                                      // 获取当前执行线程

top:
	pp := mp.p.ptr()                                    // 获取线程绑定的 P
	...
	
    // Check the global runnable queue once in a while to ensure fairness.
	// Otherwise two goroutines can completely occupy the local runqueue
	// by constantly respawning each other.
	if pp.schedtick%61 == 0 && sched.runqsize > 0 {
		lock(&sched.lock)
		gp := globrunqget(pp, 1)
		unlock(&sched.lock)
		if gp != nil {
			return gp, false, false
		}
	}

    // local runq
	if gp, inheritTime := runqget(pp); gp != nil {      // 从 P 的本地队列中获取 goroutine
		return gp, inheritTime, false
	}

    // global runq
	if sched.runqsize != 0 {                            // 如果本地队列获取不到就判断全局队列中有无 goroutine
		lock(&sched.lock)                               // 如果有的话,为全局变量加锁
		gp := globrunqget(pp, 0)                        // 从全局队列中拿 goroutine
		unlock(&sched.lock)                             // 为全局变量解锁
		if gp != nil {
			return gp, false, false
		}
	}

    // 如果全局队列中没有 goroutine 则从 network poller 中取 goroutine
    if netpollinited() && netpollWaiters.Load() > 0 && sched.lastpoll.Load() != 0 {
		...
	}

    // 如果 network poller 中也没有 goroutine,那么尝试从其它 P 中偷 goroutine
    // Spinning Ms: steal work from other Ps.
	//
	// Limit the number of spinning Ms to half the number of busy Ps.
	// This is necessary to prevent excessive CPU consumption when
	// GOMAXPROCS>>1 but the program parallelism is low.
    // 如果下面两个条件至少有一个满足,则进入偷 goroutine 逻辑
    // 条件 1: 当前线程是 spinning 自旋状态
    // 条件 2: 当前活跃的 P 要远大于自旋的线程,说明需要线程去分担活跃线程的压力,不要睡觉了
	if mp.spinning || 2*sched.nmspinning.Load() < gomaxprocs-sched.npidle.Load() {
        if !mp.spinning {                                       // 因为是两个条件至少满足一个即可,这里首先判断当前线程是不是自旋状态
			mp.becomeSpinning()                                 // 如果不是,更新线程的状态为自旋状态
		}

        gp, inheritTime, tnow, w, newWork := stealWork(now)     // 偷 goroutine
		if gp != nil {
			// Successfully stole.
			return gp, inheritTime, false                       // 如果 gp 不等于 nil,表示偷到了,返回偷到的 goroutine
		}
		if newWork {                
			// There may be new timer or GC work; restart to
			// discover.
			goto top                                            // 如果 gp 不等于 nil,且 network 为 true,则跳到 top 标签重新找 goroutine
		}

		now = tnow
		if w != 0 && (pollUntil == 0 || w < pollUntil) {
			// Earlier timer to wait for.
			pollUntil = w
		}
	}

    ...
    if sched.runqsize != 0 {                                    // 偷都没偷到,还要在找一遍全局队列,防止偷的过程中,全局队列又有 goroutine 了
		gp := globrunqget(pp, 0)
		unlock(&sched.lock)
		return gp, false, false
	}

    if !mp.spinning && sched.needspinning.Load() == 1 {         // 在判断一遍,如果 mp 不是自旋状态,且 sched.needspinning == 1 则更新 mp 为自旋,调用 top 重新找一遍 goroutine
		// See "Delicate dance" comment below.
		mp.becomeSpinning()
		unlock(&sched.lock)
		goto top
	}

    // 实在找不到 goroutine,表明当前线程多, goroutine 少,准备挂起线程
    // 首先,调用 releasep 取消线程和 P 的绑定
    if releasep() != pp {                                       
		throw("findrunnable: wrong p")
	}

    ...
    now = pidleput(pp, now)                                     // 将解绑的 P 放到全局空闲队列中
    unlock(&sched.lock)

    wasSpinning := mp.spinning                                  // 到这里 mp.spinning == true,线程处于自旋状态
	if mp.spinning {
		mp.spinning = false                                     // 设置 mp.spinning = false,这是要准备休眠了
		if sched.nmspinning.Add(-1) < 0 {                       // 将全局变量的自旋线程数减 1,因为当前线程准备休眠,不偷 goroutine 了
			throw("findrunnable: negative nmspinning")
		}
        ...
    }
    stopm()                                                     // 线程休眠,直到唤醒
	goto top                                                    // 能执行到这里,说明线程已经被唤醒了,继续找一遍 goroutine
}

看完线程的调度策略我都要被感动到了,何其的敬业,穷尽一切方式去找活干,找不到活,休眠之前还要在找一遍,真的是劳模啊。

大致流程是比较清楚的,我们把其中一些值得深挖的部分在单拎出来。

首先,从本地队列中找 goroutine,如果找不到则进入全局队列找,这里如果看
gp := globrunqget(pp, 0)
可能会觉得疑惑,从全局队列中拿 goroutine 为什么要把 P 传进去,我们看这个函数在做什么:

// Try get a batch of G's from the global runnable queue.
// sched.lock must be held.											// 注释说的挺清晰了,把全局队列的 goroutine 放到 P 的本地队列
func globrunqget(pp *p, max int32) *g {
	assertLockHeld(&sched.lock)										

	if sched.runqsize == 0 {
		return nil
	}

	n := sched.runqsize/gomaxprocs + 1								// 全局队列是线程共享的,这里要除 gomaxprocs 平摊到每个线程绑定的 P
	if n > sched.runqsize {
		n = sched.runqsize											// 执行到这里,说明 gomaxprocs == 1
	}
	if max > 0 && n > max {
		n = max
	}
	if n > int32(len(pp.runq))/2 {									
		n = int32(len(pp.runq)) / 2									// 如果 n 比本地队列长度的一半要长,则 n == len(P.runq)/2
	}

	sched.runqsize -= n												// 全局队列长度减 n,准备从全局队列中拿 n 个 goroutine 到 P 中

	gp := sched.runq.pop()											// 把全局队列队头的 goroutine 拿出来,这个 goroutine 是要返回的 goroutine
	n--																// 拿出了一个队头的 goroutine,这里 n 要减 1
	for ; n > 0; n-- {				
		gp1 := sched.runq.pop()										// 循环拿全局队列中的 goroutine 出来
		runqput(pp, gp1, false)										// 将拿出的 goroutine 放到全局队列中
	}
	return gp
}

调用
globrunqget
说明本地队列没有 goroutine 要从全局队列拿,那么就可以把全局队列中的 goroutine 放到 P 中,提高了全局队列 goroutine 的优先级。

如果全局队列也没找到 goroutine,在从
network poller
找,如果
network poller
也没找到,则准备进入自旋,从别的线程的 P 那里偷活干。我们看线程是怎么偷活的:

// stealWork attempts to steal a runnable goroutine or timer from any P.
//
// If newWork is true, new work may have been readied.
//
// If now is not 0 it is the current time. stealWork returns the passed time or
// the current time if now was passed as 0.
func stealWork(now int64) (gp *g, inheritTime bool, rnow, pollUntil int64, newWork bool) {
	pp := getg().m.p.ptr()																// pp 是当前线程绑定的 P

	ranTimer := false

	const stealTries = 4																// 线程偷四次,每次都要随机循环一遍所有 P
	for i := 0; i < stealTries; i++ {
		stealTimersOrRunNextG := i == stealTries-1

		for enum := stealOrder.start(fastrand()); !enum.done(); enum.next() {			// 为保证偷的随机性,随机开始偷 P。随机开始,后面每个 P 都可以轮到
			...
			p2 := allp[enum.position()]													// 从 allp 中获取 P
			if pp == p2 {
				continue																// 如果获取的是当前线程绑定的 P,则继续循环下一个 P
			}
			...
			// Don't bother to attempt to steal if p2 is idle.
			if !idlepMask.read(enum.position()) {										// 判断拿到的 P 是不是 idle 状态,如果是,表明 P 还没有 goroutine,跳过它,偷下一家
				if gp := runqsteal(pp, p2, stealTimersOrRunNextG); gp != nil {			// P 不是 idle,调用 runqsteal 偷它!
					return gp, false, now, pollUntil, ranTimer
				}
			}
		}
	}

	// No goroutines found to steal. Regardless, running a timer may have
	// made some goroutine ready that we missed. Indicate the next timer to
	// wait for.
	return nil, false, now, pollUntil, ranTimer
}

线程随机的偷一个可偷的 P,偷 P 的实现在
runqsteal
,查看
runqsteal
怎么偷的:

// Steal half of elements from local runnable queue of p2
// and put onto local runnable queue of p.
// Returns one of the stolen elements (or nil if failed).						// 给宝宝饿坏了,直接偷一半的 goroutine 啊,够狠的!
func runqsteal(pp, p2 *p, stealRunNextG bool) *g {
	t := pp.runqtail															// t 指向当前 P 本地队列的队尾
	n := runqgrab(p2, &pp.runq, t, stealRunNextG)								// runqgrab 把 P2 本地队列的一半 goroutine 拿到 P 的 runq 队列中
	if n == 0 {
		return nil
	}
	n--
	gp := pp.runq[(t+n)%uint32(len(pp.runq))].ptr()								// 把偷到的本地队列队尾的 goroutine 拿出来
	if n == 0 {
		return gp																// 如果只偷到了这一个,则直接返回。有总比没有好
	}
	h := atomic.LoadAcq(&pp.runqhead) // load-acquire, synchronize with consumers
	if t-h+n >= uint32(len(pp.runq)) {
		throw("runqsteal: runq overflow")										// 如果 t-h+n >= len(p.runq) 表示偷多了...
	}
	atomic.StoreRel(&pp.runqtail, t+n) 											// 更新 P 的本地队列的队尾
	return gp
}

这个偷就是把“地主家”(P2)的余粮 (goroutine) 给它抢一半过来,没办法我也要吃饭啊。

如果连偷都没偷到(好吧,太惨了点...),那就准备休眠了,不干活了还不行嘛。不干活之前在去看看全局队列有没有 goroutine 了(口是心非的 M 人)。还是没活,好吧,准备休眠了。

准备休眠,首先解除和 P 的绑定:

func releasep() *p {
	gp := getg()

	if gp.m.p == 0 {
		throw("releasep: invalid arg")
	}
	pp := gp.m.p.ptr()
	if pp.m.ptr() != gp.m || pp.status != _Prunning {
		print("releasep: m=", gp.m, " m->p=", gp.m.p.ptr(), " p->m=", hex(pp.m), " p->status=", pp.status, "\n")
		throw("releasep: invalid p state")
	}
	...
	gp.m.p = 0
	pp.m = 0
	pp.status = _Pidle
	return pp
}

就是指针的解绑操作,代码很清晰,连注释都不用,我们也不讲了。

解绑之后,
pidleput
把空闲的 P 放到全局空闲队列中。

接着,更新线程的状态,从自旋更新为非自旋,调用
stopm
准备休眠:

// Stops execution of the current m until new work is available.
// Returns with acquired P.
func stopm() {
	gp := getg()							// 当前线程执行的 goroutine

	...

	lock(&sched.lock)
	mput(gp.m)								// 将线程放到全局空闲线程队列中
	unlock(&sched.lock)
	mPark()
	acquirep(gp.m.nextp.ptr())
	gp.m.nextp = 0
}

stopm
将线程放到全局空闲线程队列,接着调用
mPark
休眠线程:

// mPark causes a thread to park itself, returning once woken.
//
//go:nosplit
func mPark() {
	gp := getg()
	notesleep(&gp.m.park)					// notesleep 线程休眠
	noteclear(&gp.m.park)
}

func notesleep(n *note) {
	gp := getg()
	if gp != gp.m.g0 {
		throw("notesleep not on g0")
	}
	ns := int64(-1)
	if *cgo_yield != nil {
		// Sleep for an arbitrary-but-moderate interval to poll libc interceptors.
		ns = 10e6
	}
	for atomic.Load(key32(&n.key)) == 0 {					// 这里通过 n.key 判断线程是否唤醒,如果等于 0,表示未唤醒,线程继续休眠
		gp.m.blocked = true
		futexsleep(key32(&n.key), 0, ns)					// 调用 futex 休眠线程,线程会“阻塞”在这里,直到被唤醒
		if *cgo_yield != nil {
			asmcgocall(*cgo_yield, nil)
		}
		gp.m.blocked = false								// “唤醒”,设置线程的 blocked 标记为 false
	}
}

// One-time notifications.
func noteclear(n *note) {									
	n.key = 0												// 执行到 noteclear 说明,线程已经被唤醒了,这时候线程重置 n.key 标志位为 0
}

线程休眠是通过调用
futex
进入操作系统内核完成线程休眠的,关于
futex
的内容可以参考
这里

线程的 n.key 是休眠的标志位,当 n.key 不等于 0 时表示有线程在唤醒休眠线程,线程从休眠状态恢复到正常状态。唤醒休眠线程通过调用
notewakeup(&nmp.park)
函数实现:

func notewakeup(n *note) {
	old := atomic.Xchg(key32(&n.key), 1)
	if old != 0 {
		print("notewakeup - double wakeup (", old, ")\n")
		throw("notewakeup - double wakeup")
	}
	futexwakeup(key32(&n.key), 1)					// 调用 futexwakeup 唤醒休眠线程
}

首先,线程是怎么找到休眠线程的?线程通过全局空闲线程队列找到空闲的线程,并且将空闲线程的休眠标志位 m.park 传给
notewakeup
,最后调用
futexwakeup
唤醒休眠线程。

值得一提的是,唤醒的线程在唤醒之后还是会继续找可运行的 goroutine 直到找到:

func stopm() {
	...
	mPark()								// 如果 mPark 返回,表示线程被唤醒,开始正常工作
	acquirep(gp.m.nextp.ptr())			// 前面休眠前,线程已经和 P 解绑了。这里在给线程找一个 P 绑定
	gp.m.nextp = 0						// 线程已经绑定到 P 了,重置 nextp
}

基本这就是调度策略中很重要的一部分,线程如何找 goroutine。找到 goroutine 之后调用
gogo
执行该 goroutine。

3. 小结

本讲继续丰富了调度器的调度策略,下一讲,我们开始非 main goroutine 的介绍。


《Redis 入门》系列文章总算完成了,希望这个系列文章可以想入门或刚入门的同学提供帮助,希望能让你形成学习Redis系统性概念。

当时为什么要写这个系列文章,是因为我自己就是迷迷糊糊一路踩坑走过来的,我踩完的坑就踩完了,希望你能少踩一些。

我们简单来回顾一下《Redis 入门》系列内容,希望通过这次回顾能总结一些学习新知识的经验,学习如何更快的掌握新知识,如何形成系统性概念。

1.首先我们了解了Redis基本信息,是什么,有什么,能干什么;

2.然后我们学习如何安装Redis,在不同的操作系统上(Windows、Linux、Docker),通过不同的方式(脚本、安装包、包管理器方式、源码编译等)安装;

3.接着我们寻找了Redis可视化工具,我们介绍了四大类可视化工具(命令行工具、桌面客户端工具、网页工具、插件工具),10来种软件,各有千秋。

4.然后我们正式进入Redis知识学习,了解Redis五大基础数据类型(字符串、集合、有序集合、列、哈希),并熟悉了相关指令。

5.然后我们继续结合具体编程语言C#/.NET Core,了解了6款C#/.NET Core接入Redis客户端库(ServiceStack.Redis、StackExchange.Redis、CSRedisCore、FreeRedis、NewLife.Redis、BeetleX.Redis),并对6款库应用场景做了基本了解,还分别学习了如何使用这6款库。

6.最后我们一起封装了一个简单的Redis C#/.NET Core客户端库Nuget,以达到和现有项目解耦,方便后期扩展的目的。

在整个过程中,很多时候我们会有很多选择,到底应该做什么,我相信第一次接触Redis的,很多时候都会感到很迷茫。然后只能不停的在网上找答案,不停的去尝试,踩了无数次坑以后,最后才发现原来是这样啊,瞬间恍然大悟。

我自己也是踩了很多坑走过来的,因此我想把我的经验分享给大家,然后可以系统给大家介绍如何学习Redis,可以让大家少踩一些坑,在学习的早期就形成系统性概念,从而提升学习效率。

后面我想继续和大家分享《Redis 进阶》,开始深入学习Redis相关知识,希望能给你带来一些收获,如果你有什么想法,也可以给我留言。


:测试方法代码以及示例源码都已经上传至代码库,有兴趣的可以看看。
https://gitee.com/hugogoos/Planner

Hugging Face NLP课程学习记录 - 0. 安装transformers库 & 1. Transformer 模型

说明:

0. 安装transformers库

创建conda环境并安装包:

conda create -n hfnlp python=3.12
conda install pytorch==2.3.1 torchvision==0.18.1 torchaudio==2.3.1 pytorch-cuda=12.1 -c pytorch -c nvidia
pip install transformers==4.44.2

# More
pip install seqeval
pip install sentencepiece

使用Hugging Face镜像(见
https://hf-mirror.com/
):

export HF_ENDPOINT=https://hf-mirror.com

或者在python中设置Hugging Face镜像:

import os
os.environ["HF_ENDPOINT"] = "https://hf-mirror.com"

1. Transformer 模型

Transformers 能做什么?

使用pipelines

Transformers 库中最基本的对象是
pipeline()
函数。它将模型与其必要的预处理和后处理步骤连接起来,使我们能够通过直接输入任何文本并获得最终的答案:

from transformers import pipeline

classifier = pipeline("sentiment-analysis")
classifier("I've been waiting for a HuggingFace course my whole life.")

提示:

No model was supplied, defaulted to distilbert/distilbert-base-uncased-finetuned-sst-2-english and revision af0f99b ....
[{'label': 'POSITIVE', 'score': 0.9598047137260437}]

输出:

[{'label': 'POSITIVE', 'score': 0.9598047137260437}]

输入多个句子:

classifier(
    ["I've been waiting for a HuggingFace course my whole life.", "I hate this so much!"]
)
[{'label': 'POSITIVE', 'score': 0.9598047137260437},
 {'label': 'NEGATIVE', 'score': 0.9994558691978455}]

将一些文本传递到pipeline时涉及三个主要步骤:

  1. 文本被预处理为模型可以理解的格式。
  2. 预处理的输入被传递给模型。
  3. 模型处理后输出最终人类可以理解的结果。

零样本分类

from transformers import pipeline

classifier = pipeline("zero-shot-classification")
classifier(
    "This is a course about the Transformers library",
    candidate_labels=["education", "politics", "business"],
)

提示:

No model was supplied, defaulted to facebook/bart-large-mnli and revision c626438 ([https://hf-mirror.com/facebook/bart-large-mnli](https://hf-mirror.com/facebook/bart-large-mnli)).
Using a pipeline without specifying a model name and revision in production is not recommended.

输出:

{'sequence': 'This is a course about the Transformers library', 'labels': ['education', 'business', 'politics'], 'scores': [0.8445952534675598, 0.11197696626186371, 0.043427806347608566]}

此pipeline称为zero-shot,因为您不需要对数据上的模型进行微调即可使用它

文本分类

现在让我们看看如何使用pipeline来生成一些文本。这里的主要使用方法是您提供一个提示,模型将通过生成剩余的文本来自动完成整段话。

from transformers import pipeline

generator = pipeline("text-generation")
generator("In this course, we will teach you how to")

提示:

No model was supplied, defaulted to openai-community/gpt2 and revision 6c0e608 (https://hf-mirror.com/openai-community/gpt2).
Using a pipeline without specifying a model name and revision in production is not recommended.

输出:

[{'generated_text': 'In this course, we will teach you how to create a simple Python script that uses the default Python scripts for the following tasks, such as adding a linker at the end of a file to a file, editing an array, etc.\n\n'}]

在pipeline中使用 Hub 中的其他模型

前面的示例使用了默认模型,但您也可以从 Hub 中选择特定模型以在特定任务的pipeline中使用 - 例如,文本生成。转到
模型中心(hub)
并单击左侧的相应标签将会只显示该任务支持的模型。
例如这样

让我们试试
distilgpt2
模型吧!以下是如何在与以前相同的pipeline中加载它:

from transformers import pipeline

generator = pipeline("text-generation", model="distilgpt2")
generator(
    "In this course, we will teach you how to",
    max_length=30,
    num_return_sequences=2
)
[{'generated_text': 'In this course, we will teach you how to make your world better. Our courses focus on how to make an improvement in your life or the things'},
 {'generated_text': 'In this course, we will teach you how to properly design your own design using what is currently in place and using what is best in place. By'}]

Mask filling

您将尝试的下一个pipeline是
fill-mask
。此任务的想法是填充给定文本中的空白:

from transformers import pipeline

unmasker = pipeline("fill-mask")
unmasker("This course will teach you all about <mask> models.", top_k=2)
[{'score': 0.19198445975780487,
  'token': 30412,
  'token_str': ' mathematical',
  'sequence': 'This course will teach you all about mathematical models.'},
 {'score': 0.04209190234541893,
  'token': 38163,
  'token_str': ' computational',
  'sequence': 'This course will teach you all about computational models.'}]

top_k
参数控制要显示的结果有多少种。请注意,这里模型填充了特殊的<
mask
>词,它通常被称为掩码标记。其他掩码填充模型可能有不同的掩码标记,因此在探索其他模型时要验证正确的掩码字是什么。

命名实体识别

命名实体识别 (NER) 是一项任务,其中模型必须找到输入文本的哪些部分对应于诸如人员、位置或组织之类的实体。让我们看一个例子:

from transformers import pipeline

ner = pipeline("ner", grouped_entities=True)
ner("My name is Sylvain and I work at Hugging Face in Brooklyn.")
No model was supplied, defaulted to dbmdz/bert-large-cased-finetuned-conll03-english ...
[{'entity_group': 'PER',
  'score': 0.9981694,
  'word': 'Sylvain',
  'start': 11,
  'end': 18},
 {'entity_group': 'ORG',
  'score': 0.9796019,
  'word': 'Hugging Face',
  'start': 33,
  'end': 45},
 {'entity_group': 'LOC',
  'score': 0.9932106,
  'word': 'Brooklyn',
  'start': 49,
  'end': 57}]

我们在pipeline创建函数中传递选项
grouped_entities=True
以告诉pipeline将对应于同一实体的句子部分重新组合在一起:这里模型正确地将“Hugging”和“Face”分组为一个组织,即使名称由多个词组成。

命名实体识别(中文)

运行来自
https://huggingface.co/shibing624/bert4ner-base-chinese
README的代码

pip install seqeval
import os
import torch
from transformers import AutoTokenizer, AutoModelForTokenClassification
from seqeval.metrics.sequence_labeling import get_entities

os.environ["KMP_DUPLICATE_LIB_OK"] = "TRUE"

# Load model from HuggingFace Hub
tokenizer = AutoTokenizer.from_pretrained("shibing624/bert4ner-base-chinese")
model = AutoModelForTokenClassification.from_pretrained("shibing624/bert4ner-base-chinese")
label_list = ['I-ORG', 'B-LOC', 'O', 'B-ORG', 'I-LOC', 'I-PER', 'B-TIME', 'I-TIME', 'B-PER']

sentence = "王宏伟来自北京,是个警察,喜欢去王府井游玩儿。"

def get_entity(sentence):
    tokens = tokenizer.tokenize(sentence)
    inputs = tokenizer.encode(sentence, return_tensors="pt")
    with torch.no_grad():
        outputs = model(inputs).logits
    predictions = torch.argmax(outputs, dim=2)
    char_tags = [(token, label_list[prediction]) for token, prediction in zip(tokens, predictions[0].numpy())][1:-1]
    print(sentence)
    print(char_tags)

    pred_labels = [i[1] for i in char_tags]
    entities = []
    line_entities = get_entities(pred_labels)
    for i in line_entities:
        word = sentence[i[1]: i[2] + 1]
        entity_type = i[0]
        entities.append((word, entity_type))

    print("Sentence entity:")
    print(entities)


get_entity(sentence)
王宏伟来自北京,是个警察,喜欢去王府井游玩儿。
[('宏', 'B-PER'), ('伟', 'I-PER'), ('来', 'I-PER'), ('自', 'O'), ('北', 'O'), ('京', 'B-LOC'), (',', 'I-LOC'), ('是', 'O'), ('个', 'O'), ('警', 'O'), ('察', 'O'), (',', 'O'), ('喜', 'O'), ('欢', 'O'), ('去', 'O'), ('王', 'O'), ('府', 'B-LOC'), ('井', 'I-LOC'), ('游', 'I-LOC'), ('玩', 'O'), ('儿', 'O')]
Sentence entity:
[('王宏伟', 'PER'), ('北京', 'LOC'), ('王府井', 'LOC')]

或者通过使用
nerpy
库来使用 shibing624/bert4ner-base-chinese 这个模型。

另外,可以使用的ltp来做中文命名实体识别,其Github仓库
https://github.com/HIT-SCIR/ltp
有4.9K的星

问答系统

from transformers import pipeline

question_answerer = pipeline("question-answering")
question_answerer(
    question="Where do I work?",
    context="My name is Sylvain and I work at Hugging Face in Brooklyn",
)
{'score': 0.6949753761291504, 'start': 33, 'end': 45, 'answer': 'Hugging Face'}

请注意,此pipeline通过从提供的上下文中提取信息来工作;它不会凭空生成答案。

文本摘要

文本摘要是将文本缩减为较短文本的任务,同时保留文本中的主要(重要)信息。下面是一个例子:

from transformers import pipeline

summarizer = pipeline("summarization", device=0)
summarizer(
    """
    America has changed dramatically during recent years. Not only has the number of 
    graduates in traditional engineering disciplines such as mechanical, civil, 
    electrical, chemical, and aeronautical engineering declined, but in most of 
    the premier American universities engineering curricula now concentrate on 
    and encourage largely the study of engineering science. As a result, there 
    are declining offerings in engineering subjects dealing with infrastructure, 
    the environment, and related issues, and greater concentration on high 
    technology subjects, largely supporting increasingly complex scientific 
    developments. While the latter is important, it should not be at the expense 
    of more traditional engineering.

    Rapidly developing economies such as China and India, as well as other 
    industrial countries in Europe and Asia, continue to encourage and advance 
    the teaching of engineering. Both China and India, respectively, graduate 
    six and eight times as many traditional engineers as does the United States. 
    Other industrial countries at minimum maintain their output, while America 
    suffers an increasingly serious decline in the number of engineering graduates 
    and a lack of well-educated engineers.
"""
)

与文本生成一样,您指定结果的
max_length

min_length

翻译

对于翻译,如果您在任务名称中提供语言对(例如“
translation_en_to_fr
”),则可以使用默认模型,但最简单的方法是在
模型中心(hub)
选择要使用的模型。在这里,我们将尝试从法语翻译成英语:

pip install sentencepiece
from transformers import pipeline

translator = pipeline("translation", model="Helsinki-NLP/opus-mt-fr-en", device=0)
translator("Ce cours est produit par Hugging Face.")
[{'translation_text': 'This course is produced by Hugging Face.'}]

将英语翻译成中文:

from transformers import pipeline

translator = pipeline("translation", model="Helsinki-NLP/opus-mt-en-zh", device=0)
translator("America has changed dramatically during recent years.")
[{'translation_text': '近年来,美国发生了巨大变化。'}]

偏见和局限性

如果您打算在正式的项目中使用经过预训练或经过微调的模型。请注意:虽然这些模型是很强大,但它们也有局限性。其中最大的一个问题是,为了对大量数据进行预训练,研究人员通常会搜集所有他们能找到的内容,中间可能夹带一些意识形态或者价值观的刻板印象。

为了快速解释清楚这个问题,让我们回到一个使用BERT模型的pipeline的例子:

from transformers import pipeline

unmasker = pipeline("fill-mask", model="bert-base-uncased", device=0)
result = unmasker("This man works as a [MASK].")
print([r["token_str"] for r in result])

result = unmasker("This woman works as a [MASK].")
print([r["token_str"] for r in result])
['carpenter', 'lawyer', 'farmer', 'businessman', 'doctor']
['nurse', 'maid', 'teacher', 'waitress', 'prostitute']

当要求模型填写这两句话中缺少的单词时,模型给出的答案中,只有一个与性别无关(服务员/女服务员)。