显示标签为“编程”的博文。显示所有博文
显示标签为“编程”的博文。显示所有博文

2012年8月10日星期五

一个编程语言之争的小话题


编程语言之争
     -- 近期(201208)源于新浪微博的编程语言之争,仅限于C与C++。

> 话题

最近在IT程序员业界,颇受关注的一个热门话题:比较C与C++的坑多坑少,然而孰优孰劣的问题。

> 故事

关于该话题,几位技术大拿们纷纷讨论起,有点儿激烈。类似这样的编程语言之争的话题,好像一直以来都是程序员的鸡血话题。不管你是菜鸟(...)或是牛人(如Andrei Alexandrescu)或是专家(如Linus)或是科学家(如Kenneth Lane Thompson 肯爷爷)例子举的都是国外的大人物都有过语言之争你可以Google搜搜的,都很容易卷入进去激烈讨论争辩一通,后续的场面很可能就是各自选好队占然后开始对阵谩骂激战,越来越激烈……话说回来,针对以上该话题目前大家均表现得矜持、斯文一些,没有对骂阵势,但后续不知,也可能就此打住了。

不过,倒是看头不少,该话题源自新浪微博@左耳朵耗子 ,后来相续大拿们各自撰文说明各自观点。涉及的内容都挺值得围观。
技术争论不要停留在非黑即白的二元价值观上,这样争论无非就是比谁的嗓门大,比哪一方的观点强,毫无价值。我们应该多看看技术是怎么演进的,怎么取舍的。”   @左耳朵耗子

还有,关于故事经过描述亦可参见这里


> 派系

为剧情的需要,在此我想简单做一下派系划分,并推几位鲜明的代表人物(均为业界大拿),同时引其微博为证。据个人娱乐估摸^_^,“派系”可以划分如下:
挺C++派
@左耳朵耗子 : 说C++比C的坑更多的人我可以理解,但理性地思考一下。C语言的坑也不少啊,如果说C语言有90个坑,那么C++就是100个坑(另,我看很多人都把C语言上的坑也归到了C++上来),但是C++你得到的东西更多,封装,多态,继承扩展,泛型编程,智能指针,……,你得到了500%东西,但却只多了10%的坑,多值啊。
@miloyip:“自己的舊文《C++强大背后》中也有一些相似的觀點。 http://t.cn/h4ARFo 另外,對於遊戲程序員來說(@简悦云风 除外),多數不能避免使用C++。”
 ……
挺C派
@简悦云风:“不要用 C++ 直接用 C , 就没那么多坑了。“
@淘宝褚霸:“自从5年前果断扔掉C++,改用了ansi c后,我的生活质量大大提升,没有各种坑坑我。”
@Laruence: “我确实用不到, C语言灵活运用struct, 可以很好的满足这些需求.//@左耳朵耗子: 封装,继承,多态,模板,智能指针,这也用不到?这也学院派?//@Laruence: 问题是, 这些东西我都用不到… C语言是工程师搞的, C++是学院派搞的”
 ……
中立派
     @老赵,……
围观者
     多数微博程序猿群众们,包括我。

其实,我的观点是挺@左耳朵耗子的,很赞成其博文《C++的坑真的多吗?》观点(挺C++)。文章描述了该话题的发生的过程,说明的观点鲜明,且引用的论据有意思。语言要简单易用没用,基本的道理而已,但要相信高级的东西设计出发点都有“简单易用”这点,只是后来发展发展相对不简单了。Unix发展到今天也不见的啊,C语言发展到今天也不简单的啊。嘿,你别说就很简单,那...那你说说有多简单?

> 引文

作者陈皓(@左耳朵耗子),关于陈皓参见这里

Laruence,关于语言的选择-选易用的
Laruence,PHP开发组成员, PECL开发者. Yaf, Taint等Pecl扩展作者.,

miloyip,C++背后强大

