一、概述

    线程是在同一程序同一时间内允许执行不同函数的离散处理队列,这使得在一个长时间进行某种特殊运算的函数在执行时不阻碍其他的函数时变得十分重要。线程实际上允许同时执行两种函数,而这两者不必相互等待。
    一旦一个应用程序启动,它仅包含一个默认线程。此线程执行main()函数。在main()中被调用的函数则按这个线程的上下文顺序地执行,这样的程序称为单线程程序。
    反之,那些创建新的线程的程序就是多线程程序。他们不仅可以在同一时间执行多个函数,而且这在如今多核盛行的时代显得尤为重要。既然多核允许同时执行多个函数,这就使得对开发人员相应地使用这种处理能力提出了要求。然而线程一直被用来当并发地执行多个函数,开发人员现在不得不仔细地构建应用来支持这种并发。多线程编程知识也因此在多核系统时代变得越来越重要。
    本章介绍的是C++ Boost库Boost.Thread,它可以开发独立于平台的多线程应用程序。

二、线程管理

    这个库中最重要的一个类就是boost::thread,它在boost/thread.hpp里定义,用来创建一个新线程。下面的示例来说明如何运用它。
 #include <boost/thread.hpp>
#include <iostream> void wait(int seconds)
{
boost::this_thread::sleep(boost::posix_time::seconds(seconds));
} void thread()
{
for (int i = ; i < ; ++i)
{
wait();
std::cout << i << std::endl;
}
} int main()
{
boost::thread t(thread);
t.join();
}
    新建线程里执行的那个函数的名称被传递到 boost::thread 的构造函数。一旦上述示例中的变量t被创建,该thread函数就在其所在线程中被立即执行,同时在main()里也并发地执行该thread。
    示例中,为了防止程序终止,就需要对新建线程调用join方法。join方法是一个阻塞调用:它可以暂停当前线程,直到调用join的线程运行结束。这就使得main函数一直会等待到thread运行结束。
    正如上面例子中看到的,一个特定的线程可以通过诸如t的变量访问,通过这个变量等待着它的使用join方法终止。 但是,即使t越界或者析构了,该线程也将继续执行。一个线程总是在一开始就绑定到一个类型为 boost::thread 的变量,但是一旦创建,就不在取决于它。 甚至还存在着一个叫detach的方法,允许类型为 boost::thread 的变量从它对应的线程里分离。当然,像 join的方法之后也就不能被调用,因为这个变量不再是一个有效的线程。
    任何一个函数内可以做的事情也可以在一个线程内完成。所以,一个线程只不过是一个函数,除了它是同时执行的。在上述例子中,使用一个循环把5个数字写入标准输出流。为了减缓输出,每一个循环中调用wait函数让执行延迟了一秒。wait可以调用一个名为sleep的函数,这个函数也来自于 Boost.Thread,位于 boost::this_thread 命名空间内。
    sleep()可以在预计的一段时间或一个特定的时间点后才让线程继续执行。通过传递一个类型为 boost::posix_time::seconds 的对象,在这个例子里我们指定了一段时间。 boost::posix_time::seconds 来自于 Boost.DateTime 库,它被 Boost.Thread 用来管理和处理时间的数据。
    虽然前面的例子说明了如何等待一个不同的线程,但下面的例子演示了如何通过所谓的中断点让一个线程中断。
 #include <boost/thread.hpp>
#include <iostream> void wait(int seconds)
{
boost::this_thread::sleep(boost::posix_time::seconds(seconds));
} void thread()
{
try
{
for (int i = ; i < ; ++i)
{
wait();
std::cout << i << std::endl;
}
}
catch (boost::thread_interrupted&)
{
}
} int main()
{
boost::thread t(thread);
wait();
t.interrupt();
t.join();
}
    在一个线程对象上调用 interrupt() 会中断相应的线程。 在这方面,中断意味着一个类型为 boost::thread_interrupted 的异常,它会在这个线程中抛出。 然后这只有在线程达到中断点时才会发生。
    如果给定的线程不包含任何中断点,简单调用interrupt就不会起作用。 每当一个线程中断点,它就会检查interrupt是否被调用过。只有被调用过了, boost::thread_interrupted 异常才会相应地抛出。
    Boost.Thread定义了一系列的中断点,例如sleep() 函数,由于sleep() 在这个例子里被调用了五次,该线程就检查了五次它是否应该被中断。然而sleep()之间的调用,却不能使线程中断。
    一旦该程序被执行,它只会打印三个数字到标准输出流。这是由于在main里3秒后调用 interrupt()方法。 因此,相应的线程被中断,并抛出一个 boost::thread_interrupted 异常。这个异常在线程内也被正确地捕获,catch 处理是空的。由于thread()函数在处理程序后返回,线程也被终止。这反过来也将终止整个程序,因为 main() 等待该线程使用join终止该线程。
    Boost.Thread定义包括上述 sleep()函数等十个中断。 有了这些中断点,线程可以很容易及时中断。然而,他们并不总是最佳的选择,因为中断点必须事前读入以检查 boost::thread_interrupted 异常。
    为了提供一个对 Boost.Thread 里提供的多种函数的整体概述,下面的例子将会再介绍两个。
 #include <boost/thread.hpp>
