分类 技术 下的文章

Go 是一个内置支持并发编程的语言。借助使用 go 关键字去创建 协程 goroutine (轻量级线程)和在 Go 中提供的 使用 信道其它的并发 同步方法,使得并发编程变得很容易、很灵活和很有趣。

另一方面,Go 并不会阻止一些因 Go 程序员粗心大意或者缺乏经验而造成的并发编程错误。在本文的下面部分将展示一些在 Go 编程中常见的并发编程错误,以帮助 Go 程序员们避免再犯类似的错误。

需要同步的时候没有同步

代码行或许 不是按出现的顺序运行的

在下面的程序中有两个错误。

  • 第一,在 main 协程中读取 b 和在新的 协程 中写入 b 可能导致数据争用。
  • 第二,条件 b == true 并不能保证在 main 协程 中的 a != nil。在新的协程中编译器和 CPU 可能会通过 重排序指令 进行优化,因此,在运行时 b 赋值可能发生在 a 赋值之前,在 main 协程 中当 a 被修改后,它将会让部分 a 一直保持为 nil
package main

import (
    "time"
    "runtime"
)

func main() {
    var a []int // nil
    var b bool  // false

    // a new goroutine
    go func () {
        a = make([]int, 3)
        b = true // write b
    }()

    for !b { // read b
        time.Sleep(time.Second)
        runtime.Gosched()
    }
    a[0], a[1], a[2] = 0, 1, 2 // might panic
}

上面的程序或者在一台计算机上运行的很好,但是在另一台上可能会引发异常。或者它可能运行了 N 次都很好,但是可能在第 (N+1) 次引发了异常。

我们将使用 sync 标准包中提供的信道或者同步方法去确保内存中的顺序。例如,

package main

func main() {
    var a []int = nil
    c := make(chan struct{})

    // a new goroutine
    go func () {
        a = make([]int, 3)
        c <- struct{}{}
    }()

    <-c
    a[0], a[1], a[2] = 0, 1, 2
}

使用 time.Sleep 调用去做同步

我们先来看一个简单的例子。

package main

import (
    "fmt"
    "time"
)

func main() {
    var x = 123

    go func() {
        x = 789 // write x
    }()

    time.Sleep(time.Second)
    fmt.Println(x) // read x
}

我们预期程序将打印出 789。如果我们运行它,通常情况下,它确定打印的是 789。但是,这个程序使用的同步方式好吗?No!原因是 Go 运行时并不保证 x 的写入一定会发生在 x 的读取之前。在某些条件下,比如在同一个操作系统上,大部分 CPU 资源被其它运行的程序所占用的情况下,写入 x 可能就会发生在读取 x 之后。这就是为什么我们在正式的项目中,从来不使用 time.Sleep 调用去实现同步的原因。

我们来看一下另外一个示例。

package main

import (
    "fmt"
    "time"
)

var x = 0

func main() {
    var num = 123
    var p = &num

    c := make(chan int)

    go func() {
        c <- *p + x
    }()

    time.Sleep(time.Second)
    num = 789
    fmt.Println(<-c)
}

你认为程序的预期输出是什么?123 还是 789?事实上它的输出与编译器有关。对于标准的 Go 编译器 1.10 来说,这个程序很有可能输出是 123。但是在理论上,它可能输出的是 789,或者其它的随机数。

现在,我们来改变 c <- *p + xc <- *p,然后再次运行这个程序。你将会发现输出变成了 789 (使用标准的 Go 编译器 1.10)。这再次说明它的输出是与编译器相关的。

是的,在上面的程序中存在数据争用。表达式 *p 可能会被先计算、后计算、或者在处理赋值语句 num = 789 时计算。time.Sleep 调用并不能保证 *p 发生在赋值语句处理之前进行。

对于这个特定的示例,我们将在新的协程创建之前,将值保存到一个临时值中,然后在新的协程中使用临时值去消除数据争用。

...
    tmp := *p + x
    go func() {
        c <- tmp
    }()
...

使协程挂起

挂起协程是指让协程一直处于阻塞状态。导致协程被挂起的原因很多。比如,

  • 一个协程尝试从一个 nil 信道中或者从一个没有其它协程给它发送值的信道中检索数据。
  • 一个协程尝试去发送一个值到 nil 信道,或者发送到一个没有其它的协程接收值的信道中。
  • 一个协程被它自己死锁。
  • 一组协程彼此死锁。
  • 当运行一个没有 default 分支的 select 代码块时,一个协程被阻塞,以及在 select 代码块中 case 关键字后的所有信道操作保持阻塞状态。

除了有时我们为了避免程序退出,特意让一个程序中的 main 协程保持挂起之外,大多数其它的协程挂起都是意外情况。Go 运行时很难判断一个协程到底是处于挂起状态还是临时阻塞。因此,Go 运行时并不会去释放一个挂起的协程所占用的资源。

谁先响应谁获胜 的信道使用案例中,如果使用的 future 信道容量不够大,当尝试向 Future 信道发送结果时,一些响应较慢的信道将被挂起。比如,如果调用下面的函数,将有 4 个协程处于永远阻塞状态。

func request() int {
    c := make(chan int)
    for i := 0; i < 5; i++ {
        i := i
        go func() {
            c <- i // 4 goroutines will hang here.
        }()
    }
    return <-c
}

为避免这 4 个协程一直处于挂起状态, c 信道的容量必须至少是 4

实现谁先响应谁获胜的第二种方法 的信道使用案例中,如果将 future 信道用做非缓冲信道,那么有可能这个信息将永远也不会有响应而挂起。例如,如果在一个协程中调用下面的函数,协程可能会挂起。原因是,如果接收操作 <-c 准备就绪之前,五个发送操作全部尝试发送,那么所有的尝试发送的操作将全部失败,因此那个调用者协程将永远也不会接收到值。