Milo Yip是香港同胞,现任职于上海麻辣马,开发多平台游戏项目。
在高一高二时兼职开发游戏《王子传奇》后,便潜心向学(伪),得到认知科学学士和系统工程及工程管理学哲学硕士后, 于大学里做游戏科技的相关项目,直至2008年才来到上海,再次投身游戏业界,之前的作品是《美食从天而降》Xbox360/PS3/Wii/PC。

赵劼,资深码农,InfoQ中文站编辑,Jscex类库作者

(完)

2012年7月16日星期一

谈谈内存越界读写的危险

谈谈内存越界读写的危险
  -- 记某一次低级而糟糕的程序犯错。


# 故事
这是发生在上周的事情了。当时就有想法,要写篇文章以表示‘铭记’。

想必,这个话题对于C/C++程序员一定很熟悉,都有过或多或少,或轻微或严重(惨痛)的经历吧。

最近正在开发的项目,正在内部开发人员自己测试阶段,突然爆出好多的程序崩溃,大概估摸了下这些崩溃的规律如下:

     1,程序崩溃不确定性但必崩,只要你多操作几次,或重启程序再来试试。
     2,这些崩溃基本都不一样,(从堆栈看来)分散在不同的几个模块;
     3,分析程序崩溃堆栈看到崩溃在正常的代码上(显然不可能),亦看不出错误代码地方了。
     4,由于有辅助程序可在程序崩溃时候帮助自动抓取程序堆栈。但是有时程序崩溃无堆栈或堆栈数据无效。
……

最后,辛苦地安分地回头好好Review最近提交的代码,(……由于这几次提交改动均无法自己做到完整的测试保证,只能自己简单的功能程序和代码Review……),涉及的代码量较大,所以花了不少时间才找到了错误所在,有地方内存越界读写了。虽然找到了错误并很快纠正过来了,但是那样的低级错误让吾情何以堪?也正是为了纪念这么一‘尴尬’,我才想整理一篇博文表示‘罪过’。前车之鉴,后车之师。


个人觉得,上面总结的规律四点不妨可作为“内存越界读写”这样错误的表象。若以后还是遇见这样情况,不如先怀疑怀疑你的内存被越界读写了,赶紧回头仔细Review代码吧,相信你已很快顺利找到错误地方……

# 示例
下面内容就简单描述一下如何导致程序乱蹦,即越界“乱踩”内存了!示例代码如下:

struct A {
     ...
};
struct B {
     ...
     A a;
     ...
};

// 错误程序如下:
void some_func(struct B* b_ptr) {
     ...
     memset(&(b_ptr->a), 0, sizeof(*b_ptr)); // Error: memory overwrite.
     ...
}

// 修正程序如下:
void some_func(struct B* b_ptr) {
     ...
     memset(&(b_ptr->a), 0, sizeof(b_ptr->a));    // 1. ok
     // memset(&(b_ptr->a), 0, sizeof(struct A)); // 2. or else, also well.
     ...
}

果然吧,错误的程序那么低级,So stupid,而找到了错误修正程序那么简单的!程序就恢复正常了。
我想当时写程序脑子犯晕了吧。好了,不再多说了。还是我以前的那么句话,“以后写程序时候头脑得时刻保持精神点,否则后果有你好受的。”

# 借鉴
最后顺便引用一段关于内存越界的描述吧,延伸看看,想想。
--
对Memory overwrite 和memory corruption有什么好办法哪?常见的,就是设置CPU的breakpoint,也就是对某一地址的写操作,dump出stack,然后找是哪个模块写坏的。或者可以把整个page设置成只读,在操作系统层面检查,然后dump。这些对只读的变量或者page还还用,但是对可读写的变量又该怎么办哪?如何区分合法的读写和非法的读写?
对于非法的读写,也有在语言层次进行控制的,比如c++/java/c#等面向对象的语言,就有对对象的保护。但是这些语言并没有排除全局变量,也就是说,在语言层次的控制并不彻底。基于语言的静态检查是个好办法,但是对大规模代码,检查也需要时间和精力。任何经过仔细review的代码都是好代码,但是,有哪些公司能够坚持严格的代码review哪?
对于第三方和legacy的代码,review也基本是不现实的,但是静态检查是第一道防线,一定要坚持。
在操作系统层面,没有对象的概念,导致在操作系统层面没法保护。或者说,操作系统层面的保护代价太大了。我觉得还是操作系统的设计思想没有体现保护,隔离的要求,传统操作系统在这方面,做的很不够。
新的操作系统应该能够在更细粒度上做到隔离,保护,但是性能,通信等等又如何解决?