#include <iostream> int main()
{
std::cout << boost::this_thread::get_id() << std::endl;
std::cout << boost::thread::hardware_concurrency() << std::endl;
}
    使用 boost::this_thread命名空间,能提供独立的函数应用于当前线程,比如前面出现的sleep() 。另一个是 get_id():它会返回一个当前线程的ID号。它也是由 boost::thread 提供的。
    boost::thread 类提供了一个静态方法 hardware_concurrency() ,它能够返回基于CPU数目或者CPU内核数目的刻在同时在物理机器上运行的线程数。在常用的双核机器上调用这个方法,返回值为2。 这样的话就可以确定在一个多核程序可以同时运行的理论最大线程数。

三、同步

    虽然多线程的使用可以提高应用程序的性能,但也增加了复杂性。如果使用线程在同一时间执行几个函数,访问共享资源时必须相应地同步。一旦应用达到了一定规模,这涉及相当一些工作。本段介绍了Boost.Thread提供同步线程的类。
 #include <boost/thread.hpp>
#include <iostream> void wait(int seconds)
{
boost::this_thread::sleep(boost::posix_time::seconds(seconds));
} boost::mutex mutex; void thread()
{
for (int i = ; i < ; ++i)
{
wait();
mutex.lock();
std::cout << "Thread " << boost::this_thread::get_id() << ": " << i << std::endl;
mutex.unlock();
}
} int main()
{
boost::thread t1(thread);
boost::thread t2(thread);
t1.join();
t2.join();
}  
    多线程程序使用所谓的互斥对象来同步。Boost.Thread提供多个的互斥类,boost::mutex是最简单的一个,它的使用就像linux下的二进制互斥量。互斥的基本原则是当一个特定的线程拥有资源的时候防止其他线程夺取其所有权,一旦释放,其他的线程可以取得所有权。这将导致线程等待至另一个线程完成处理一些操作,从而相应地释放互斥对象的所有权。
    上面的示例使用一个类型为 boost::mutex 的mutex全局互斥对象。thread()函数获取此对象的所有权才在 for 循环内使用 lock()方法写入到标准输出流的。一旦信息被写入,使用unlock()方法释放所有权。
    main() 创建两个线程,同时执行thread ()函数。利用 for 循环,每个线程数到5,用一个迭代器写一条消息到标准输出流。然而,标准输出流是一个全局性的被所有线程共享的对象,该标准不提供任何保证 std::cout 可以安全地从多个线程访问。 因此,访问标准输出流必须同步:在任何时候,只有一个线程可以访问 std::cout。
    由于两个线程试图在写入标准输出流前获得互斥体,实际上只能保证一次只有一个线程访问 std::cout。不管哪个线程成功调用 lock() 方法,其他所有线程必须等待,直到 unlock() 被调用。
    获取和释放互斥体是一个典型的模式,是由Boost.Thread通过不同的数据类型支持。 例如,不直接地调用 lock() 和 unlock(),使用 boost::lock_guard 类也是可以的。
 #include <boost/thread.hpp>