func request() int {
    c := make(chan int)
    for i := 0; i < 5; i++ {
        i := i
        go func() {
            select {
            case c <- i:
            default:
            }
        }()
    }
    return <-c
}

将信道 c 变成缓冲信道将保证五个发送操作中的至少一个操作会发送成功,这样,上面函数中的那个调用者协程将不会被挂起。

sync 标准包中拷贝类型值

在实践中,sync 标准包中的类型值不会被拷贝。我们应该只拷贝这个值的指针。

下面是一个错误的并发编程示例。在这个示例中,当调用 Counter.Value 方法时,将拷贝一个 Counter 接收值。作为接收值的一个字段,Counter 接收值的各个 Mutex 字段也会被拷贝。拷贝不是同步发生的,因此,拷贝的 Mutex 值可能会出错。即便是没有错误,拷贝的 Counter 接收值的访问保护也是没有意义的。

import "sync"

type Counter struct {
    sync.Mutex
    n int64
}

// This method is okay.
func (c *Counter) Increase(d int64) (r int64) {
    c.Lock()
    c.n += d
    r = c.n
    c.Unlock()
    return
}

// The method is bad. When it is called, a Counter
// receiver value will be copied.
func (c Counter) Value() (r int64) {
    c.Lock()
    r = c.n
    c.Unlock()
    return
}

我们只需要改变 Value 接收类型方法为指针类型 *Counter,就可以避免拷贝 Mutex 值。

在官方的 Go SDK 中提供的 go vet 命令将会报告潜在的错误值拷贝。

在错误的地方调用 sync.WaitGroup 的方法

每个 sync.WaitGroup 值维护一个内部计数器,这个计数器的初始值为 0。如果一个 WaitGroup 计数器的值是 0,调用 WaitGroup 值的 Wait 方法就不会被阻塞,否则,在计数器值为 0 之前,这个调用会一直被阻塞。

为了让 WaitGroup 值的使用有意义,当一个 WaitGroup 计数器值为 0 时,必须在相应的 WaitGroup 值的 Wait 方法调用之前,去调用 WaitGroup 值的 Add 方法。

例如,下面的程序中,在不正确位置调用了 Add 方法,这将使最后打印出的数字不总是 100。事实上,这个程序最后打印的数字可能是在 [0, 100) 范围内的一个随意数字。原因就是 Add 方法的调用并不保证一定会发生在 Wait 方法调用之前。

package main

import (
    "fmt"
    "sync"
    "sync/atomic"
)

func main() {
    var wg sync.WaitGroup
    var x int32 = 0
    for i := 0; i < 100; i++ {
        go func() {
            wg.Add(1)
            atomic.AddInt32(&x, 1)
            wg.Done()
        }()
    }

    fmt.Println("To wait ...")
    wg.Wait()
    fmt.Println(atomic.LoadInt32(&x))
}

为让程序的表现符合预期,在 for 循环中,我们将把 Add 方法的调用移动到创建的新协程的范围之外,修改后的代码如下。

...
    for i := 0; i < 100; i++ {
        wg.Add(1)
        go func() {
            atomic.AddInt32(&x, 1)
            wg.Done()
        }()
    }
...

不正确使用 futures 信道

信道使用案例 的文章中,我们知道一些函数将返回 futures 信道。假设 fafb 就是这样的两个函数,那么下面的调用就使用了不正确的 future 参数。

doSomethingWithFutureArguments(<-fa(), <-fb())

在上面的代码行中,两个信道接收操作是顺序进行的,而不是并发的。我们做如下修改使它变成并发操作。

ca, cb := fa(), fb()
doSomethingWithFutureArguments(<-c1, <-c2)

没有等协程的最后的活动的发送结束就关闭信道

Go 程序员经常犯的一个错误是,还有一些其它的协程可能会发送值到以前的信道时,这个信道就已经被关闭了。当这样的发送(发送到一个已经关闭的信道)真实发生时,将引发一个异常。

这种错误在一些以往的著名 Go 项目中也有发生,比如在 Kubernetes 项目中的 这个 bug这个 bug

如何安全和优雅地关闭信道,请阅读 这篇文章

在值上做 64 位原子操作时没有保证值地址 64 位对齐

到目前为止(Go 1.10),在标准的 Go 编译器中,在一个 64 位原子操作中涉及到的值的地址要求必须是 64 位对齐的。如果没有对齐则导致当前的协程异常。对于标准的 Go 编译器来说,这种失败仅发生在 32 位的架构上。请阅读 内存布局 去了解如何在一个 32 位操作系统上保证 64 位对齐。

没有注意到大量的资源被 time.After 函数调用占用

time 标准包中的 After 函数返回 一个延迟通知的信道。这个函数在某些情况下用起来很便捷,但是,每次调用它将创建一个 time.Timer 类型的新值。这个新创建的 Timer 值在通过传递参数到 After 函数指定期间保持激活状态,如果在这个期间过多的调用了该函数,可能会有太多的 Timer 值保持激活,这将占用大量的内存和计算资源。

例如,如果调用了下列的 longRunning 函数,将在一分钟内产生大量的消息,然后在某些周期内将有大量的 Timer 值保持激活,即便是大量的这些 Timer 值已经没用了也是如此。

import (
    "fmt"
    "time"
)

// The function will return if a message arrival interval
// is larger than one minute.
func longRunning(messages <-chan string) {
    for {
        select {
        case <-time.After(time.Minute):
            return
        case msg := <-messages:
            fmt.Println(msg)
        }
    }
}

为避免在上述代码中创建过多的 Timer 值,我们将使用一个单一的 Timer 值去完成同样的任务。