-- 

还有,在stackoverflow上面关于内存越界的一段讨论,这里

(完)




2012年6月30日星期六

单链表删除节点的技巧

单链表删除节点的技巧
     -- 注:删除节点非尾节点!
 


在此分享一个小小技巧,在单向链表中删除节点时不必知道其前面节点(若存在前面节点的话),亦可高效地完成删除节点。

===
从一篇别人的博文说起吧,引用自这里,内容很少,仅此如下:

标题:学习数据结构的感想
在链表中删除动作比较多的时候,用双联表比用单链表效率要高,双链表不用从头开始遍历去删除,而是可以直接删除。
用单链表删除的时候,每次知道需要删除元素的前一个指针就好了。不过这个好像很难知道。

我是在无意之间,看到那位朋友的上面问题,先是无意间网上搜到他转载了我的一篇博文《STL稳定排序源码分析》,好奇之下到了他的csdn博客的。
所以我就一时兴致勃勃回复了其问题,与之讨论,内容如下:

===
单向链表删除节点时候,可做到不需要知道(若有的话)上一个节点同样完成删除操作。你可以这么做,描述如下:
原来的链表 ... A -> B[b_ptr] -> C[c_ptr] -> D[d_ptr] ...
现在要删除节点 B,其对于指针为b_ptr注:删除节点非尾节点,重要

首先你可以使用B得到C,D指针,c_ptr和d_ptr,然后你可以用C去覆盖掉B,即*b_ptr = *c_ptr,那么这时你可以删除C即可达到你要的结果即删除节点B。具体操作:
*b_ptr = *c_ptr;
b_ptr->next = d_ptr;
delete c_ptr;

结果的链表 ... A -> C[b_ptr] -> D[d_ptr] ...
按照以上的操作,这样就不需要从头去遍历链表定位上一个节点A了,简单有效吧☺。

===
但是,删除节点不应该为尾节点(链表的最后一个节点),因为B作为尾节点,虽然你可以删除节点B,但无法给节点A的next赋值NULL。若删除的节点为尾节点,那就得老老实实从头遍历链表了,将其前面节点A的next赋NULL。

===
当然你说的“用双(向)链表比用单链表效率要高”,是对的。确切说应该叫实用性高吧,但是其实现的逻辑要复杂不少,可参见STL链表的实现源码。像C++ STL里面的链表(包括单向双向)底层的实现都是以双向链表形式的。可以参考sgi stl源码std::liststd::slist

(完)

2012年6月28日星期四

一种特殊的排序算法 II



一种特殊的排序算法 II
  -- 计数排序(Counting sort)

后续想找时间再写一篇《一种特殊的排序算法 II》,关于计数排序☺。(引自 《一种特殊的排序算法 I》)

所以,继上篇文章《一种特殊的排序算法 I》,在此就(继续)描述,总结另一种特殊的排序算法,计数排序。算法的步骤如下:
  1. 找出待排序的数组中最大和最小的元素
  2. 统计数组中每个值为i的元素出现的次数,存入数组C的第i
  3. 对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加)
  4. 反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1

引用自维基内容,完整介绍可以参见计数排序(维基)

(后面,开始分享我的内容)
关于计数排序(算法),可以划分两种具体的计数法,如下:
 * 比较计数法
 * 分布计数法