#include <iostream> void wait(int seconds)
{
boost::this_thread::sleep(boost::posix_time::seconds(seconds));
} boost::mutex mutex; void thread()
{
for (int i = ; i < ; ++i)
{
wait();
boost::lock_guard<boost::mutex> lock(mutex);
std::cout << "Thread " << boost::this_thread::get_id() << ": " << i << std::endl;
}
} int main()
{
boost::thread t1(thread);
boost::thread t2(thread);
t1.join();
t2.join();
}
    boost::lock_guard 在其内部构造和析构函数分别自动调用lock() 和 unlock() 。 访问共享资源是需要同步的,因为它显示地被两个方法调用。 boost::lock_guard 类是另一个出现在我之前第2个系列智能指针单元的RAII用语。
    除了boost::mutex 和 boost::lock_guard 之外,Boost.Thread也提供其他的类支持各种同步。其中一个重要的就是 boost::unique_lock ,相比较 boost::lock_guard 而言,它提供许多有用的方法。
 #include <boost/thread.hpp>
#include <iostream> void wait(int seconds)
{
boost::this_thread::sleep(boost::posix_time::seconds(seconds));
} boost::timed_mutex mutex; void thread()
{
for (int i = ; i < ; ++i)
{
wait();
boost::unique_lock<boost::timed_mutex> lock(mutex, boost::try_to_lock);
if (!lock.owns_lock())
lock.timed_lock(boost::get_system_time() + boost::posix_time::seconds());
std::cout << "Thread " << boost::this_thread::get_id() << ": " << i << std::endl;
boost::timed_mutex *m = lock.release();
m->unlock();
}
} int main()
{
boost::thread t1(thread);
boost::thread t2(thread);
t1.join();
t2.join();
}
    上面的例子用不同的方法来演示 boost::unique_lock 的功能。 当然了,这些功能的用法对给定的情景不一定适用;boost::lock_guard 在上个例子的用法还是挺合理的。 这个例子就是为了演示 boost::unique_lock 提供的功能。
    boost::unique_lock 通过多个构造函数来提供不同的方式获得互斥体。这个期望获得互斥体的函数简单地调用了lock()方法,一直等到获得这个互斥体。所以它的行为跟 boost::lock_guard 的那个是一样的。
    如果第二个参数传入一个 boost::try_to_lock 类型的值,对应的构造函数就会调用 try_lock方法。这个方法返回 bool 型的值:如果能够获得互斥体则返回true,否则返回 false。相比lock函数,try_lock会立即返回,而且在获得互斥体之前不会被阻塞。
    上面的程序向boost::unique_lock 的构造函数的第二个参数传入boost::try_to_lock。然后通过 owns_lock() 可以检查是否可获得互斥体。如果不能, owns_lock() 返回false。这也用到 boost::unique_lock 提供的另外一个函数: timed_lock() 等待一定的时间以获得互斥体。 给定的程序等待长达1秒,应较足够的时间来获取更多的互斥。
    其实这个例子显示了三个方法获取一个互斥体:lock() 会一直等待,直到获得一个互斥体。try_lock()则不会等待,但如果它只会在互斥体可用的时候才能获得,否则返回 false。最后,timed_lock()试图获得在一定的时间内获取互斥体。和try_lock()一样,返回bool 类型的值意味着成功是否。
 
 
    虽然boost::mutex 提供了lock和try_lock两个方法,但是 boost::timed_mutex 只支持 timed_lock,这就是上面示例那么使用的原因。如果不用timed_lock的话,也可以像以前的例子那样用 boost::mutex。
    就像 boost::lock_guard 一样, boost::unique_lock 的析构函数也会相应地释放互斥量。此外,可以手动地用 unlock() 释放互斥量。也可以像上面的例子那样,通过调用 release() 解除boost::unique_lock 和互斥量之间的关联。然而在这种情况下,必须显式地调用 unlock() 方法来释放互斥量,因为 boost::unique_lock 的析构函数不再做这件事情。
    boost::unique_lock 这个所谓的独占锁意味着一个互斥量同时只能被一个线程获取。其他线程必须等待,直到互斥体再次被释放。 除了独占锁,还有非独占锁。 Boost.Thread里有个 boost::shared_lock 的类提供了非独占锁。 正如下面的例子,这个类必须和 boost::shared_mutex 型的互斥量一起使用。
 #include <boost/thread.hpp>