func longRunning(messages <-chan string) {
    timer := time.NewTimer(time.Minute)
    defer timer.Stop()

    for {
        select {
        case <-timer.C:
            return
        case msg := <-messages:
            fmt.Println(msg)
            if !timer.Stop() {
                <-timer.C
            }
        }

        // The above "if" block can also be put here.

        timer.Reset(time.Minute)
    }
}

不正确地使用 time.Timer

在最后,我们将展示一个符合语言使用习惯的 time.Timer 值的使用示例。需要注意的一个细节是,那个 Reset 方法总是在停止或者 time.Timer 值释放时被使用。

select 块的第一个 case 分支的结束部分,time.Timer 值被释放,因此,我们不需要去停止它。但是必须在第二个分支中停止定时器。如果在第二个分支中 if 代码块缺失,它可能至少在 Reset 方法调用时,会(通过 Go 运行时)发送到 timer.C 信道,并且那个 longRunning 函数可能会早于预期返回,对于 Reset 方法来说,它可能仅仅是重置内部定时器为 0,它将不会清理(耗尽)那个发送到 timer.C 信道的值。

例如,下面的程序很有可能在一秒内而不是十秒时退出。并且更重要的是,这个程序并不是 DRF 的(LCTT 译注:data race free,多线程程序的一种同步程度)。

package main

import (
    "fmt"
    "time"
)

func main() {
    start := time.Now()
    timer := time.NewTimer(time.Second/2)
    select {
    case <-timer.C:
    default:
        time.Sleep(time.Second) // go here
    }
    timer.Reset(time.Second * 10)
    <-timer.C
    fmt.Println(time.Since(start)) // 1.000188181s
}

time.Timer 的值不再被其它任何一个东西使用时,它的值可能被停留在一种非停止状态,但是,建议在结束时停止它。

在多个协程中如果不按建议使用 time.Timer 值并发,可能会有 bug 隐患。

我们不应该依赖一个 Reset 方法调用的返回值。Reset 方法返回值的存在仅仅是为了兼容性目的。


via: https://go101.org/article/concurrent-common-mistakes.html

作者:<go101.org> 译者:qhwdw 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

在这个 Git 入门系列的第三篇中,我们来学习一下如何添加和删除 Git 分支。

在本系列的前两篇文章中,我们开始使用 Git,学会如何克隆项目,修改、增加和删除内容。在这第三篇文章中,我将介绍 Git 分支,为何以及如何使用分支。

 title=

不妨用树来描绘 Git 仓库。图中的树有很多分支,或长或短,或从树干延伸或从其它分支延伸。在这里,我们用树干比作仓库的 master 分支,其中 master 代指 ”master 分支”,是 Git 仓库的中心分支或第一个分支。为简单起见,我们假设 master 是树干,其它分支都是从该分支分出的。

为何在 Git 仓库中使用分支

使用分支的主要理由为:

  • 如果你希望为项目增加新特性,但很可能会影响当前可正常工作的代码。对于该项目的活跃用户而言,这是很糟糕的事情。与其将特性加入到其它人正在使用的 master 分支,更好的方法是在仓库的其它分支中变更代码,下面会给出具体的工作方式。
  • 更重要的是,Git 其设计用于协作。如果所有人都在你代码仓库的 master 分支上操作,会引发很多混乱。对编程语言或项目的知识和阅历因人而异;有些人可能会编写有错误或缺陷的代码,也可能会编写你觉得不适合该项目的代码。使用分支可以让你核验他人的贡献并选择适合的加入到项目中。(这里假设你是代码库唯一的所有者,希望对增加到项目中的代码有完全的控制。在真实的项目中,代码库有多个具有合并代码权限的所有者)

创建分支

让我们回顾本系列上一篇文章,看一下在我们的 Demo 目录中分支是怎样的。如果你没有完成上述操作,请按照文章中的指示从 GitHub 克隆代码并进入 Demo 目录。运行如下命令:

pwd
git branch
ls -la

pwd 命令(是当前工作目录的英文缩写)返回当前你所处的目录(以便确认你在 Demo 目录中),git branch 列出该项目在你主机上的全部分支,ls -la 列出当前目录下的所有文件。你的终端输出类似于:

 title=

master 分支中,只有一个文件 README.md。(Git 会友好地忽略掉其它目录和文件。)

接下来,运行如下命令:

git status
git checkout -b myBranch
git status

第一条命令 git status 告知你当前位于 branch master,(就像在终端中看到的那样)它与 origin/master 处于同步状态,这意味着 master 分支的本地副本中的全部文件也出现在 GitHub 中。两份副本没有差异,所有的提交也是一致的。

下一条命令 git checkout -b myBranch 中的 -b 告知 Git 创建一个名为 myBranch 的新分支,然后 checkout 命令将我们切换到新创建的分支。运行第三条命令 git status 确保你已经位于刚创建的分支下。

如你所见,git status 告知你当前处于 myBranch 分支,没有变更需要提交。这是因为我们既没有增加新文件,也没有修改已有文件。

 title=

如果希望以可视化的方式查看分支,可以运行 gitk 命令。如果遇到报错 bash: gitk: command not found...,请先安装 gitk 软件包(找到你操作系统对应的安装文档,以获得安装方式)。

(LCTT 译注:需要在有 X 服务器的终端运行 gitk,否则会报错)

下图展示了我们在 Demo 项目中的所作所为:你最后一次提交(的对应信息)是 Delete file.txt,在此之前有三次提交。当前的提交用黄点标注,之前的提交用蓝点标注,黄点和 Delete file.txt 之间的三个方块展示每个分支所在的位置(或者说每个分支中的最后一次提交的位置)。由于 myBranch 刚创建,提交状态与 master 分支及其对应的记为 remotes/origin/master 的远程 master 分支保持一致。(非常感谢来自 Red Hat 的 Peter Savage 让我知道 gitk 这个工具)

 title=