===
> 比较计数法
算法 ComparisonCountingSort(A[0 ... n-1])
          // 用比较计数法对数组排序
          // 输入:可排序数组 A[0 ... n-1]
          // 输出:将A中元素按照升序排列的数组 S[0 ... n-1]
          for i : 0 to n-1 do 
               Count[i] = 0;
          for i : 0 to n-2 do
               for j : i+1 to n-1 do
                    if A[i] < A[j]
                         Count[j] += 1;
                    else
                         Count[i] += 1;
          for i : 0 to n-1 do
               S[Count[i]] = A[i];
          return S;

该算法的时间效率如何?答案为O(n^2)。因为该书法执行的键值比较次数和选择排序一样多,并且还占用了线性数量的额外空间,我们几乎不能推荐它来实际的应用。但是计数思想在一种情况下还是卓有成效的,在这种情况下,待排序的元素的值都来自于一个已知的小集合,这就是下面要描述的另一种计数排序算法,分布计算法。

===
> 分布计数法
算法 DistributionCountingSort(A[0 ... n-1, l, u])
          // 用分布计数法对数组排序,对来自于有限范围整数的一个数组进行排序
          // 输入:可排序数组 A[0 ... n-1],数组中的整数位于l和u之间( l <= u)
          // 输出:将A中元素按照升序排列的数组 S[0 ... n-1]

          for i : 0 to u-l do 
               D[i] = 0;                       // 初始化频率数组
          for i : 0 to n-1 do 
               D[A[i] - l] = D[A[i] - l] + 1;  // 计算频率值
          for i : 1 to u-l do 
               D[i] = D[i-1] + D[i];           // 重用于分布值

          for i : n-1 downto 0 do 
               j = A[i] - l;
               S[D[j] - 1] = A[i];             // D[j] - 1]为对应的数组下标
               D[j] -= 1;

 
        return S;

注:频率值,表示元素出现的次数;分布值表示在最后有序数组中,元素最后一次出现的位置。


假设数组值的范围是固定的,这显然是一个线性效率的算法,因为它仅仅对输入数组A从头到尾连续处理两遍,其时间复杂度 O(n)。然而,要重点记忆的是,除了空间换时间之外,分布计数排序这种高效率是因为利用了输入列表独特的自然属性。

===
附:
文章内容乃学习心得(笔记),亦包括引用自《算法设计与分析基础(第2版)》内容(第七章时空权衡,7.1计数排序)。
以上描述的算法,以C\C++完成了简单的实现,示例代码参见 github


(完)

2012年6月26日星期二

一种特殊的排序算法 I


一种特殊的排序算法 I
     -- 位排序 OR 桶排序