#include <iostream>
#include <vector>
#include <cstdlib>
#include <ctime> void wait(int seconds)
{
boost::this_thread::sleep(boost::posix_time::seconds(seconds));
} boost::shared_mutex mutex;
std::vector<int> random_numbers; void fill()
{
std::srand(static_cast<unsigned int>(std::time()));
for (int i = ; i < ; ++i)
{
boost::unique_lock<boost::shared_mutex> lock(mutex);
random_numbers.push_back(std::rand());
lock.unlock();
wait();
}
} void print()
{
for (int i = ; i < ; ++i)
{
wait();
boost::shared_lock<boost::shared_mutex> lock(mutex);
std::cout << random_numbers.back() << std::endl;
}
} int sum = ; void count()
{
for (int i = ; i < ; ++i)
{
wait();
boost::shared_lock<boost::shared_mutex> lock(mutex);
sum += random_numbers.back();
}
} int main()
{
boost::thread t1(fill);
boost::thread t2(print);
boost::thread t3(count);
t1.join();
t2.join();
t3.join();
std::cout << "Sum: " << sum << std::endl;
}
    boost::shared_lock 类型的非独占锁可以在线程只对某个资源读访问的情况下使用。一个线程修改的资源需要写访问,因此需要一个独占锁。 这样做也很明显:只需要读访问的线程不需要知道同一时间其他线程是否访问。 因此非独占锁可以共享一个互斥体。
     在给定的例子里, print() 和 count() 都可以只读访问 random_numbers 。 虽然 print() 函数把 random_numbers 里的最后一个数写到标准输出,count() 函数把它统计到 sum 变量。 由于没有函数修改 random_numbers,所有的都可以在同一时间用 boost::shared_lock 类型的非独占锁访问它。
     在 fill() 函数里,需要用一个 boost::unique_lock 类型的独占锁,因为它插入了一个新的随机数到 random_numbers。在 unlock() 显式地调用 unlock() 来释放互斥量之后, fill() 等待了一秒。 相比于之前的那个样子,在 for 循环的尾部调用 wait() 以保证容器里至少存在一个随机数,可以被print() 或者 count() 访问。 对应地,这两个函数在 for 循环的开始调用了 wait() 。
    考虑到在不同的地方每个单独地调用 wait() ,一个潜在的问题变得很明显:函数调用的顺序直接受CPU执行每个独立进程的顺序决定。 利用所谓的条件变量,可以同步哪些独立的线程,使数组的每个元素都被不同的线程立即添加到 random_numbers 。
 #include <boost/thread.hpp>
#include <iostream>
#include <vector>
#include <cstdlib>
#include <ctime> boost::mutex mutex;
boost::condition_variable_any cond; //条件变量
std::vector<int> random_numbers; void fill()
{
std::srand(static_cast<unsigned int>(std::time()));
for (int i = ; i < ; ++i)
{
boost::unique_lock<boost::mutex> lock(mutex);
random_numbers.push_back(std::rand()); //尾部加入一位
cond.notify_all();
cond.wait(mutex);
}
} void print()
{
std::size_t next_size = ;
for (int i = ; i < ; ++i)
{
boost::unique_lock<boost::mutex> lock(mutex);
while (random_numbers.size() != next_size)
cond.wait(mutex);
std::cout << random_numbers.back() << std::endl;
++next_size;
cond.notify_all();
}
} int main()
{
boost::thread t1(fill);
boost::thread t2(print);
t1.join();
t2.join();
}
    这个例子的程序删除了wait() 和count() 。线程不用在每个循环迭代中等待一秒,而是尽可能快地执行。此外,没有计算总额;数字完全写入标准输出流。
    为确保正确地处理随机数,需要一个允许检查多个线程之间特定条件的条件变量来同步不每个独立的线程。
    正如上面所说,fill() 函数用在每个迭代产生一个随机数,然后放在 random_numbers容器中。 为了防止其他线程同时访问这个容器,就要相应得使用一个排它锁。 不是等待一秒,实际上这个例子却用了一个条件变量。调用 notify_all() 会唤醒每个哪些正在分别通过调用wait() 等待此通知的线程。
    通过查看 print() 函数里的 for 循环,可以看到相同的条件变量被 wait() 函数调用了。 如果这个线程被 notify_all() 唤醒,它就会试图这个互斥量,但只有在 fill() 函数完全释放之后才能成功。
    这里的窍门就是调用 wait() 会释放相应的被参数传入的互斥量。 在调用 notify_all()后, fill() 函数会通过 wait() 相应地释放线程。 然后它会阻止和等待其他的线程调用 notify_all() ,一旦随机数已写入标准输出流,这就会在 print() 里发生。
    注意到在 print() 函数里调用 wait() 事实上发生在一个单独 while 循环里。 这样做的目的是为了处理在 print() 函数里第一次调用 wait() 函数之前随机数已经放到容器里。 通过比较 random_numbers 里元素的数目与预期值,发现这成功地处理了把随机数写入到标准输出流。