下面让我们在 myBranch 分支下创建一个新文件并观察终端输出。运行如下命令:

echo "Creating a newFile on myBranch" > newFile
cat newFile
git status

第一条命令中的 echo 创建了名为 newFile 的文件,接着 cat newFile 打印出文件内容,最后 git status 告知你我们 myBranch 分支的当前状态。在下面的终端输出中,Git 告知 myBranch 分支下有一个名为 newFile 的文件当前处于 untracked 状态。这表明我们没有让 Git 追踪发生在文件 newFile 上的变更。

 title=

下一步是增加文件,提交变更并将 newFile 文件推送至 myBranch 分支(请回顾本系列上一篇文章获得更多细节)。

git add newFile
git commit -m "Adding newFile to myBranch"
git push origin myBranch

在上述命令中,push 命令使用的分支参数为 myBranch 而不是 master。Git 添加 newFile 并将变更推送到你 GitHub 账号下的 Demo 仓库中,告知你在 GitHub 上创建了一个与你本地副本分支 myBranch 一样的新分支。终端输出截图给出了运行命令的细节及命令输出。

 title=

当你访问 GitHub 时,在分支选择的下拉列表中可以发现两个可供选择的分支。

 title=

点击 myBranch 切换到 myBranch 分支,你可以看到在此分支上新增的文件。

 title=

截至目前,我们有两个分支:一个是 master 分支,只有一个 README.md 文件;另一个是 myBranch 分支,有两个文件。

你已经知道如何创建分支了,下面我们再创建一个分支。输入如下命令:

git checkout master
git checkout -b myBranch2
touch newFile2
git add newFile2
git commit -m "Adding newFile2 to myBranch2"
git push origin myBranch2

我不再给出终端输出,需要你自己尝试,但你可以在 GitHub 代码库 中验证你的结果。

删除分支

由于我们增加了两个分支,下面删除其中的一个(myBranch),包括两步:

  1. 删除本地分支 你不能删除正在操作的分支,故切换到 master 分支 (或其它你希望保留的分支),命令及终端输出如下:

git branch 可以列出可用的分支,使用 checkout 切换到 master 分支,然后使用 git branch -D myBranch 删除该分支。再次运行 git branch 检查是否只剩下两个分支(而不是三个)。

  1. 删除 GitHub 上的分支 使用如下命令删除 myBranch 的远程分支:
git push origin :myBranch

 title=

上面 push 命令中分支名称前面的冒号(:)告知 GitHub 删除分支。另一种写法为:

git push -d origin myBranch

其中 -d (也可以用 --delete) 也用于告知 GitHub 删除你的分支。

我们学习了 Git 分支的使用,在本系列的下一篇文章中,我们将介绍如何执行 fetchrebase 操作,对于多人同时的贡献的项目而言,这是很必须学会的。


via: https://opensource.com/article/18/5/git-branching

作者:Kedar Vijay Kulkarni 选题:lujun9972 译者:pinewall 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

将您的计算机变成一个多任务的动力室。

你是否有过这种感觉,你的主机运行速度没有预期的那么快?我也曾经有过这种感觉,直到我发现了 GNU Parallel。

GNU Parallel 是一个 shell 工具,可以并行执行任务。它可以解析多种输入,让你可以同时在多份数据上运行脚本或命令。你终于可以使用全部的 CPU 了!

如果你用过 xargs,上手 Parallel 几乎没有难度。如果没有用过,这篇教程会告诉你如何使用,同时给出一些其它的用例。

安装 GNU Parallel

GNU Parallel 很可能没有预装在你的 Linux 或 BSD 主机上,你可以从软件源中安装。以 Fedora 为例:

$ sudo dnf install parallel

对于 NetBSD:

# pkg_add parallel

如果各种方式都不成功,请参考项目主页

从串行到并行

正如其名称所示,Parallel 的强大之处是以并行方式执行任务;而我们中不少人平时仍然以串行方式运行任务。

当你对多个对象执行某个命令时,你实际上创建了一个任务队列。一部分对象可以被命令处理,剩余的对象需要等待,直到命令处理它们。这种方式是低效的。只要数据够多,总会形成任务队列;但与其只使用一个任务队列,为何不使用多个更小规模的任务队列呢?

假设你有一个图片目录,你希望将目录中的图片从 JEEG 格式转换为 PNG 格式。有多种方法可以完成这个任务。可以手动用 GIMP 打开每个图片,输出成新格式,但这基本是最差的选择,费时费力。

上述方法有一个漂亮且简洁的变种,即基于 shell 的方案:

$ convert 001.jpeg 001.png
$ convert 002.jpeg 002.png
$ convert 003.jpeg 003.png
... 略 ...

对于初学者而言,这是一个不小的转变,而且看起来是个不小的改进。不再需要图像界面和不断的鼠标点击,但仍然是费力的。

进一步改进:

$ for i in *jpeg; do convert $i $i.png ; done

至少,这一步设置好任务执行,让你节省时间去做更有价值的事情。但问题来了,这仍然是串行操作;一张图片转换完成后,队列中的下一张进行转换,依此类推直到全部完成。

使用 Parallel:

$ find . -name "*jpeg" | parallel -I% --max-args 1 convert % %.png

这是两条命令的组合:find 命令,用于收集需要操作的对象;parallel 命令,用于对象排序并确保每个对象按需处理。

  • find . -name "*jpeg" 查找当前目录下以 jpeg 结尾的所有文件。
  • parallel 调用 GNU Parallel。
  • -I% 创建了一个占位符 %,代表 find 传递给 Parallel 的内容。如果不使用占位符,你需要对 find 命令的每一个结果手动编写一个命令,而这恰恰是你想要避免的。
  • --max-args 1 给出 Parallel 从队列获取新对象的速率限制。考虑到 Parallel 运行的命令只需要一个文件输入,这里将速率限制设置为 1。假如你需要执行更复杂的命令,需要两个文件输入(例如 cat 001.txt 002.txt > new.txt),你需要将速率限制设置为 2。
  • convert % %.png 是你希望 Parallel 执行的命令。

