Shell编程之正则表达式详解((合集10篇))由网友“花季少女ycy”投稿提供,以下是小编收集整理的Shell编程之正则表达式详解,希望对大家有所帮助。
篇1:Shell编程之正则表达式详解
正则表达式详解
普通字符:只是字面的意思
元字符:超过字面意思的意义
基本正则表达式元字符及其意义
* #0个或多个在*字符之前的那个普通字符
. #匹配任意字符
^ #匹配行首$ #匹配行尾
$$ #执行上一条命令
!$ #上一条命令的最后一个参数,例如是/etc/passwd,继续对该参数操作,可以为 cat !$
[] #匹配字符集合
#转义符
{n} #匹配前面字符出现n次
{n,} #匹配前面字符至少出现n次
{n,m} #匹配前面字符出现n-m次示例
* hel*o #可以匹配helll8o、hello、hellllo
. ...73.#可以匹配abc73.、!@#73?、12373c
^ ^...X86* #可以匹配以abcX86fang开头的字符
$ make$ #表示以make结尾
^$ #表示空行
^.$ #表示某行只有一个字符
[a-z] #表示a-z所有小写字母
[0-9] #表示0-9所有数字
[b-q] #表示b-q字母
[^b-q]#取反,除了b-q的所有字符
[A-Za-z] [A-Za-z]*#能匹配任何一个英文单词
* #将后面的元字符转换为字面意思
{n} #JO{3}B 匹配值为JOOOB
{n,} #JO{3,}B 匹配值为JOOOB JOOOOB JOOOOOOOB
{n,m}#JO{3,6}B 匹配值为JOOOB JOOOOOB JOOOOOOB
[a-z] {5} #hello house whowh 精确匹配5个小写字母
echo [abc]* #开头为a/b/c的任意多字符的文件
echo [^abc]* #开头不是a/b/c的任意多字符的文件
touch {a,b,c}-{1,2,3} #通过穷举来创建文件,每个文件匹配来创建,总共9个文件
!$#上一条命令中的最后一个参数
;#表示命令结束
===========================================================================
扩展正则表达式元字符及其意义
? #匹配0个或1个在其之前的那个普通字符
+ #匹配1个或多个在其之前的那个普通字符
#表示一个字符集合或用在expr中
|#表示或,匹配一组可选的字符
? JO?B #匹配JO1B JO@B JO!B ,只匹配一个字符,
Shell编程之正则表达式详解
,
如果是JOB或JOOOB则不匹配
+ S+EU #至少匹配前面的字符1次,可以匹配多次
* S*U #可匹配S123U SSUU S!@#U SU
()符号和|符号 re(a|e|o)d #匹配read reed reod
示例
1、列出所有以.awk结尾的文件
ll *.awk
2、列出以0开头,后面跟一个字符且以.pem为后缀的文件
ll 0?.pem
3、列出在a-h范围内以字母开头并以.awk结尾的文件
ll [a-h]*.awk
4、列出以a-h范围内字母开头且句点后不是以.awk结尾的文件
ll [a-h]*.[^awk]*
5、列出满足在a-h范围内以字母开头并以.awk结尾的文件,或列出以0开头,后面跟一个字符且以.pem为后缀的文件
ll {[a-h]*.awk,0?.pem}
篇2:Shell编程之grep命令详解
grep 一种强大的文本搜索工具,它能使用正则表达式搜索文本,并把匹配的行打印出来
grep [option] [mode] [file]
-c #只输出匹配行的数量
-i #搜索时忽略大小写
-h #查询多文件时不显示文件名
-l #只列出符合匹配的文件名,而不列出具体的匹配行
-n #列出所有的匹配行,并显示行号
-s #不显示不存在或无匹配文本的错误信息
-v #显示不包含匹配文本的所有行
-w #匹配整词
-x #匹配整行
-r #递归搜索,不仅搜索当前工作目录,而且搜索子目录
-q #禁止输出任何结果,以退出状态表示搜索是否成功
-b #打印匹配行距文件头部的偏移量,以自己为单位
-o #与-b选项结合使用,打印匹配的词距头部的偏移量,以字节为单位
-E #支持扩展的正则表达式
-F #不支持正则表达式,按照字符串的字面意思进行匹配
grep -c root /etc/passwd #文本中有两个root匹配,即显示行数为2
grep -i xxx file #文本中有XXX和xxx,-i之后会忽略大小写都输出出来
grep -h root /etc/passwd /etc/shadow #原本查询多文件时,匹配会显示出文件名及匹配行,加-h之后只会显示匹配的行,而不会显示匹配与哪个文件
grep -l root /etc/passwd /etc/shadow #加-l之后只会显示匹配的文件名,而不列出具体的匹配行
grep -n root /etc/passwd #-n在输出匹配结果前面会加匹配的行号
grep -s root /etc/passwd #不显示错误信息,例如: grep root /etc/passwdwdwd
grep -v root /etc/passwd #列出除了匹配行root以外的所有行
grep -w root /etc/passwd #列出文件中有单个root的单词的行
grep -x root /etc/passwd #列出文件中有单个root为整行的行
grep -r root /tmp #列出/tmp下面的root及子目录文件下的root,递归查询出所有的root字符
grep -q root /tmp/root #不输出任何信息,以退出方式,成功为0,不成功为其他值
grep -vc root /etc/passwd /etc/shadow #显示出文件pass和sha中有多少行是不包含root的行
1、匹配行首(1)、检索“/etc/passwd”中以“root”开头的行,并打印出行号
grep -n ^root /etc/passwd
(2)、检索“/etc/services”文件中的空行,列出行号,再次检索空行有几行
grep -n ^$ /etc/services
grep -c ^$ /etc/services
(3)、搜索文件中以“-”开头,并且重复任意次,然后是D字符的匹配行
搜索文件中以“/”开头,中间“4”个任意字符,第“6”个字符仍为“/”的匹配行
grep ^-*D filename
grep ^/..../ filename
(4)、搜索“sed.edu.cn”的匹配行
grep sed.edu.cn
(5)、搜索“-”符号重复5次的文本行
搜索“the”精确匹配的行
grep ‘-{5}‘ filename #考验‘‘和{}表达式的特性
grep ‘
grep #标准grep命令,
支持正则表达式
egrep #扩展grep命令。支持基本和扩展正则表达式
fgrep #快速grep命令。不支持正则表达式,按照字符串的字面意思进行匹配
egrep 命令与 grep -E等价
fgrep 命令与 grep -F等价
分析下面的正则表达式表达了什么含义
(1)、kK* grep kK* test #列出文件test包含kK及后面有任意多个字符的行
(2)、k{6,8} grep ‘k{6,8}‘ test #列出文件test中k单词重复6-8个的行
(3)、k{6,} grep ‘k{6,}‘ test #列出文件test中k单词大于6个的行
(4)、k{10} grep ‘k{6}‘ test #列出文件test中k单词为6个的行
(5)、^NEW YEAR$
(6)、^$ #空行
(7)、[0-9][0-9][a-z] grep [0-9][0-9][a-z] file #列出文件中前两个字符为数字,后一个字符为字母的匹配行
(8)、[A-H]{1,3},[0-9]{5} #grep ‘[a-h]{1,3}‘ test | grep ‘[0-9]{5}‘ #列出1-3个a-h单词,5个数字的字符
(9)、^...
(10)、[^p-z]*. #列出除了p-z以外的多个任意字符的行
2、利用通配功能列出某目录下所有以数字开头,最后3为是句点和2个任意字母的文件名
[0-9]*[a-z]{2}...
3、查看下面三条命令
grep -c ^$ filename #列出空行的数目
grep -c ^[^$] filename #列出非空行的数目
grep -c ^^$ filename
4、统计当前目录及子目录下的所有文件所包含空白行的行数
grep -r ^$ /root/ | wc -l
5、统计当前目录及子目录下的所有文件包含非空白行的行数
grep -r ^[^$] /root/ | wc -l
6、结合对-符号的阐述,观察是否对错
grep -n -{5,} file #缺引号,缺转义符-
grep -n ‘-{5,}‘ file #缺转义符,系统默认不知道-是选项
grep -n ‘-{5,}‘ file #正确
篇3:linuxshell编程之正则表达式
正则表达式和通配符区别:
通配符也就三个*、?、[]
正则表达式是在文件中进行查找,
linuxshell编程之正则表达式
,
篇4:shell编程之cut
cut以文件中行为单位,根据你设定的条件,把部分内容剪切打印出来,输出到标准输出,并不会保存到任何文件中
-b 范围 根据字节确定范围-c 范围 根据字符确定范围-f 范围根据设定field确定范围。默认是TAB分割field。使用-d指定。
范围设定的格式n 剪切n位的内容。-n 剪切1-n位的内容。n- 剪切n-最后一位的内容,
n-m 剪切n-m位的内容
-d ‘:’ 指定分隔符。-s 只打印有分隔符存在的行。--out=delimiter=string 制定分隔符的输出,可以用来格式化输出。
-b与-c的区别就是字符跟字节的区别。字节是存储在计算机里面的最小单位。而字符是指编码里面有一个意义的独立单元。比如汉字编码中的一个汉字或者一个标点,又或者acsii码里面的一个字母或者标点。所以一个字符肯呢过包含一个或多个字节。就这么个关系。
例子:cut -d ‘:‘ -f 2-3 -s /etc/passwd
篇5:Linux 操作系统编程之Shell问答录
Q1: shell如何执行“简单”命令?
A: 这里的简单命令和bash参考手册里的含义相同,形式上一般是:命令的名称加上它的参数。有三种不同的简单命令:
1.内置命令(builtin)
是shell解释程序内建的,有shell直接执行,不需要派生新的进程。有一些内部命令可以用来改变当前的shell环境,如:
cd /path
var=value
read var
export var
...
2.外部命令(“external command” or “disk command”)
二进制可执行文件,需要由磁盘装入内存执行。会派生新的进程,shell解释程序会调用fork自身的一个拷贝,然后用exec系列函数来执行外部命令,然后外部命令就取代了先前fork的子shell。
3.shell脚本(script)
shell解释程序会fork+exec执行这个脚本命令,在exec调用中内核会检查脚本的第一行(如:#!/bin/sh),找到用来执行脚本的解释程序,然后装入这个解释程序,由它解释执行脚本程序。解释程序可能有很多种,各种shell(Bourne shell,Korn shell cshell,rc及其变体ash,dash,bash,zshell,pdksh,tcsh,es...),awk,tcl/tk,expect,perl,python,等等。在此解释程序显然是当前shell的子进程。如果这个解释程序与当前使用的shell是同一种shell,比如都是bash,那么它就是当前shell的子shell,脚本中的命令都是在子shell环境中执行的,不会影响当前shell的环境。
Q2: shell脚本是否作为单独的一个进程执行?
A: 不是,shell脚本本身不能作为一个进程。如上面讲的,shell脚本由一个shell解释程序来解释、运行其中的命令。这个shell解释程序是单独的一个进程,脚本中的外部命令也都作为独立进程依次被运行。这也就是为什么ps不能找到正在运行的脚本的名字的原因了。作为一个替代方案,你可以这样调用脚本:
sh script-name
这时shell解释程序“sh”作为一个外部命令被显式地调用,而script-name作为该命令的命令行参数可以被我们ps到。
另外,如果你的系统上有pidof命令可用,它倒是可以找出shell脚本进程(实际上应该是执行shell脚本的子shell进程)的进程ID:
pidof -x script-name
Q3: shell何时在子shell中执行命令?
A: 在此我们主要讨论Bourne shell及其兼容shell。在许多情况下shell会在子shell中执行命令:
1.(...)结构
小括号内的命令会在一个子shell环境中执行,命令执行的结果不会影响当前的shell环境。需要注意是此时变量$$会显示当前shell的进程id,而不是子shell的进程id。
参考:{...;}结构中的命令在当前shell中执行,(内部)命令执行的结果会影响当前的shell环境。
2.后台执行或异步执行
command&命令由一个子shell在后台执行,当前shell立即取得控制等候用户输入。后台命令和当前shell的执行是并行的,但没有互相的依赖、等待关系,所以是异步的并行。
3.命令替换
command`(Bourn shell及兼容shell/csh),$(command)(在ksh/bash/zsh中可用)将command命令执行的标准输出代换到当前的命令行。command在子shell环境中执行,结果不会影响当前的shell环境。
4.管道(不同的shell处理不同)
cmd1和cmd2并行执行,并且相互有依赖关系,cmd2的标准输入来自cmd1的标准输出,二者是“同步”的。对管道的处理不同的shell实现的方式不同。在linux环境下大多数shell(bash/pdksh/ash/dash等,除了zshell例外)都将管道中所有的命令在子shell环境中执行,命令执行的结果不会影响当前的shell环境。
Korn shell的较新的版本(ksh93以后)比较特殊,管道最后一级的命令是在当前shell执行的。这是一个feature而非BUG,在POSIX标准中也是允许的。这样就使下面的命令结构成为可能:
command|read var
由于read var(read是一个内部命令)在当前shell中执行,var的值在当前shell就是可用的,
反之bash/pdksh/ash/dash中read var在子shell环境中执行,var读到的值无法传递到当前shell,所以变量var无法取得期望的值。类似这样的问题在各种论坛和news group中经常被问到。个人认为command|read var的结构很清晰,并且合乎逻辑,所以我认为Korn shell的这个feature很不错。可惜不是所有的shell都是这样实现的。:(如开源的pdksh就是在子shell执行管道的每一级命令。
Korn shell对管道的处理还有一个特殊的地方,就是管道如果在后台执行的话,管道前面的命令会由最后一级的命令派生,而不是由当前shell派生出来。据说Bourne shell也有这个特点(标准的Bourne shell没有测试环境,感兴趣的朋友有条件的可以自行验证)。但是他们的开源模仿者,pdksh和ash却不是这样处理。
最特殊的是zshell,比较新的zshell实现(好像至少3.0.5以上)会在当前shell中执行管道中的每一级命令,不仅仅是最后一条。每一条命令都由当前shell派生,在后台执行时也是一样。可见在子sehll中执行管道命令并不是不得已的做法,大概只是因为实现上比较方便或者这样的处理已经成为unix的传统之一了吧。;-)
让我们总结一下,不同的shell对管道命令的处理可能不同。有的shell中command|read var这样的结构是ok的,但我们的代码出于兼容性的缘故不能依赖这一点,最好能避免类似的代码。
5.进程替换(仅bash/zsh中,非POSIX兼容)
<(...)
>(...)
与管道有点类似,例子:cmd1 <(cmd2) >(cmd3), cmd1, cmd2, cmd3的执行是同步并行的。
<(command)形式可以用在任何命令行中需要填写输入文件名的地方,command的标准输出会被该命令当作一个输入文件读入。
>(command)形式可以用在任何命令行中需要填写输出文件的地方,该命令的输出会被command作为标准输入读入。两种形式中的command都在子shell环境中执行,结果不会影响当前的shell环境。
6.if或while命令块的输入输出重定向
在SVR4.2的Bourne shell中对此情况会fork一个子shell执行if块和while块中的命令;在linux下似乎其它的shell中都不这样处理。
7.协进程(ksh)
只有Korn shell和pdksh有协进程的机制(其它shell中可以用命名管道来模拟)。类似于普通的后台命令,协进程在后台同步运行,所以必须在子shell中运行。协进程与后台命令不同的是它要和前台进程(使用read -p和print -p)进行交互,而后者一般只是简单地异步运行。
Q4: 既然在当前shell中执行命令也会派生子shell,那么它与在子shell中执行命令又有什么区别呢?
A: 这种说法不准确。
在当前shell中执行内部命令不会派生子shell,因此有些内部命令才能够改变当前的shell执行环境。
在当前shell中执行外部命令或脚本时会派生子shell,所以这时命令的执行不会影响当前 的shell环境。注意:子shell中执行的内部命令只会改变子shell的执行环境,而不会改变当前shell(父shell)的环境。
Q5: 怎样把子shell中的变量传回父shell?
A: 例如(echo “$a”) | read b不能工作,如何找到一个替代方案?下面给出一些可能的方案:
1.使用临时文件
...
#in subshell
a=100
echo “$a”>tmpfile
...
#in parent
read b
2.使用命名管道
mkfifo pipef
(...
echo “$a” > pipef
...)
read b
3.使用coprocess(ksh)
( echo “$a” |&)
read -p b
4.使用命令替换
b=`echo “$a”`
5.使用eval命令
eval `echo “b=$a”`
6.使用here document
read b <`echo “$a”`
END
7.使用here string(bash/pdksh)
read b <<<`echo “$a”`
8.不用子shell,用.命令或source命令执行脚本。
即在当前shell环境下执行脚本,没有子shell,也就没有了子shell的烦恼。:)
解决的方法还不止于此,其它的进程间通信手段应该也能使用,这有待于大家一起发掘了。^_^
篇6:Shell正则表达式验证IP地址
这篇文章主要介绍了Shell正则表达式验证IP地址,本文给出了多个方法,并分别给出实现代码,需要的朋友可以参考下
本机多个IP
代码如下:
ifconfig | awk ‘/inet/{print $2}‘ | awk -F: ‘{print $2}‘
首先,先用这个来着
代码如下:
CheckIPAddress
{
echo $1 > /tmp/tmpserverip
echo $1 |grep “^[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}$” > /dev/null
if [ $? = 1 ]; then
return 1
else
a=$(cut -d. -f1 /tmp/tmpserverip)
b=$(cut -d. -f2 /tmp/tmpserverip)
c=$(cut -d. -f3 /tmp/tmpserverip)
d=$(cut -d. -f4 /tmp/tmpserverip)
for loop in $a $b $c $d
do
if [ $loop -ge 255 ] || [ $loop -le 0 ]; then
return 2
fi
done
fi
return 0
}
最初的时候,参考过下面的这些
代码如下:
grep “^([01]?dd?|2[0-4]d|25[0-5]).([01]?dd?|2[0-4]d|25[0-5]).([01]?dd?|2[0-4]d|25[0-5]).([01]?dd?|2[0-4]d|25[0-5]).$”
输入ip,检查其是否满足的ip书写规范,即不能大于255,不能有字母,和其他标点,参考网上的,自己搞了个如下,做个标记!@
代码如下:
echo -n ‘Enter the Server-ip:‘
read BISSip
echo $BISSip > /tmp/tmpserverip
echo $BISSip|grep “^[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}$” > /dev/null
num=$?
if [ $num = 1 ]
then
echo error ip,please enter correct
else
{
a=$(cut -d. -f1 /tmp/tmpserverip)
b=$(cut -d. -f2 /tmp/tmpserverip)
c=$(cut -d. -f3 /tmp/tmpserverip)
d=$(cut -d. -f4 /tmp/tmpserverip)
{
if [ $a -ge 255 ]||[ $a -le 0 ]
then
echo a:error ip
else
echo 1 > /tmp/jack
fi
}
{
if [ $b -ge 255 ]||[ $b -lt 0 ]
then
echo b:error ip
else
echo 1 >>/tmp/jack
fi
}
{ if [ $c -ge 255 ]||[ $c -lt 0 ]
then
echo c:error ip
else
echo 1 >>/tmp/jack
fi
}
{ if [ $d -ge 255 ]||[ $d -le 0 ]
then
echo d:error ip
else
echo 1 >> /tmp/jack
fi
}
篇7:python正则表达式re模块详解
最近更 新
python实现代理服务功能实例
python网络编程学习笔记(九):数据库客户
python在linux中输出带颜色的文字的方法
浅析python 内置字符串处理函数的使用方法
python实现DNS正向查询、反向查询的例子
python代码检查工具pylint 让你的python更
python中getattr函数使用方法 getattr实现
python fabric实现远程操作和部署示例
python计算程序开始到程序结束的运行时间
python不带重复的全排列代码
热 点 排 行
Python入门教程 超详细1小时学会
python 中文乱码问题深入分析
比较详细Python正则表达式操作指
Python字符串的encode与decode研
Python open读写文件实现脚本
Python enumerate遍历数组示例应
Python 深入理解yield
Python+Django在windows下的开发
python 字符串split的用法分享
python 文件和路径操作函数小结
篇8:Linux编程之将PHP作为Shell脚本使用
我们都知道,PHP是一种非常好的动态网页开发语言(速度飞快,开发周期短……),但是只有很少数的人意识到PHP也可以很好的作为编写Shell脚本的语言,当PHP作为编写Shell脚本的语言时,他并没有Perl或者Bash那么强大,但是他却有着很好的优势,特别是对于我这种熟悉PHP但是不怎么熟悉Perl的人。
要使用PHP作为Shell脚本语言,你必须将PHP作为二进制的CGI编译,而不是Apache模式;编译成为二进制CGI模式运行的PHP有一些安全性的问题。一开始你可能会对于编写Shell脚本感到不适应,但是会慢慢好起来的:将PHP作为一般的动态网页编写语言和作为Shell脚本语言的唯一不同就在于一个Shell脚本需要在第一行生命解释本脚本的程序路径。
我们在PHP执行文件后面加入了参数“-1”,这样子PHP就不会输出HTTPHeader(如果仍需要作为Web的动态网页,那么你需要自己使用header函数输出HTTPHeader)。当然,在Shell脚本的里面你还是需要使用PHP的开始和结束标记:
现在让我们看一个例子,以便于更好的了解用PHP作为Shell脚本语言的使用:
print(“Hello, world!n”);
?>
上面这个程序会简单的输出“Hello, world!”到显示器上。
一、传递Shell脚本运行参数给PHP:
作为一个Shell脚本,经常会在运行程序时候加入一些参数,PHP作为Shell脚本时有一个内嵌的数组“$argv”,使用“$argv”数组可以很方便的读取Shell脚本运行时候的参数(“$argv[1]”对应的是第一个参数,“$argv[2]”对应的是第二个参数,依此类推)。比如下面这个程序:
#!/usr/local/bin/php -q
$first_name = $argv[1];
$last_name = $argv[2];
printf(“Hello, %s %s! How are you today?n”, $first_name, $last_name);
?>
上面的代码在运行的时候需要两个参数,分别是姓和名,比如这样子运行:
[dbrogdon@artemis dbrogdon]$ scriptname.ph Darrell Brogdon
Shell脚本在显示器上面会输出:
Hello, Darrell Brogdon! How are you today?
[dbrogdon@artemis dbrogdon]$
在PHP作为动态网页编写语言的时候也含有“$argv”这个数组,不过和这里有一些不同:当PHP作为Shell脚本语言的时候“$argv[0]”对应的是脚本的文件名,而当用于动态网页编写的时候,“$argv[1]”对应的是QueryString的第一个参数。
二、编写一个具有交互式的Shell脚本:
如果一个Shell脚本仅仅是自己运行,失去了交互性,那么也没有什么意思了。当PHP用于Shell脚本的编写的时候,怎么读取用户输入的信息呢?很不幸的是PHP自身没有读取用户输入信息的函数或者方法,但是我们可以效仿其他语言编写一个读取用户输入信息的函数:
“read”:
function read {
$fp = fopen('/dev/stdin', 'r');
$input = fgets($fp, 255);
fclose($fp);
return $input;
}
?>
需要注意的是上面这个函数只能用于Unix系统(其他系统需要作相应的改变),
上面的函数会打开一个文件指针,然后读取一个不超过255字节的行(就是fgets的作用),然后会关闭文件指针,返回读取的信息。
现在我们可以使用函数“read”将我们前面编写的程序1修改一下,使他更加具有“交互性”了:
function read() {
$fp = fopen('/dev/stdin', 'r');
$input = fgets($fp, 255);
fclose($fp);
return $input;
}
print(“What is your first name? ”);
$first_name = read();
print(“What is your last name? ”);
$last_name = read();
print(“nHello, $first_name $last_name! Nice to meet you!n”);
?>
将上面的程序保存下来,运行一下,你可能会看到一件预料之外的事情:最后一行的输入变成了三行!这是因为“read”函数返回的信息还包括了用户每一行的结尾换行符“n”,保留到了姓和名中,要去掉结尾的换行符,需要把“read”函数修改一下:
function read() {
$fp = fopen('/dev/stdin', 'r');
$input = fgets($fp, 255);
fclose($fp);
$input = chop($input); // 去除尾部空白
return $input;
}
?>
三、在其他语言编写的Shell脚本中包含PHP编写的Shell脚本:
有时候我们可能需要在其他语言编写的Shell脚本中包含PHP编写的Shell脚本。其实非常简单,下面是一个简单的例子:
echo This is the Bash section of the code.
/usr/local/bin/php -q << EOF
print(“This is the PHP section of the coden”);
?>
EOF
其实就是调用PHP来解析下面的代码,然后输出;那么,再试试下面的代码:
echo This is the Bash section of the code.
/usr/local/bin/php -q << EOF
$myVar = 'PHP';
print(“This is the $myVar section of the coden”);
?>
EOF
可以看出两次的代码唯一的不同就是第二次使用了一个变量“$myVar”,试试运行,PHP竟然给出出错的信息:“Parse error: parse error in - on line 2”!这是因为Bash中的变量也是“$myVar”,而Bash解析器先将变量给替换掉了,要想解决这个问题,你需要在每个PHP的变量前面加上“”转义符,那么刚才的代码修改如下:
echo This is the Bash section of the code.
/usr/local/bin/php -q << EOF
$myVar = 'PHP';
print(“This is the $myVar section of the coden”);
?>
EOF
篇9:ADO.NET编程之基础知识
ADO.NET是专门为帮助开发人员建立在Intranet或Internet上使用的高效多层数据库应用程序而设计的,它作为Windows平台下开发应用系统的数据访问技术,已经在企业信息系统的开发中得到了广泛的应用,
一.ADO.NET基础
程序和数据库交互,要通过ADO.NET进行;通过ADO.NET就能在数据库中执行SQL了。ADO.NET中提供了对不同数据库的统一操作接口(ODBC)。另外还有一种操作数据库的接口是JDBC。
ADO.NET中通过SqlConnection类创建到SQL Server的链接,Sqlconnection代表一个数据库的链接,ADO.NET中的链接等资源都实现了IDisposable接口。
实现了IDisposable接口的对象,在使用完了,要进行资源的释放。调用Dispose方法。
连接方式访问数据库,连接要打开,使用完要关闭。关闭之后,还可以再打开,这就是Close和Dispose的区别。Dispose(),是完全释放资源。
现在来测试一下,连接数据库是否成功了。
using System;using System.Collections.Generic;using System.Linq;using System.Text;using System.Threading.Tasks;//使用ADO.NET技术,必须要导入这两个命名空间using System.Data;using System.Data.SqlClient;//namespace ADO.NET1{ class Program { static void Main(string[] args) {//创建数据库连接(连接字符串)string sqlCon = “server=.;database=DB_MyStudentLife;uid=sa;pwd=Password_1”;SqlConnection scon = new SqlConnection(sqlCon);//打开数据库连接 scon.Open();//测试,判断数据库的连接状态if (scon.State == ConnectionState.Closed){ Console.WriteLine(“连接数据库失败”);}if (scon.State == ConnectionState.Open){ Console.WriteLine(“成功打开连接数据库,连接成功”);}Console.ReadKey(); } }}
测试效果图:
最后我们来看看SqlConnection对象的内部原理吧:使用反编译工具reflector就可以;
1.从图中,我们看出来,SqlConnection继承了DbConnection类,实现了ICloneable接口
2.ICloneable接口里面是啥样子呢,我们来看一下,反编译:
3.接下来,我们看下Dbconnection类:
从图中可以看出,Dbconnection类是一个抽象类,继承了Component类,实现了IDbConnection接口,和IDisposable接口,下面我们分别来看看这几个
4.Component类:
看得出来这个类,也实现了IDisposable接口哦,
好了,其他还是来看看IDisposable接口内部的神秘面纱吧:
看得出来,IDisosable定义了一个Dispose(),抽象无返回值的方法,
以上所述就是本文的全部内容了,希望大家能够喜欢。
篇10:GO语言并发编程之互斥锁、读写锁详解
这篇文章主要介绍了GO语言并发编程之互斥锁、读写锁详解,本文是GO并发编程实战一书的样章,详细讲解了互斥锁、读写锁,然后给出了一个完整示例,需要的朋友可以参考下
在本节,我们对Go语言所提供的与锁有关的API进行说明,这包括了互斥锁和读写锁。我们在第6章描述过互斥锁,但却没有提到过读写锁。这两种锁对于传统的并发程序来说都是非常常用和重要的。
一、互斥锁
互斥锁是传统的并发程序对共享资源进行访问控制的主要手段。它由标准库代码包sync中的Mutex结构体类型代表。sync.Mutex类型(确切地说,是*sync.Mutex类型)只有两个公开方法――Lock和Unlock。顾名思义,前者被用于锁定当前的互斥量,而后者则被用来对当前的互斥量进行解锁。
类型sync.Mutex的零值表示了未被锁定的互斥量。也就是说,它是一个开箱即用的工具。我们只需对它进行简单声明就可以正常使用了,就像这样:
代码如下:
var mutex sync.Mutex
mutex.Lock()
在我们使用其他编程语言(比如C或Java)的锁类工具的时候,可能会犯的一个低级错误就是忘记及时解开已被锁住的锁,从而导致诸如流程执行异常、线程执行停滞甚至程序死锁等等一系列问题的发生。然而,在Go语言中,这个低级错误的发生几率极低。其主要原因是有defer语句的存在。
我们一般会在锁定互斥锁之后紧接着就用defer语句来保证该互斥锁的及时解锁。请看下面这个函数:
代码如下:
var mutex sync.Mutex
func write() {
mutex.Lock()
defer mutex.Unlock()
// 省略若干条语句
}
函数write中的这条defer语句保证了在该函数被执行结束之前互斥锁mutex一定会被解锁。这省去了我们在所有return语句之前以及异常发生之时重复的附加解锁操作的工作。在函数的内部执行流程相对复杂的情况下,这个工作量是不容忽视的,并且极易出现遗漏和导致错误。所以,这里的defer语句总是必要的。在Go语言中,这是很重要的一个惯用法。我们应该养成这种良好的习惯。
对于同一个互斥锁的锁定操作和解锁操作总是应该成对的出现。如果我们锁定了一个已被锁定的互斥锁,那么进行重复锁定操作的Goroutine将会被阻塞,直到该互斥锁回到解锁状态。请看下面的示例:
代码如下:
func repeatedlyLock() {
var mutex sync.Mutex
fmt.Println(“Lock the lock. (G0)”)
mutex.Lock()
fmt.Println(“The lock is locked. (G0)”)
for i := 1; i <= 3; i++ {
go func(i int) {
fmt.Printf(“Lock the lock. (G%d)n”, i)
mutex.Lock()
fmt.Printf(“The lock is locked. (G%d)n”, i)
}(i)
}
time.Sleep(time.Second)
fmt.Println(“Unlock the lock. (G0)”)
mutex.Unlock()
fmt.Println(“The lock is unlocked. (G0)”)
time.Sleep(time.Second)
}
我们把执行repeatedlyLock函数的Goroutine称为G0。而在repeatedlyLock函数中,我们又启用了3个Goroutine,并分别把它们命名为G1、G2和G3。可以看到,我们在启用这3个Goroutine之前就已经对互斥锁mutex进行了锁定,并且在这3个Goroutine将要执行的go函数的开始处也加入了对mutex的锁定操作。这样做的意义是模拟并发地对同一个互斥锁进行锁定的情形。当for语句被执行完毕之后,我们先让G0小睡1秒钟,以使运行时系统有充足的时间开始运行G1、G2和G3。在这之后,解锁mutex。为了能够让读者更加清晰地了解到repeatedlyLock函数被执行的情况,我们在这些锁定和解锁操作的前后加入了若干条打印语句,并在打印内容中添加了我们为这几个Goroutine起的名字。也由于这个原因,我们在repeatedlyLock函数的最后再次编写了一条“睡眠”语句,以此为可能出现的其他打印内容再等待一小会儿。
经过短暂的执行,标准输出上会出现如下内容:
代码如下:
Lock the lock. (G0)
The lock is locked. (G0)
Lock the lock. (G1)
Lock the lock. (G2)
Lock the lock. (G3)
Unlock the lock. (G0)
The lock is unlocked. (G0)
The lock is locked. (G1)
从这八行打印内容中,我们可以清楚的看出上述四个Goroutine的执行情况。首先,在repeatedlyLock函数被执行伊始,对互斥锁的第一次锁定操作便被进行并顺利地完成。这由第一行和第二行打印内容可以看出。而后,在repeatedlyLock函数中被启用的那三个Goroutine在G0的第一次“睡眠”期间开始被运行。当相应的go函数中的对互斥锁的锁定操作被进行的时候,它们都被阻塞住了。原因是该互斥锁已处于锁定状态了。这就是我们在这里只看到了三个连续的Lock the lock. (G)而没有立即看到The lock is locked. (G)的原因。随后,G0“睡醒”并解锁互斥锁。这使得正在被阻塞的G1、G2和G3都会有机会重新锁定该互斥锁。但是,只有一个Goroutine会成功。成功完成锁定操作的某一个Goroutine会继续执行在该操作之后的语句。而其他Goroutine将继续被阻塞,直到有新的机会到来。这也就是上述打印内容中的最后三行所表达的含义。显然,G1抢到了这次机会并成功锁定了那个互斥锁。
实际上,我们之所以能够通过使用互斥锁对共享资源的唯一性访问进行控制正是因为它的这一特性。这有效的对竞态条件进行了消除。
互斥锁的锁定操作的逆操作并不会引起任何Goroutine的阻塞。但是,它的进行有可能引发运行时恐慌。更确切的讲,当我们对一个已处于解锁状态的互斥锁进行解锁操作的时候,就会已发一个运行时恐慌。这种情况很可能会出现在相对复杂的流程之中――我们可能会在某个或多个分支中重复的加入针对同一个互斥锁的解锁操作。避免这种情况发生的最简单、有效的方式依然是使用defer语句。这样更容易保证解锁操作的唯一性。
虽然互斥锁可以被直接的在多个Goroutine之间共享,但是我们还是强烈建议把对同一个互斥锁的成对的锁定和解锁操作放在同一个层次的代码块中。例如,在同一个函数或方法中对某个互斥锁的进行锁定和解锁。又例如,把互斥锁作为某一个结构体类型中的字段,以便在该类型的多个方法中使用它。此外,我们还应该使代表互斥锁的变量的访问权限尽量的低。这样才能尽量避免它在不相关的流程中被误用,从而导致程序不正确的行为。
互斥锁是我们见到过的众多同步工具中最简单的一个。只要遵循前面提及的几个小技巧,我们就可以以正确、高效的方式使用互斥锁,并用它来确保对共享资源的访问的唯一性。下面我们来看看稍微复杂一些的锁实现――读写锁。
二、读写锁
读写锁即是针对于读写操作的互斥锁。它与普通的互斥锁最大的不同就是,它可以分别针对读操作和写操作进行锁定和解锁操作。读写锁遵循的访问控制规则与互斥锁有所不同。在读写锁管辖的范围内,它允许任意个读操作的同时进行。但是,在同一时刻,它只允许有一个写操作在进行。并且,在某一个写操作被进行的过程中,读操作的进行也是不被允许的。也就是说,读写锁控制下的多个写操作之间都是互斥的,并且写操作与读操作之间也都是互斥的。但是,多个读操作之间却不存在互斥关系。
这样的规则对于针对同一块数据的并发读写来讲是非常贴切的。因为,无论读操作的并发量有多少,这些操作都不会对数据本身造成变更。而写操作不但会对同时进行的其他写操作进行干扰,还有可能造成同时进行的读操作的结果的不正确。例如,在32位的操作系统中,针对int64类型值的读操作和写操作都不可能只由一个CPU指令完成。在一个写操作被进行的过程当中,针对同一个只的读操作可能会读取到未被修改完成的值。该值既不与旧的值相等,也不等于新的值。这种错误往往不易被发现,且很难被修正。因此,在这样的场景下,读写锁可以在大大降低因使用锁而对程序性能造成的损耗的情况下完成对共享资源的访问控制。
在Go语言中,读写锁由结构体类型sync.RWMutex代表。与互斥锁类似,sync.RWMutex类型的零值就已经是立即可用的读写锁了。在此类型的方法集合中包含了两对方法,即:
代码如下:
func (*RWMutex) Lock
func (*RWMutex) Unlock
和
代码如下:
func (*RWMutex) RLock
func (*RWMutex) RUnlock
前一对方法的名称和签名与互斥锁的那两个方法完全一致。它们分别代表了对写操作的锁定和解锁。以下简称它们为写锁定和写解锁。而后一对方法则分别表示了对读操作的锁定和解锁。以下简称它们为读锁定和读解锁。
对已被写锁定的读写锁进行写锁定,会造成当前Goroutine的阻塞,直到该读写锁被写解锁。当然,如果有多个Goroutine因此而被阻塞,那么当对应的写解锁被进行之时只会使其中一个Goroutine的运行被恢复。类似的,对一个已被写锁定的读写锁进行读锁定,也会阻塞相应的Goroutine。但不同的是,一旦该读写锁被写解锁,那么所有因欲进行读锁定而被阻塞的Goroutine的运行都会被恢复。另一方面,如果在进行过程中发现当前的读写锁已被读锁定,那么这个写锁定操作将会等待直至所有施加于该读写锁之上的读锁定都被清除。同样的,在有多个写锁定操作为此而等待的情况下,相应的读锁定的全部清除只能让其中的某一个写锁定操作获得进行的机会。
现在来关注写解锁和读解锁。如果对一个未被写锁定的读写锁进行写解锁,那么会引发一个运行时恐慌。类似的,当对一个未被读锁定的读写锁进行读解锁的时候也会引发一个运行时恐慌。写解锁在进行的同时会试图唤醒所有因进行读锁定而被阻塞的Goroutine。而读解锁在进行的时候则会试图唤醒一个因进行写锁定而被阻塞的Goroutine。
无论锁定针对的是写操作还是读操作,我们都应该尽量及时的对相应的锁进行解锁。对于写解锁,我们自不必多说。而读解锁的及时进行往往更容易被我们忽视。虽说读解锁的进行并不会对其他正在进行中的读操作产生任何影响,但它却与相应的写锁定的进行关系紧密。注意,对于同一个读写锁来说,施加在它之上的读锁定可以有多个。因此,只有我们对互斥锁进行相同数量的读解锁,才能够让某一个相应的写锁定获得进行的机会。否则,后者会继续使进行它的Goroutine处于阻塞状态。由于sync.RWMutex和*sync.RWMutex类型都没有相应的方法让我们获得已进行的读锁定的数量,所以这里是很容易出现问题的。还好我们可以使用defer语句来尽量避免此类问题的发生。请记住,针对同一个读写锁的写锁定和读锁定是互斥的。无论是写解锁还是读解锁,操作的不及时都会对使用该读写锁的流程的正常执行产生负面影响。
除了我们在前面详细讲解的那两对方法之外,*sync.RWMutex类型还拥有另外一个方法――RLocker。这个RLocker方法会返回一个实现了sync.Locker接口的值。sync.Locker接口类型包含了两个方法,即:Lock和Unlock。细心的读者可能会发现,*sync.Mutex类型和*sync.RWMutex类型都是该接口类型的实现类型。实际上,我们在调用*sync.RWMutex类型值的RLocker方法之后所得到的结果值就是这个值本身。只不过,这个结果值的Lock方法和Unlock方法分别对应了针对该读写锁的读锁定操作和读解锁操作。换句话说,我们在对一个读写锁的RLocker方法的结果值的Lock方法或Unlock方法进行调用的时候实际上是在调用该读写锁的RLock方法或RUnlock方法。这样的操作适配在实现上并不困难。我们自己也可以很容易的编写出这些方法的实现。通过读写锁的RLocker方法获得这样一个结果值的实际意义在于,我们可以在之后以相同的方式对该读写锁中的“写锁”和“读锁”进行操作。这为相关操作的灵活适配和替换提供了方便。
三、锁的完整示例
我们下面来看一个与上述锁实现有关的示例。在Go语言的标准库代码包os中有一个名为File的结构体类型。os.File类型的值可以被用来代表文件系统中的某一个文件或目录。它的方法集合中包含了很多方法,其中的一些方法被用来对相应的文件进行写操作和读操作。
假设,我们需要创建一个文件来存放数据。在同一个时刻,可能会有多个Goroutine分别进行对此文件的进行写操作和读操作。每一次写操作都应该向这个文件写入若干个字节的数据。这若干字节的数据应该作为一个独立的数据块存在。这就意味着,写操作之间不能彼此干扰,写入的内容之间也不能出现穿插和混淆的情况。另一方面,每一次读操作都应该从这个文件中读取一个独立、完整的数据块。它们读取的数据块不能重复,且需要按顺序读取。例如,第一个读操作读取了数据块1,那么第二个读操作就应该去读取数据块2,而第三个读操作则应该读取数据块3,以此类推。对于这些读操作是否可以被同时进行,这里并不做要求。即使它们被同时进行,程序也应该分辨出它们的先后顺序。
为了突出重点,我们规定每个数据块的长度都是相同的。该长度应该在初始化的时候被给定。若写操作实际欲写入数据的长度超过了该值,则超出部分将会被截掉。
当我们拿到这样一个需求的时候,首先应该想到使用os.File类型。它为我们操作文件系统中的文件提供了底层的支持。但是,该类型的相关方法并没有对并发操作的安全性进行保证。换句话说,这些方法不是并发安全的。我只能通过额外的同步手段来保证这一点。鉴于这里需要分别对两类操作(即写操作和读操作)进行访问控制,所以读写锁在这里会比普通的互斥锁更加适用。不过,关于多个读操作要按顺序且不能重复读取的这个问题,我们需还要使用其他辅助手段来解决。
为了实现上述需求,我们需要创建一个类型。作为该类型的行为定义,我们先编写了一个这样的接口:
代码如下:
// 数据文件的接口类型。
type DataFile interface {
// 读取一个数据块。
Read() (rsn int64, d Data, err error)
// 写入一个数据块。
Write(d Data) (wsn int64, err error)
// 获取最后读取的数据块的序列号。
Rsn() int64
// 获取最后写入的数据块的序列号。
Wsn() int64
// 获取数据块的长度
DataLen() uint32
}
其中,类型Data被声明为一个[]byte的别名类型:
代码如下:
// 数据的类型
type Data []byte
而名称wsn和rsn分别是Writing Serial Number和Reading Serial Number的缩写形式,
它们分别代表了最后被写入的数据块的序列号和最后被读取的数据块的序列号。这里所说的序列号相当于一个计数值,它会从1开始。因此,我们可以通过调用Rsn方法和Wsn方法得到当前已被读取和写入的数据块的数量。
根据上面对需求的简单分析和这个DataFile接口类型声明,我们就可以来编写真正的实现了。我们将这个实现类型命名为myDataFile。它的基本结构如下:
代码如下:
// 数据文件的实现类型。
type myDataFile struct {
f *os.File // 文件。
fmutex sync.RWMutex // 被用于文件的读写锁。
woffset int64 // 写操作需要用到的偏移量。
roffset int64 // 读操作需要用到的偏移量。
wmutex sync.Mutex // 写操作需要用到的互斥锁。
rmutex sync.Mutex // 读操作需要用到的互斥锁。
dataLen uint32 // 数据块长度。
}
类型myDataFile共有七个字段。我们已经在前面说明过前两个字段存在的意义。由于对数据文件的写操作和读操作是各自独立的,所以我们需要两个字段来存储两类操作的进行进度。在这里,这个进度由偏移量代表。此后,我们把woffset字段称为写偏移量,而把roffset字段称为读偏移量。注意,我们在进行写操作和读操作的时候会分别增加这两个字段的值。当有多个写操作同时要增加woffset字段的值的时候就会产生竞态条件。因此,我们需要互斥锁wmutex来对其加以保护。类似的,rmutex互斥锁被用来消除多个读操作同时增加roffset字段的值时产生的竞态条件。最后,由上述的需求可知,数据块的长度应该是在初始化myDataFile类型值的时候被给定的。这个长度会被存储在该值的dataLen字段中。它与DataFile接口中声明的DataLen方法是对应的。下面我们就来看看被用来创建和初始化DataFile类型值的函数NewDataFile。
关于这类函数的编写,读者应该已经驾轻就熟了。NewDataFile函数会返回一个DataFile类型值,但是实际上它会创建并初始化一个*myDataFile类型的值并把它作为它的结果值。这样可以通过编译的原因是,后者会是前者的一个实现类型。NewDataFile函数的完整声明如下:
代码如下:
func NewDataFile(path string, dataLen uint32) (DataFile, error) {
f, err := os.Create(path)
if err != nil {
return nil, err
}
if dataLen == 0 {
return nil, errors.New(“Invalid data length!”)
}
df := &myDataFile{f: f, dataLen: dataLen}
return df, nil
}
可以看到,我们在创建*myDataFile类型值的时候只需要对其中的字段f和dataLen进行初始化。这是因为woffset字段和roffset字段的零值都是0,而在未进行过写操作和读操作的时候它们的值理应如此。对于字段fmutex、wmutex和rmutex来说,它们的零值即为可用的锁。所以我们也不必对它们进行显式的初始化。
把变量df的值作为NewDataFile函数的第一个结果值体现了我们的设计意图。但要想使*myDataFile类型真正成为DataFile类型的一个实现类型,我们还需要为*myDataFile类型编写出已在DataFile接口类型中声明的所有方法。其中最重要的当属Read方法和Write方法。
我们先来编写*myDataFile类型的Read方法。该方法应该按照如下步骤实现。
(1) 获取并更新读偏移量。
(2) 根据读偏移量从文件中读取一块数据。
(3) 把该数据块封装成一个Data类型值并将其作为结果值返回。
其中,前一个步骤在被执行的时候应该由互斥锁rmutex保护起来。因为,我们要求多个读操作不能读取同一个数据块,并且它们应该按顺序的读取文件中的数据块。而第二个步骤,我们也会用读写锁fmutex加以保护。下面是这个Read方法的第一个版本:
代码如下:
func (df *myDataFile) Read() (rsn int64, d Data, err error) {
// 读取并更新读偏移量
var offset int64
df.rmutex.Lock()
ffset = df.roffset
df.roffset += int64(df.dataLen)
df.rmutex.Unlock()
//读取一个数据块
rsn = offset / int64(df.dataLen)
df.fmutex.RLock()
defer df.fmutex.RUnlock()
bytes := make([]byte, df.dataLen)
_, err = df.f.ReadAt(bytes, offset)
if err != nil {
return
}
d = bytes
return
}
可以看到,在读取并更新读偏移量的时候,我们用到了rmutex字段。这保证了可能同时运行在多个Goroutine中的这两行代码:
代码如下:
ffset = df.roffset
df.roffset += int64(df.dataLen)
的执行是互斥的。这是我们为了获取到不重复且正确的读偏移量所必需采取的措施。
另一方面,在读取一个数据块的时候,我们适时的进行了fmutex字段的读锁定和读解锁操作。fmutex字段的这两个操作可以保证我们在这里读取到的是完整的数据块。不过,这个完整的数据块却并不一定是正确的。为什么会这样说呢?
请想象这样一个场景。在我们的程序中,有3个Goroutine来并发的执行某个*myDataFile类型值的Read方法,并有2个Goroutine来并发的执行该值的Write方法。通过前3个Goroutine的运行,数据文件中的数据块被依次的读取了出来。但是,由于进行写操作的Goroutine比进行读操作的Goroutine少,所以过不了多久读偏移量roffset的值就会等于甚至大于写偏移量woffset的值。也就是说,读操作很快就会没有数据可读了。这种情况会使上面的df.f.ReadAt方法返回的第二个结果值为代表错误的非nil且会与io.EOF相等的值。实际上,我们不应该把这样的值看成错误的代表,而应该把它看成一种边界情况。但不幸的是,我们在这个版本的Read方法中并没有对这种边界情况做出正确的处理。该方法在遇到这种情况时会直接把错误值返回给它的调用方。该调用方会得到读取出错的数据块的序列号,但却无法再次尝试读取这个数据块。由于其他正在或后续执行的Read方法会继续增加读偏移量roffset的值,所以当该调用方再次调用这个Read方法的时候只可能读取到在此数据块后面的其他数据块。注意,执行Read方法时遇到上述情况的次数越多,被漏读的数据块也就会越多。为了解决这个问题,我们编写了Read方法的第二个版本:
代码如下:
func (df *myDataFile) Read() (rsn int64, d Data, err error) {
// 读取并更新读偏移量
// 省略若干条语句
//读取一个数据块
rsn = offset / int64(df.dataLen)
bytes := make([]byte, df.dataLen)
for {
df.fmutex.RLock()
_, err = df.f.ReadAt(bytes, offset)
if err != nil {
if err == io.EOF {
df.fmutex.RUnlock()
continue
}
df.fmutex.RUnlock()
return
}
d = bytes
df.fmutex.RUnlock()
return
}
}
在上面的Read方法展示中,我们省略了若干条语句。原因在这个位置上的那些语句并没有任何变化。为了进一步节省篇幅,我们在后面也会遵循这样的省略原则。
第二个版本的Read方法使用for语句是为了达到这样一个目的:在其中的df.f.ReadAt方法返回io.EOF错误的时候继续尝试获取同一个数据块,直到获取成功为止。注意,如果在该for代码块被执行期间一直让读写锁fmutex处于读锁定状态,那么针对它的写锁定操作将永远不会成功,且相应的Goroutine也会被一直阻塞。因为它们是互斥的。所以,我们不得不在该for语句块中的每条return语句和continue语句的前面都加入一个针对该读写锁的读解锁操作,并在每次迭代开始时都对fmutex进行一次读锁定。显然,这样的代码看起来很丑陋。冗余的代码会使代码的维护成本和出错几率大大增加。并且,当for代码块中的代码引发了运行时恐慌的时候,我们是很难及时的对读写锁fmutex进行读解锁的。即便可以这样做,那也会使Read方法的实现更加丑陋。我们因为要处理一种边界情况而去掉了defer df.fmutex.RUnlock()语句。这种做法利弊参半。
其实,我们可以做得更好。但是这涉及到了其他同步工具。因此,我们以后再来对Read方法进行进一步的改造。顺便提一句,当df.f.ReadAt方法返回一个非nil且不等于io.EOF的错误值的时候,我们总是应该放弃再次获取目标数据块的尝试而立即将该错误值返回给Read方法的调用方。因为这样的错误很可能是严重的(比如,f字段代表的文件被删除了),需要交由上层程序去处理。
现在,我们来考虑*myDataFile类型的Write方法。与Read方法相比,Write方法的实现会简单一些。因为后者不会涉及到边界情况。在该方法中,我们需要进行两个步骤,即:获取并更新写偏移量和向文件写入一个数据块。我们直接给出Write方法的实现:
代码如下:
func (df *myDataFile) Write(d Data) (wsn int64, err error) {
// 读取并更新写偏移量
var offset int64
df.wmutex.Lock()
ffset = df.woffset
df.woffset += int64(df.dataLen)
df.wmutex.Unlock()
//写入一个数据块
wsn = offset / int64(df.dataLen)
var bytes []byte
if len(d) > int(df.dataLen) {
bytes = d[0:df.dataLen]
} else {
bytes = d
}
df.fmutex.Lock()
df.fmutex.Unlock()
_, err = df.f.Write(bytes)
return
}
这里需要注意的是,当参数d的值的长度大于数据块的最大长度的时候,我们会先进行截短处理再将数据写入文件。如果没有这个截短处理,我们在后面计算的已读数据块的序列号和已写数据块的序列号就会不正确。
有了编写前面两个方法的经验,我们可以很容易的编写出*myDataFile类型的Rsn方法和Wsn方法:
代码如下:
func (df *myDataFile) Rsn() int64 {
df.rmutex.Lock()
defer df.rmutex.Unlock()
return df.roffset / int64(df.dataLen)
}
func (df *myDataFile) Wsn() int64 {
df.wmutex.Lock()
defer df.wmutex.Unlock()
return df.woffset / int64(df.dataLen)
}
这两个方法的实现分别涉及到了对互斥锁rmutex和wmutex的锁定操作。同时,我们也通过使用defer语句保证了对它们的及时解锁。在这里,我们对已读数据块的序列号rsn和已写数据块的序列号wsn的计算方法与前面示例中的方法是相同的。它们都是用相关的偏移量除以数据块长度后得到的商来作为相应的序列号(或者说计数)的值。
至于*myDataFile类型的DataLen方法的实现,我们无需呈现。它只是简单地将dataLen字段的值作为其结果值返回而已。
编写上面这个完整示例的主要目的是展示互斥锁和读写锁在实际场景中的应用。由于还没有讲到Go语言提供的其他同步工具,所以我们在相关方法中所有需要同步的地方都是用锁来实现的。然而,其中的一些问题用锁来解决是不足够或不合适的。我们会在本节的后续部分中逐步的对它们进行改进。
从这两种锁的源码中可以看出,它们是同源的。读写锁的内部是用互斥锁来实现写锁定操作之间的互斥的。我们可以把读写锁看做是互斥锁的一种扩展。除此之外,这两种锁实现在内部都用到了操作系统提供的同步工具――信号灯。互斥锁内部使用一个二值信号灯(只有两个可能的值的信号灯)来实现锁定操作之间的互斥,而读写锁内部则使用一个二值信号灯和一个多值信号灯(可以有多个可能的值的信号灯)来实现写锁定操作与读锁定操作之间的互斥。当然,为了进行精确的协调,它们还使用到了其他一些字段和变量。由于篇幅原因,我们就不在这里赘述了。如果读者对此感兴趣的话,可以去阅读sync代码包中的相关源码文件。
【Shell编程之正则表达式详解(合集10篇)】相关文章:
电路系统调试总结2022-04-30
嵌入式实习总结2023-02-11
其他教案-键盘操作与练习2023-06-13
软件测试简历自我评价2023-09-01
嵌入式系统的实习报告2023-08-04
脚本范文2022-04-29
数据库工程师工作的职责2023-03-16
后门750字作文2022-09-01
高级运维工程师的职责2022-08-30
3DSMAX学习心得总结2023-12-30