四、线程本地存储

    线程本地存储(TLS)是一个只能由一个线程访问的专门的存储区域。 TLS的变量可以被看作是一个只对某个特定线程而非整个程序可见的全局变量。 下面的例子显示了这些变量的好处。
 #include <boost/thread.hpp>
#include <iostream>
#include <cstdlib>
#include <ctime> void init_number_generator()
{
static bool done = false;
if (!done)
{
done = true;
std::srand(static_cast<unsigned int>(std::time())); //初始化随机数发生器
}
} boost::mutex mutex; void random_number_generator()
{
init_number_generator();
int i = std::rand();
boost::lock_guard<boost::mutex> lock(mutex);
std::cout << i << std::endl;
} int main()
{
boost::thread t[]; for (int i = ; i < ; ++i)
t[i] = boost::thread(random_number_generator); for (int i = ; i < ; ++i)
t[i].join();
}
     该示例创建三个线程,每个线程写一个随机数到标准输出流。 random_number_generator() 函数将会利用在C++标准里定义的 std::rand() 函数创建一个随机数。 但是用于 std::rand() 的随机数产生器必须先用 std::srand() 正确地初始化。 如果没做,程序始终打印同一个随机数。
    随机数产生器,通过 std::time() 返回当前时间, 在 init_number_generator() 函数里完成初始化。 由于这个值每次都不同,可以保证产生器总是用不同的值初始化,从而产生不同的随机数。因为产生器只要初始化一次, init_number_generator() 用了一个静态变量 done 作为条件量。
    如果程序运行了多次,写入的三分之二的随机数显然就会相同。事实上这个程序有个缺陷:std::rand所用的产生器必须被各个线程初始化。因此init_number_generator() 的实现实际上是不对的,因为它只调用了一次std::srand()。使用TLS,这一缺陷可以得到纠正。
 #include <boost/thread.hpp>
#include <iostream>
#include <cstdlib>
#include <ctime> void init_number_generator()
{
static boost::thread_specific_ptr<bool> tls;
if (!tls.get())
tls.reset(new bool(false));
if (!*tls)
{
*tls = true;
std::srand(static_cast<unsigned int>(std::time()));
}
} boost::mutex mutex; void random_number_generator()
{
init_number_generator();
int i = std::rand();
boost::lock_guard<boost::mutex> lock(mutex);
std::cout << i << std::endl;
} int main()
{
boost::thread t[]; for (int i = ; i < ; ++i)
t[i] = boost::thread(random_number_generator); for (int i = ; i < ; ++i)
t[i].join();
}
    用一个TLS变量 tls 代替静态变量 done,是基于用 bool 类型实例化的 boost::thread_specific_ptr 。原则上, tls 工作起来就像 done :它可以作为一个条件指明随机数发生器是否被初始化。 但是关键的区别,就是 tls 存储的值只对相应的线程可见和可用。
    一旦一个 boost::thread_specific_ptr 型的变量被创建,它可以相应地设置。 不过,它期望得到一个 bool 型变量的地址,而非它本身。使用reset()方法,可以把它的地址保存到 tls 里面。 在给出的例子中,会动态地分配一个bool型的变量,由new返回它的地址,并保存到 tls 里。为了避免每次调用init_number_generator()都设置tls ,它会通过get()函数检查是否已经保存了一个地址。
    由于boost::thread_specific_ptr保存了一个地址,它的行为就像一个普通的指针。 因此,operator*()和operator->()都被被重载以方便使用。这个例子用*tls检查这个条件当前是true还是false。再根据当前的条件,随机数生成器决定是否初始化。
    正如所见,boost::thread_specific_ptr允许为当前进程保存一个对象的地址,然后只允许当前进程获得这个地址。然而,当一个线程已经成功保存这个地址,其他的线程就会可能就失败。
    如果程序正在执行时,它可能会令人感到奇怪:尽管有了TLS的变量,生成的随机数仍然相等。 这是因为,三个线程在同一时间被创建,从而造成随机数生成器在同一时间初始化。如果该程序执行了几次,随机数就会改变,这就表明生成器初始化正确了。
 
文章来源:Linux公社 https://www.linuxidc.com/Linux/2011-07/39215p10.htm