组合命令的执行效果如下:find 命令收集所有相关的文件信息并传递给 parallel,后者(使用当前参数)启动一个任务,(无需等待任务完成)立即获取参数行中的下一个参数(LCTT 译注:管道输出的每一行对应 parallel 的一个参数,所有参数构成参数行);只要你的主机没有瘫痪,Parallel 会不断做这样的操作。旧任务完成后,Parallel 会为分配新任务,直到所有数据都处理完成。不使用 Parallel 完成任务大约需要 10 分钟,使用后仅需 3 至 5 分钟。

多个输入

只要你熟悉 findxargs (整体被称为 GNU 查找工具,或 findutils),find 命令是一个完美的 Parallel 数据提供者。它提供了灵活的接口,大多数 Linux 用户已经很习惯使用,即使对于初学者也很容易学习。

find 命令十分直截了当:你向 find 提供搜索路径和待查找文件的一部分信息。可以使用通配符完成模糊搜索;在下面的例子中,星号匹配任何字符,故 find 定位(文件名)以字符 searchterm 结尾的全部文件:

$ find /path/to/directory -name "*searchterm"

默认情况下,find 逐行返回搜索结果,每个结果对应 1 行:

$ find ~/graphics -name "*jpg"
/home/seth/graphics/001.jpg
/home/seth/graphics/cat.jpg
/home/seth/graphics/penguin.jpg
/home/seth/graphics/IMG_0135.jpg

当使用管道将 find 的结果传递给 parallel 时,每一行中的文件路径被视为 parallel 命令的一个参数。另一方面,如果你需要使用命令处理多个参数,你可以改变队列数据传递给 parallel 的方式。

下面先给出一个不那么实际的例子,后续会做一些修改使其更加有意义。如果你安装了 GNU Parallel,你可以跟着这个例子操作。

假设你有 4 个文件,按照每行一个文件的方式列出,具体如下:

$ echo ada > ada ; echo lovelace > lovelace
$ echo richard > richard ; echo stallman > stallman
$ ls -1
ada
lovelace
richard
stallman

你需要将两个文件合并成第三个文件,后者同时包含前两个文件的内容。这种情况下,Parallel 需要访问两个文件,使用 -I% 变量的方式不符合本例的预期。

Parallel 默认情况下读取 1 个队列对象:

$ ls -1 | parallel echo
ada
lovelace
richard
stallman

现在让 Parallel 每个任务使用 2 个队列对象:

$ ls -1 | parallel --max-args=2 echo
ada lovelace
richard stallman

现在,我们看到行已经并合并;具体而言,ls -1 的两个查询结果会被同时传送给 Parallel。传送给 Parallel 的参数涉及了任务所需的 2 个文件,但目前还只是 1 个有效参数:(对于两个任务分别为)“ada lovelace” 和 “richard stallman”。你真正需要的是每个任务对应 2 个独立的参数。

值得庆幸的是,Parallel 本身提供了上述所需的解析功能。如果你将 --max-args 设置为 2,那么 {1}{2} 这两个变量分别代表传入参数的第一和第二部分:

$ ls -1 | parallel --max-args=2 cat {1} {2} ">" {1}_{2}.person

在上面的命令中,变量 {1} 值为 adarichard (取决于你选取的任务),变量 {2} 值为 lovelacestallman。通过使用重定向符号(放到引号中,防止被 Bash 识别,以便 Parallel 使用),(两个)文件的内容被分别重定向至新文件 ada_lovelace.personrichard_stallman.person

$ ls -1
ada
ada_lovelace.person
lovelace
richard
richard_stallman.person
stallman

$ cat ada_*person
ada lovelace
$ cat ri*person
richard stallman

如果你整天处理大量几百 MB 大小的日志文件,那么(上述)并行处理文本的方法对你帮忙很大;否则,上述例子只是个用于上手的示例。

然而,这种处理方法对于很多文本处理之外的操作也有很大帮助。下面是来自电影产业的真实案例,其中需要将一个目录中的视频文件和(对应的)音频文件进行合并。

$ ls -1
12_LS_establishing-manor.avi
12_wildsound.flac
14_butler-dialogue-mixed.flac
14_MS_butler.avi
...略...

使用同样的方法,使用下面这个简单命令即可并行地合并文件:

$ ls -1 | parallel --max-args=2 ffmpeg -i {1} -i {2} -vcodec copy -acodec copy {1}.mkv

简单粗暴的方式

上述花哨的输入输出处理不一定对所有人的口味。如果你希望更直接一些,可以将一堆命令甩给 Parallel,然后去干些其它事情。

首先,需要创建一个文本文件,每行包含一个命令:

$ cat jobs2run
bzip2 oldstuff.tar
oggenc music.flac
opusenc ambiance.wav
convert bigfile.tiff small.jpeg
ffmepg -i foo.avi -v:b 12000k foo.mp4
xsltproc --output build/tmp.fo style/dm.xsl src/tmp.xml
bzip2 archive.tar

接着,将文件传递给 Parallel:

$ parallel --jobs 6 < jobs2run

现在文件中对应的全部任务都在被 Parallel 执行。如果任务数量超过允许的数目(LCTT 译注:应该是 --jobs 指定的数目或默认值),Parallel 会创建并维护一个队列,直到任务全部完成。

更多内容