绝大部分经典的排序算法,都是以元素间的比较为基础,如冒泡,选择,插入,快速,归并,堆排序等,虽然这些排序算法的时间和空间复杂度存在不同,但其算法平均时间复杂度均不超过O(n logn) (基数排序也比较特殊可做到O(k*n),详细对比情况可以参见维基排序算法
===

而下面内容描述的排序算法比较特殊,并非以元素间比较为基础的,其算法时间复杂度达到了O(n)。先来看看一道简单的示例题目吧(引用自:Google groups TopLanguage
   
特殊排序算法题目,如下:
     姓名集合names,名次集合ranks,按照名次顺序输出她们的名字,要求O(N)的时间复杂度。

解法如下: 
     解法一,不改变原来集合顺序,O(N)时间, O(N)空间。
     描述: 数组下标相当于姓名索引,新空间以名次顺序依次记录着姓名索引。

     解法二,在位重新排序,改变原来集合顺序,O(N)时间, O(1)空间。
     描述:每一次swap确定一元素的位置,最多总共N次swap能全部定位。


具体的程序设计(C/C++)代码在这里
===

有一种特殊的排序算法,在《编程珠玑I II》有讲述到的位排序(bitsort),也称桶排序
其实,上面的排序题目正是位排序的一种变种的应用,不知你是否已经发现了否?

位排序算法的整体思想,可以描述如下:
     1. 每字节有8bit(位),那么它就可标记8个连续的数,分别对应每bit位,若数值存在则对应bit位置为1。
     2. 把N个数分为1+N/8组(相当于有这么多个桶),每组标记连续的8个数。
     3. 申请(1+N/8)字节数组均初始化为0,遍历要排序的集合,存在的元素在对应的bit位标记1。
     标记规则: array[i/8] = (array[i/8] | (1<<(i%8)))
注:算法描述的字节亦可替换为整型类型,对应的比特位数字变为32。


===
关于位排序,同样可以瞄瞄其他网友描述,这里
后续想找时间再写一篇《一种特殊的排序算法 II》(20120629更新链接),关于计数排序☺。

(完)



2012年6月21日星期四

资源的拥有权


资源的拥有权
     -- C++程序申请与释放资源小结

开篇先引用一下,《Effective C++》的条款16:成对使用new和delete时要采取相同形式。简单的示例就是下面这样的,如下:

  example 1.
     type* ptr = new type;
     ...
     delete ptr;


  example 2.   
     type* ptr = new[10] type;
     ...
     delete[] ptr;

 
  还有C语言的版本,如下:
example 3.
     type* ptr = (type*) malloc(sizeof(type));
     ...
     free(ptr);   
      
一条编程实践的经验条款,经如上的简单示例显得很简单,浅显易懂。类似地,我想在此延伸到另一点:在哪里申请资源,最后就在哪里释放资源吧。
PS. 这里提到的“哪里”,其实理解了模块更好一些。若缩小了(作用域)范围如函数显然不妥,做不到在函数A申请资源,又在函数A释放资源,毕竟资源是要提供给对外使用的。若“哪里”理解成类型,好像还可以如某工厂类,其负责对象的创建和析构。

比如说,在模块A申请资源R,资源R属于多模块B,C,D等共享的,那么最后应该同样在模块A释放资源R。
虽然,一般情况下你在其它模块最后使用完资源R之后即完成释放,不会出现什么问题,但是这样的逻辑看着也令人觉得也不合理乎。“某女人生的孩子,日子过着过着,结果孩子长大成人了,却客死他乡了。”多么令人遗憾,心生不妥啊。(额,我承认举得例子太言重,恐怖了)

某些情况下,这种做法(在其它模块是否资源R)明显会出现程序异常情况,如释放资源导致程序崩溃。

什么情况下呢?
答:当其它模块如B,C,D模块编译链接时候所使用的运行时库与模块A的不同,如VC++编译器的不同运行时库包括如下:
多线程 (/MT)
多线程调试 (/MTd)
多线程 DLL (/MD)
单线程 (/ML)
单线程调试 (/MLd)



还有另一种情况,更可能导致程序异常,模块A与其他模块使用着不同的内存分配器

所以,你应该(或必须)做到在同一模块申请和释放内存资源。如模块A存在这样成对接口:
resource* allocate_resourse(...);
void release_resourse(resource* res);

这样,不管模块A,或其他模块B,C,D若需要新的resource,即通过模块A接口allocate_resourse申请;若最后用完了resource,即通过模块A接口release_resourse完成释放。逻辑也足够简单。


忌讳、痛恨那种在模块A申请资源,却在其他模块做释放资源的。很可能,最后你(程序)怎么死的都不知道。
好吧,最后总结一句,资源的拥有权始终保持唯一性。

(完)

2012-06-18/ Junkun Huang.



2012年5月29日星期二

单例安全性


单例安全性

# 单例模式

单例模式,一种用于确保整个应用程序中只有一个类实例且这个实例所占资源在整个应用程序中是共享时的程序设计模式。
关于单件模式的更多讲解可以参见维基百科百度百科这里

# 实现模式
(转自维基百科)
实现单例模式的思路是:一个类能返回对象一个引用(永远是同一个)和一个获得该实例的方法(必须是静态方法,通常使用getInstance这个名称);当我们调用这个方法时,如果类持有的引用不为空就返回这个引用,如果类保持的引用为空就创建该类的实例并将实例的引用赋予该类保持的引用;同时我们还将该类的构造函数定义为私有方法,这样其他处的代码就无法通过调用该类的构造函数来实例化该类的对象,只有通过该类提供的静态方法来得到该类的唯一实例。
单例模式在多线程的应用场合下必须小心使用。如果当唯一实例尚未创建时,有两个线程同时调用创建方法,那么它们同时没有检测到唯一实例的存在,从而同时各自创建了一个实例,这样就有两个实例被构造出来,从而违反了单例模式中实例唯一的原则。 解决这个问题的办法是为指示类是否已经实例化的变量提供一个互斥锁(虽然这样会降低效率)。

# 单例安全性


由于保证单例唯一性,引进互斥锁导致效率的问题,其实存在某种良好的解决办法的。
综上所述,对于单例模式,其具体的程序实现(方案)要点概括如下:对于单例模式,其具体的程序实现(方案)要点概括如下:
1 单例对象延迟加载(构造),。即使用的时候(获取时)才给予申请构造。

2 获取单例对象时,“双重检测”(判断)是否需要构造对象,。可解决上述的效率问题。

在此,我想说更多的是“双重检测”技术,保证单例对象的多线程访问安全性。

# 示例说明
去年,项目组的周会讨论过,关于创建件模式的安全性问题。那时候,我‘引荐’了“双重检测”该方法,同时提到Loki类库实现单例模式的做法。当时,大家却表现“未曾相识”的感觉,所以我那天晚上发了一段解释到项目组的邮件群。如下:
……
之前的周会讨论过,关于创建件模式的安全性问题。大家是否还有印象? **同学应该还有印象的吧,呵呵!
好像大家对此了解不多,所以我晚上在家里翻出了Loki老代码查看对照了下,在此稍微总结下,和大家一起分享和讨论。

Loki泛型类库实现件,使用的是“双检测”件对象,保证创建安全。见图说明:



btw:
附件 -- Loki件的实现。

至于Loki类库是什么,大家可以google下就知道了!
先描述这么多吧,有情况再讨论。
--

(完)

2012年5月27日星期日

锁,编程你需要知道的!

锁的归纳
     -- windows系统编程的锁(Linux系统类似)

lock & unlock


# 锁的概念
当然,在此要讲述的锁并非生活中的锁(门锁,车锁等)。而是编程世界的锁。
锁,在并发程序逻辑中保证某操作的同步性的一利器。该并发包括多线程与多进程(的线程)两种情况。
举个例子说明情况吧,如下:

Lock lock;
Job job;

func1()
{// run in thread1 or program1.
     do_lock(lock);
     handle(job);
     ... 
}

func2()
{// run in thread2 or program2.
     do_lock(lock);
     handle(job);
     ... 
}

这样,借助锁变量lock的同步操作,就可以保证处理事务job的原子性。在同一时刻只允许在一个地方处理事务job。
若时刻t,程序先进入func1执行handle(job)但未完成时,lock已被加锁,那么同时再进入func2时候由于lock已被加锁,导致程序会停在等待lock被解锁。
直到执行handle(job)完成,func2得到锁了才开始执行handle(job)。反之亦然。

# 锁的抽象设计

锁,最关键的两个操作:
a. 加锁
b. 解锁
而其构造函数,需根据不同实现方案而定。其他的操作可根据不同需求情况而定。

一般情况,锁都不希望被设计成具有可复制性。

比如我之前设计过的锁,示例如下:
class locker
{
    LIMIT_COPY_AND_ASSIGN_CTOR(locker)

public:
    locker();
     ... other constructor ...
    virtual ~locker() {}
     // main func
    virtual bool lock() = 0;
    virtual bool unlock() = 0;

     // extended func
    virtual bool is_valid() const;
    HANDLE handle() const;
    string_t name() const;
    int get_wait_ret() const;
    unsigned get_wait_time() const;
    void set_wait_time(unsigned wait_time);
    int error() const;
     ... other ...

protected:
    ... member var ...
};

在面向对象编程,我们一般把加锁与解锁的操作对应到一起,设计一个类型将两者包含,一个在构造函数,一个在析构函数。这样做可以保证加锁与解锁的对称性,不管程序中途返回或抛出异常,锁都是安全的。其设计很简单,如下:


template
 <class _Locker>
class scoped_lock_t
{
public:
    scoped_lock_t( _Locker& mutex_obj )
        : _locker(mutex_obj)
    {
        bool lock_ret = _locker.lock();
        assert (lock_ret);
    }
    ~scoped_lock_t()
    {
        bool lock_ret = _locker.unlock();
        assert (lock_ret);
    }
private:
    _Locker& _locker;
};

typedef scoped_lock_t<locker> scoped_lock;


锁的实现方案
     --(windows系统编程实现方案)
1.  互斥变量
Mutex functionDescription
CreateMutexCreates or opens a named or unnamed mutex object.
CreateMutexExCreates or opens a named or unnamed mutex object and returns a handle to the object.
OpenMutexOpens an existing named mutex object.
ReleaseMutexReleases ownership of the specified mutex object.

2. 事件
Event functionDescription
CreateEventCreates or opens a named or unnamed event object.
CreateEventExCreates or opens a named or unnamed event object and returns a handle to the object.
OpenEventOpens an existing named event object.
PulseEventSets the specified event object to the signaled state and then resets it to the nonsignaled state after releasing the appropriate number of waiting threads.
ResetEventSets the specified event object to the nonsignaled state.
SetEventSets the specified event object to the signaled state.

3. 信号量
Semaphore functionDescription
CreateSemaphoreCreates or opens a named or unnamed semaphore object.
CreateSemaphoreExCreates or opens a named or unnamed semaphore object and returns a handle to the object.
OpenSemaphoreOpens an existing named semaphore object.
ReleaseSemaphoreIncreases the count of the specified semaphore object by a specified amount.

4. 临界区变量
Critical section functionDescription
DeleteCriticalSectionReleases all resources used by an unowned critical section object.
EnterCriticalSectionWaits for ownership of the specified critical section object.
InitializeCriticalSectionInitializes a critical section object.
InitializeCriticalSectionAndSpinCountInitializes a critical section object and sets the spin count for the critical section.
InitializeCriticalSectionExInitializes a critical section object with a spin count and optional flags.
LeaveCriticalSectionReleases ownership of the specified critical section object.
SetCriticalSectionSpinCountSets the spin count for the specified critical section.
TryEnterCriticalSectionAttempts to enter a critical section without blocking.

还有其他,详见msdn


线程间的加锁
以上描述到实现方案,都适用于线程间的加锁操作。但是不同锁有不同的用法,效率也不同。
信号量与其他三者的用法不同,其不仅仅具备加锁解锁的功能。信号量对象对线程的同步方式不同于其他,信号量允许多个线程同时使用共享资源 ,这与操作系统中的PV操作相同。

注意仅限于线程间使用的锁,其效率较高,如临界区对象。

之间区别的更详细说明可以参考这里这里

进程间的加锁
上述四种具体的实现方案,除了临界区对象,其他三者都适用于进程间的加锁操作,并且三者创建的对象都是windows系统的内核对象。由于属于内核对象那么系统需要在内核层负责管理这些对象的属性与操作,所以这三者内核对象加锁(解锁)的效率要低于临界区对象。

锁的效率问题
不同锁之间有不同的效率问题。只要你明确该点,且理清并发场景与锁的具体使用情况。
当然,要是无需加锁操作就可以解决问题的实现方案,多半情况下就是最高效的方案了(哦,是对比于加锁方案)。

# 链接