Boost多线程的更多相关文章

  1. Boost多线程编程

    Boost多线程编程   背景 •       今天互联网应用服务程序普遍使用多线程来提高与多客户链接时的效率:为了达到最大的吞吐量,事务服务器在单独的线程上运行服务程序: GUI应用程序将那些费时, ...

  2. 【C/C++学院】0904-boost智能指针/boost多线程锁定/哈希库/正則表達式

    boost_array_bind_fun_ref Array.cpp #include<boost/array.hpp> #include <iostream> #includ ...

  3. Boost多线程-替换MFC线程

           Mfc的多线程看起来简单,可以把线程直接压入向量,由系统类似进行调配,其实在内存的处理问题上留下了漏洞.在新线程里面载入大量流,会导致内存泄露. 方便之处:直接使用结构体传入函数参数,供 ...

  4. boost多线程使用简例

    原文链接:http://www.cppblog.com/toMyself/archive/2010/09/22/127347.html C++ Boost Thread 编程指南 转自cnblog: ...

  5. boost多线程入门介绍

    :first-child { margin-top: 0px; } .markdown-preview:not([data-use-github-style]) h1, .markdown-previ ...

  6. 27.boost多线程

    #define _CRT_SECURE_NO_WARNINGS #include <boost/thread.hpp> #include <iostream> #include ...

  7. boost多线程编译出错

    添加 -lpthread CPLUS_INCLUDE_PATH=$CPLUS_INCLUDE_PATH:/tools/boost/includeexport CPLUS_INCLUDE_PATH LI ...

  8. linux下编译boost的多线程程序

    linux下面用boost库进行多线程编程,一开始总是编译不成功,花了好多的时间. 下面是一段小示例代码: //start from the very beginning,and to create ...

  9. boost进程间通信经常使用开发一篇全(消息队列,共享内存,信号)

    本文概要: 敏捷开发大家想必知道并且评价甚高,缩短开发周期,提高开发质量.将大project独立为不同的小app开发,整个开发过程,程序可用可測,所以提高了总体的质量.基于这样的开发模式和开发理念,进 ...

随机推荐

  1. 【二叉树的递归】05二叉树中找任意起点和终点使他们的路径和最大【Binary Tree Maximum Path Sum】

    ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 给定一个二叉树,寻找值最大的路径. ...

  2. 分享几个高效编写JS 的心得

    原则 不要做任何优化除非的确需要优化   任何的性能优化都必须以测量数据为基础,如果你怀疑代码存在性能问题,首先通过测试来验证你的想法.性能优化三问 我还能做哪些工作从而让代码变得更有效率? 流行的J ...

  3. Mathf.Sin正弦

    输入参数是弧度 Mathf.Sin   public static float Sin(float f); Parameters Description Returns the sine of ang ...

  4. Linux编程之错误代码

    头文件/usr/include/asm-generic/errno-base.h定义错误码: #ifndef _ASM_GENERIC_ERRNO_BASE_H #define _ASM_GENERI ...

  5. 微服务理论之六:ESB与SOA的关系

    一.SOA和ESB一直是没有明确概念的两个缩略词 SOA----面向服务架构,实际上强调的是软件的一种架构,一种支撑软件运行的相对稳定的结构,表面含义如此,其实SOA是一种通过服务整合来解决系统集成的 ...

  6. String/ StringBuilder/ StringBuffer

    1. 首先String不属于8种基本数据类型,String是一个对象. 因为对象的默认值是null,所以String的默认值也是null:但它又是一种特殊的对象,有其它对象没有的一些特性. 2. ne ...

  7. [51nod1247]可能的路径(思维题)

    题意:给定(a,b),(x,y)  ,(a,b)可以通向(a-b,b) (a+b,b) (a,a+b) (a,a-b) 求能否到达(x,y) 解题关键:类似于更相减损,变换过程中gcd是一样的. #i ...

  8. 【总结整理】JQuery基础学习---样式篇

    进入官方网站获取最新的版本 http://jquery.com/download/    中文 https://www.jquery123.com/ <!--JQuery:轻量级的JavaScr ...

  9. Centos 查看机器型号

    测试机器的硬件信息: 查看CPU信息(型号) # cat /proc/cpuinfo | grep name | cut -f2 -d: | uniq -c 8 Intel(R) Xeon(R) CP ...

  10. 38、EST序列拼接流程

    转载:http://fhqdddddd.blog.163.com/blog/static/18699154201241014835362/ http://blog.sina.com.cn/s/blog ...