GNU Parallel 是个强大而灵活的工具,还有很多很多用例无法在本文中讲述。工具的 man 页面提供很多非常酷的例子可供你参考,包括通过 SSH 远程执行和在 Parallel 命令中使用 Bash 函数等。YouTube 上甚至有一个系列,包含大量操作演示,让你可以直接从 GNU Parallel 团队学习。GNU Paralle 的主要维护者还发布了官方使用指导手册,可以从 Lulu.com 获取。

GNU Parallel 有可能改变你完成计算的方式;即使没有,也会至少改变你主机花在计算上的时间。马上上手试试吧!


via: https://opensource.com/article/18/5/gnu-parallel

作者:Seth Kenlon 选题:lujun9972 译者:pinewall 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

你的下一个 Python 项目需要一个模板引擎来自动生成 HTML 吗?这有几种选择。

在我的日常工作中,我花费大量的时间将各种来源的数据转化为可读的信息。虽然很多时候这只是电子表格或某种类型的图表或其他数据可视化的形式,但也有其他时候,将数据以书面形式呈现是有意义的。

但我的头疼地方就是复制和粘贴。如果你要将数据从源头移动到标准化模板,则不应该复制和粘贴。这很容易出错,说实话,这会浪费你的时间。

因此,对于我定期发送的任何遵循一个共同的模式的信息,我倾向于找到某种方法来自动化至少一部分信息。也许这涉及到在电子表格中创建一些公式,一个快速 shell 脚本或其他解决方案,以便使用从外部源提取的信息自动填充模板。

但最近,我一直在探索 Python 模板来完成从其他数据集创建报告和图表的大部分工作。

Python 模板引擎非常强大。我的简化报告创建的使用案例仅仅触及了它的皮毛。许多开发人员正在利用这些工具来构建完整的 web 应用程序和内容管理系统。但是,你并不需要有一个复杂的 web 应用程序才能使用 Python 模板工具。

为什么选择模板?

每个模板工具都不甚相同,你应该阅读文档以了解其确切的用法。但让我们创建一个假设的例子。假设我想创建一个简短的页面,列出我最近编写的所有 Python 主题。就像这样:

<html>
  <head>
    <title>My Python articles</title>
  </head>
  <body>

    <p>These are some of the things I have written about Python:</p>
    <ul>
      <li>Python GUIs</li>
      <li>Python IDEs</li>
      <li>Python web scrapers</li>
    </ul>

  </body>
</html>

当它仅仅是这三个项目时,维护它是很简单的。但是当我想添加第四个、第五个或第六十七个时会发生什么?我可以从包含我所有页面列表的 CSV 文件或其他数据文件生成它,而不是手动编码此页面吗?我可以轻松地为我写的每个主题创建重复内容吗?我可以以编程方式更改每个页面上的文本标题吗?这就是模板引擎可以发挥作用的地方。

有许多不同的选择,今天我将与你其中分享三个,顺序不分先后:MakoJinja2Genshi

Mako

Mako 是以 MIT 许可证发布的 Python 模板工具,专为快速展现而设计的(与 Jinja2 不同)。Reddit 已经使用 Mako 来展现他们的网页,它同时也是 Pyramid 和 Pylons 等 web 框架的默认模板语言。它相当简单且易于使用。你可以使用几行代码来设计模板;支持 Python 2.x 和 3.x,它是一个功能强大且功能丰富的工具,具有良好的文档,这一点我认为是必须的。其功能包括过滤器、继承、可调用块和内置缓存系统,这些系统可以被大型或复杂的 web 项目导入。

Jinja2

Jinja2 是另一个快速且功能全面的选项,可用于 Python 2.x 和 3.x,遵循 BSD 许可证。Jinja2 从功能角度与 Mako 有很多重叠,因此对于新手来说,你在两者之间的选择可能会归结为你喜欢的格式化风格。Jinja2 还将模板编译为字节码,并具有 HTML 转义、沙盒、模板继承和模板沙盒部分的功能。其用户包括 Mozilla、 SourceForge、 NPR、 Instagram 等,并且还具有强大的文档。与 Mako 在模板内部使用 Python 逻辑不同的是,Jinja2 使用自己的语法。

Genshi

Genshi 是我会提到的第三个选择。它是一个 XML 工具,具有强大的模板组件,所以如果你使用的数据已经是 XML 格式,或者你需要使用网页以外的格式,Genshi 可能成为你的一个很好的解决方案。HTML 基本上是一种 XML(好吧,不是精确的,但这超出了本文的范围,有点卖弄学问了),因此格式化它们非常相似。由于我通常使用的很多数据都是 XML 或其他类型的数据,因此我非常喜欢使用我可以用于多种事物的工具。

发行版目前仅支持 Python 2.x,尽管 Python 3 支持存在于主干中,但我提醒你,它看起来并没有得到有效的开发。Genshi 遵循 BSD 许可证提供。

示例

因此,在上面的假设示例中,我不会每次写新主题时都更新 HTML 文件,而是通过编程方式对其进行更新。我可以创建一个模板,如下所示:

<html>
  <head>
    <title>My Python articles</title>
  </head>
  <body>

    <p>These are some of the things I have written about Python:</p>
    <ul>
      %for topic in topics:
      <li>${topic}</li>
      %endfor
    </ul>

  </body>
</html>

然后我可以使用我的模板库来迭代每个主题,比如使用 Mako,像这样:

from mako.template import Template

mytemplate = Template(filename='template.txt')
print(mytemplate.render(topics=("Python GUIs","Python IDEs","Python web scrapers")))

当然,在现实世界的用法中,我不会将这些内容手动地列在变量中,而是将它们从外部数据源(如数据库或 API)中提取出来。

这些不是仅有的 Python 模板引擎。如果你正在开始创建一个将大量使用模板的新项目,那么你考虑的可能不仅仅是这三种选择。在 Python 维基上查看更全面的列表,以获得更多值得考虑的项目。


via: https://opensource.com/resources/python/template-libraries

作者:Jason Baker 选题:lujun9972 译者:MjSeven 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

在 Linux 中有几种使用命令行或图形界面终止一个程序的方式。

进程出错的时候,您可能会想要中止或是杀掉这个进程。在本文中,我们将探索在命令行和图形界面中终止进程或是应用程序,这里我们使用 gedit 作为样例程序。

使用命令行或字符终端界面

Ctrl + C

在命令行中调用 gedit (如果您没有使用 gedit & 命令)程序的一个问题是 shell 会话被阻塞,没法释放命令行提示符。在这种情况下,Ctrl + CCtrlC 的组合键) 会很管用。这会终止 gedit ,并且所有的工作都将丢失(除非文件已经被保存)。Ctrl + C 会给 gedit 发送了 SIGINT 信号。这是一个默认终止进程的停止信号,它将指示 shell 停止 gedit 的运行,并返回到主函数的循环中,您将返回到提示符。

$ gedit
^C

Ctrl + Z

它被称为挂起字符。它会发送 SIGTSTP 信号给进程。它也是一个停止信号,但是默认行为不是杀死进程,而是挂起进程。

下面的命令将会停止(杀死/中断) gedit 的运行,并返回到 shell 提示符。

$ gedit
^Z
[1]+  Stopped                 gedit
$

一旦进程被挂起(以 gedit 为例),将不能在 gedit 中写入或做任何事情。而在后台,该进程变成了一个作业,可以使用 jobs 命令验证。

$ jobs
[1]+  Stopped                 gedit

jobs 允许您在单个 shell 会话中控制多个进程。您可以终止,恢复作业,或是根据需要将作业移动到前台或是后台。

让我们在后台恢复 gedit,释放提示符以运行其它命令。您可以通过 bg 命令来做到,后跟作业 ID(注意上面的 jobs 命令显示出来的 [1],这就是作业 ID)。

$ bg 1
[1]+ gedit &amp;

这和直接使用 gedit & 启动程序效果差不多:

$ gedit &amp;

使用 kill

kill 命令提供信号的精确控制,允许您通过指定信号名或是信号数字为进程发送信号,后跟进程 ID 或是 PID。

我喜欢 kill 命令的一点是它也能够根据作业 ID 控制进程。让我们使用 gedit & 命令在后台开启 gedit 服务。假设通过 jobs 命令我得到了一个 gedit 的作业 ID,让我们为 gedit 发送 SIGINT 信号:

$ kill -s SIGINT %1

作业 ID 需要使用 % 前缀,不然 kill 会将其视作 PID。

不明确指定信号,kill 仍然可以工作。此时,默认会发送能中断进程的 SIGTERM 信号。执行 kill -l 可以查看信号名列表,使用 man kill 命令阅读手册。

使用 killall

如果您不想使用特定的工作 ID 或者 PID,killall 允许您使用特定的进程名。中断 gedit 最简单的 killall 使用方式是:

$ killall gedit

它将终止所有名为 gedit 的进程。和 kill 相似,默认发送的信号是 SIGTERM。使用 -I 选项忽略进程名的大小写。

$ gedit &amp;
[1] 14852

$ killall -I GEDIT
[1]+  Terminated              gedit

查看手册学习更多 killall 命令选项(如 -u)。

使用 xkill

您是否遇见过播放器崩溃,比如 VLC 灰屏或挂起?现在你可以像上面一样获得进程的 PID 来杀掉它,或者使用 xkill 命令终止应用程序。

 title=

xkill 允许您使用鼠标关闭窗口。仅需在终端执行 xkill 命令,它将会改变鼠标光标为一个 X 或是一个小骷髅图标。在你想关闭的进程窗口上点击 x。小心使用 xkill,如手册描述的一致,它很危险。我已经提醒过您了!

参阅手册,了解上述命令更多信息。您还可以接续探索 pkillpgrep 命令。


via: https://opensource.com/article/18/5/how-kill-process-stop-program-linux

作者:Sachin Patil 选题:lujun9972 译者:CYLeft 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

使用开源硬件和软件的 DIY 绘图仪可以自动地绘制、雕刻。

在上学时,科学系的壁橱里藏着一台惠普绘图仪。虽然我在上学的期间可以经常使用它,但我还是想拥有一台属于自己的绘图仪。许多年之后,步进电机已经很容易获得了,我又在从事电子产品和微控制器方面的工作,最近,我看到有人用丙烯酸塑料(acrylic)制作了一个显示器。这件事启发了我,并最终制作了我自己的绘图仪。

 title=

我 DIY 的绘图仪;在这里看它工作的视频

由于我是一个很怀旧的人,我真的很喜欢最初的 Arduino Uno。下面是我用到的其它东西的一个清单(仅供参考,其中一些我也不是很满意):

  • FabScan shield:承载步进电机驱动器。
  • SilentStepSticks:步进电机驱动器,因为 Arduino 自身不能处理步进电机所需的电压和电流。因此我使用了一个 Trinamic TMC2130 芯片,但它是工作在单独模式。这些替换为 Pololu 4988,但是它们运转更安静。
  • SilentStepStick 保护装置:一个防止你的电机驱动器转动过快的二极管(相信我,你肯定会需要它的)。
  • 步进电机:我选择的是使用 12 V 电压的 NEMA 17 电机(如,来自 WatterottSparkFun 的型号)。
  • 直线导杆
  • 木制的基板
  • 木螺丝
  • GT2 皮带
  • GT2 同步滑轮

这是我作为个人项目而设计的。如果你想找到一个现成的工具套件,你可以从 German Make 杂志上找到 MaXYposi

硬件安装

正如你所看到的,我刚开始做的太大了。这个绘图仪并不合适放在我的桌子上。但是,没有关系,我只是为了学习它(并且,我也将一些东西进行重新制作,下次我将使用一个更小的横梁)。

 title=

带 X 轴和 Y 轴轨道的绘图仪基板

皮带安装在轨道的侧面,并且用它将一些辅助轮和电机挂在一起:

 title=

电机上的皮带路由

我在 Arduino 上堆叠了几个组件。Arduino 在最下面,它之上是 FabScan shield,接着是一个安装在 1 和 2 号电机槽上的 StepStick 保护装置,SilentStepStick 在最上面。注意,SCK 和 SDI 针脚没有连接。

Arduino and Shield

Arduino 堆叠配置(高清大图

注意将电机的连接线接到正确的针脚上。如果有疑问,就去查看它的数据表,或者使用欧姆表去找出哪一对线是正确的。

软件配置

基础部分

虽然像 grbl 这样的软件可以解释诸如像装置移动和其它一些动作的 G-codes,并且,我也可以将它刷进 Arduino 中,但是我很好奇,想更好地理解它是如何工作的。(我的 X-Y 绘图仪软件可以在 GitHub 上找到,不过我不提供任何保修。)

使用 StepStick(或者其它兼容的)驱动器去驱动步进电机,基本上只需要发送一个高电平信号或者低电平信号到各自的针脚即可。或者使用 Arduino 的术语:

digitalWrite(stepPin, HIGH);
delayMicroseconds(30);
digitalWrite(stepPin, LOW);

stepPin 的位置上是步进电机的针脚编号:3 是 1 号电机,而 6 是 2 号电机。

在步进电机能够工作之前,它必须先被启用。

digitalWrite(enPin, LOW);

实际上,StepStick 能够理解针脚的三个状态:

  • Low:电机已启用
  • High:电机已禁用
  • Pin 未连接:电机已启用,但在一段时间后进入节能模式

电机启用后,它的线圈已经有了力量并用来保持位置。这时候几乎不可能用手来转动它的轴。这样可以保证很好的精度,但是也意味着电机和驱动器芯片都“充满着”力量,并且也因此会发热。

最后,也是很重要的,我们需要一个决定绘图仪方向的方法:

digitalWrite(dirPin, direction);

下面的表列出了功能和针脚:

功能1 号电机2 号电机
启用25
方向47
步进36

在我们使用这些针脚之前,我们需要在代码的 setup() 节中设置它的 OUTPUT 模式。

pinMode(enPin1, OUTPUT);
pinMode(stepPin1, OUTPUT);
pinMode(dirPin1, OUTPUT);
digitalWrite(enPin1, LOW);

了解这些知识后,我们可以很容易地让步进电机四处移动:

    totalRounds = ...
    for (int rounds =0 ; rounds < 2*totalRounds; rounds++) {
       if (dir==0){ // set direction
         digitalWrite(dirPin2, LOW);
       } else {
         digitalWrite(dirPin2, HIGH);
       }
       delay(1); // give motors some breathing time
       dir = 1-dir; // reverse direction
       for (int i=0; i < 6400; i++) {
         int t = abs(3200-i) / 200;
         digitalWrite(stepPin2, HIGH);
         delayMicroseconds(70 + t);
         digitalWrite(stepPin2, LOW);
         delayMicroseconds(70 + t);
       }
    }

这将使滑块向左和向右移动。这些代码只操纵一个步进电机,但是,对于一个 X-Y 绘图仪,我们要考虑两个轴。

命令解释器

我开始做一个简单的命令解释器去使用规范的路径,比如:

"X30|Y30|X-30 Y-30|X-20|Y-20|X20|Y20|X-40|Y-25|X40 Y25

用毫米来描述相对移动(1 毫米等于 80 步)。

绘图仪软件实现了一个 持续模式 ,这可以允许一台 PC 给它提供一个很大的路径(很多的路径)去绘制。(在这个视频中展示了如何绘制 Hilbert 曲线)

设计一个好用的握笔器

在上面的第一张图中,绘图笔是细绳子绑到 Y 轴上的。这样绘图也不精确,并且也无法在软件中实现提笔和下笔(如示例中的大黑点)。

因此,我设计了一个更好用的、更精确的握笔器,它使用一个伺服器去提笔和下笔。可以在下面的这张图中看到这个新的、改进后的握笔器,上面视频链接中的 Hilbert 曲线就是使用它绘制的。

 title=

图中的特写镜头就是伺服器臂提起笔的图像

笔是用一个小夹具固定住的(图上展示的是一个大小为 8 的夹具,它一般用于将线缆固定在墙上)。伺服器臂能够提起笔;当伺服器臂放下来的时候,笔就会被放下来。

驱动伺服器

驱动伺服器是非常简单的:只需要提供位置,伺服器就可以完成所有的工作。

#include <Servo.h>

// Servo pin
#define servoData PIN_A1

// Positions
#define PEN_UP 10
#define PEN_DOWN 50

Servo penServo;

void setup() {
  // Attach to servo and raise pen
  penServo.attach(servoData);
  penServo.write(PEN_UP);
}

我把伺服器接头连接在 FabScan shield 的 4 号电机上,因此,我将用 1 号模拟针脚。

放下笔也很容易:

  penServo.write(PEN_DOWN);

进一步扩展

我的进一步扩展的其中一项就是添加一些终止检测器,但是,我也可以不用它们,进而使用 TMC2130 的 StallGuard 模式来代替。这些检测器也可以用于去实现一个 home 命令。

以后,我或许还将添加一个真实的 Z 轴,这样它就可以对一个木头进行铣削雕刻,或者钻一个 PCB 板,或者雕刻一块丙烯酸塑料,或者 … (我还想到了用激光)。

这篇文章最初发布在 Some Things to Remember 博客中并授权重分发。


via: https://opensource.com/article/18/3/diy-plotter-arduino

作者:Heiko W.Rupp 译者:qhwdw 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出