Chapter 1-01

Please indicate the source: http://blog.csdn.net/gaoxiangnumber1

Welcome to my github: https://github.com/gaoxiangnumber1

?读者应具有C++多线程编程经验,熟悉互斥器、竞态条件等概念,了解智能指针,知道Observer设计模式。

1.1 当析构函数遇到多线程

?C++要求程序员自己管理对象的生命期。当一个对象能被多个线程同时看到时,对象的销毁时机就会变得模糊不清,可能出现多种竞态条件(race condition):

?在析构一个对象前,如何知道此刻是否有别的线程正在执行该对象的成员函数?

?如何保证在执行成员函数时,对象不会在另一个线程被析构?

?在调用某个对象的成员函数之前,如何得知这个对象还活着?它的析构函数会不会碰巧执行到一半?

1.1.1 线程安全的定义

?依据[JCP],一个线程安全的class应当满足以下三个条件:

1.多个线程同时访问时,其表现出正确的行为。

2.无论操作系统如何调度这些线程,无论这些线程的执行顺序如何交织(interleaving)。

3.调用端代码无须额外的同步或其他协调动作。

?C++标准库里的大多数class都不是线程安全的,包括std::string、std::vector、std::map等,因为这些class通常需要在外部加锁才能供多个线程同时访问。

1.1.2MutexLock与MutexLockGuard

?两个工具类代码见§2.4。

?MutexLock封装临界区(critical section),是一个资源类,用RAII手法[CCS,条款13] 封装互斥器的创建与销毁。临界区在Windows上是struct CRITI-CAL_SECTION,是可重入的;在 Linux 下是 pthread_mutex_t,默认是不可重入的(可重入与不可重入的讨论见 §2.1.1)。MutexLock 一般是别的 class 的数据成员。



?Resource Acquisition Is Initialization机制解决的是这样一个问题:

在C++中,如果在这个程序段结束时需要完成资源释放工作,那么正常情况下没有问题,但是当一个异常抛出时,释放资源的语句就不会被执行。确保能运行资源释放代码的地方就是在这个程序段(栈帧)中放置的对象的析构函数了,因为stack winding会保证它们的析构函数都会被执行。

?RAII (Resource Acquisition Is Initialization),也称为“资源获取就是初始化”,是C++语言的一种管理资源、避免泄漏的惯用法。C++标准保证任何情况下,已构造的对象最终会销毁,即它的析构函数最终会被调用。RAII的做法是使用一个对象,在其构造时获取资源,在对象生命期控制对资源的访问使之始终保持有效,最后在对象析构的时候释放资源。

?将初始化和资源释放都移动到一个包装类中的好处:

1.保证了资源的正常释放

2.省去了在异常处理中冗长、重复的清理逻辑,确保了代码的异常安全。

3.简化代码体积。

?资源管理技术的关键在于:要保证资源的释放顺序与获取顺序严格相反。这使我们联想到局部对象的创建和销毁过程。管理局部对象的任务非常简单,因为它们的创建和销毁工作是由系统自动完成的。我们只需在某个作用域(scope)中定义局部对象(这时系统自动调用构造函数以创建对象),然后就可以放心地使用;当控制流程超出这个作用域的范围时,系统会自动调用析构函数,从而销毁该对象。将资源抽象为类,用局部对象来表示资源,把管理资源的任务转化为管理局部对象的任务。这就是 RAII 惯用法的真谛。



?MutexLockGuard封装临界区的进入和退出,即加锁和解锁。MutexLockGuard 一般是个栈上对象,它的作用域刚好等于临界区域。

?这两个 class 都不允许拷贝构造和赋值,它们的使用原则见 § 2.1。

1.1.3 一个线程安全的 Counter 示例

编写单个的线程安全的 class只需用同步原语保护其内部状态。

Code
#include<pthread.h>
#include<assert.h>
#include <boost/noncopyable.hpp>

class MutexLock// : boost ::noncopyable
{
private:
    pthread_mutex_t mutex_;
    pid_t holder_;

public:
    MutexLock() : holder_(0)
    {
        pthread_mutex_init(&mutex_, NULL);
    }

    ~MutexLock()
    {
        assert(holder_ == 0);
        pthread_mutex_destroy(&mutex_);
    }

    void Lock()
    {
        pthread_mutex_lock(&mutex_);
        // holder_ = CurrentThread::tid();
    }

    void Unlock()
    {
        holder_ = 0;
        pthread_mutex_unlock(&mutex_);
    }
};

class MutexLockGuard : boost::noncopyable
{
private:
    MutexLock &mutex_;

public:
    explicit MutexLockGuard(MutexLock &mutex) : mutex_(mutex)
    {
        mutex_.lock();
    }

    ~MutexLockGuard()
    {
        mutex_.unlock();
    }
};

#define MutexLockGuard(x) static_assert(false, "missing mutex guard var name")

class Counter : boost::noncopyable
{
private:
    int64_t value_;
    mutable MutexLock mutex_;

public:
    Counter() : value_(0) {}

    int64_t Value() const
    {
        MutexLockGuard lock(mutex_);
        return value_;
    }

    int64_t GetAndIncrease()
    {
        MutexLockGuard lock(mutex_);
        int64_t ret = value_++;
        return ret;
    }
};

int main()
{

    return 0;
}

1.assert.h干什么的?

?assert.h是C标准库的一个头文件,该头文件的目的是提供一个assert的宏定义。assert只是对所给的表达式求值,然后如果该值为真则正常运行,否则报错,并调用abort(),产生异常中断,exit出来。该宏可以屏蔽掉,只需在包含assert.h之前#define NDEBUG,想开再#undef。

2.assert(holder == 0);的意思

?#include

Widget w1;
Widget w2(w1);  // error
Widget w2 = w1;  // error
w2 = w1;  // error

5.int64_t代表什么?

?8字节的signed integer

?这个 class 是线程安全的。每个 Counter 对象有自己的 mutex_,因此不同对象之间不构成锁争用(lock contention)。两个线程有可能同时执行 L24(int64_t ret = value_++;),前提是它们访问的不是同一个Counter 对象。mutex_ 成员是 mutable,意味着 const 成员函数如 Counter::value() 也能直接使用 non-const 的 mutex_。

?尽管这个 Counter 本身是线程安全的,但如果 Counter 是动态创建的并通过指针来访问,前面提到的对象销毁的 race condition 仍然存在。

1.2 对象的创建很简单

?对象构造要做到线程安全,唯一的要求是在构造期间不要泄露 this 指针,即

1.不要在构造函数中注册任何回调;

2.不要在构造函数中把 this 传给跨线程的对象;

3.即便在构造函数的最后一行也不行。

?之所以这样规定是因为在构造函数执行期间对象还没有完成初始化,如果this被泄露(escape)给了其他对象(其自身创建的子对象除外),那么别的线程有可能访问这个半成品对象,这会造成难以预料的后果。

recipes/thread/test/Observer.cc
#include <algorithm>
#include <vector>
#include <stdio.h>

class Observable;

class Observer
{
public:
    virtual ~Observer();
    virtual void update() = 0;

    void observe(Observable* s);

protected:
    Observable* subject_;
};

class Observable
{
public:
    void register_(Observer* x);
    void unregister(Observer* x);

    void notifyObservers()
    {
        for (size_t i = 0; i < observers_.size(); ++i)
        {
            Observer* x = observers_[i];
            if (x)
            {
                x->update(); // (3)
            }
        }
    }

private:
    std::vector<Observer*> observers_;
};

Observer::~Observer()
{
    subject_->unregister(this);
}

void Observer::observe(Observable* s)
{
    s->register_(this);
    subject_ = s;
}

void Observable::register_(Observer* x)
{
    observers_.push_back(x);
}

void Observable::unregister(Observer* x)
{
    std::vector<Observer*>::iterator it = std::find(observers_.begin(), observers_.end(), x);
    if (it != observers_.end())
    {
        std::swap(*it, observers_.back());
        observers_.pop_back();
    }
}

// ---------------------

class Foo : public Observer
{
    virtual void update()
    {
        printf("Foo::update() %p\n", this);
    }
};

int main()
{
    Foo* p = new Foo;
    Observable subject;
    p->observe(&subject);
    subject.notifyObservers();
    delete p;
    subject.notifyObservers();
}

1.virtual void update() = 0;

?纯虚函数(pure virtual function).申明格式如下:

class Shape

{

public:

virtual void Show() = 0;

};

?在普通的虚函数后面加上” = 0”这样就声明了一个pure virtual function.

?什么情况下使用纯虚函数(pure virtual function)?

1)想在基类中抽象出一个方法,且该基类只能被继承,而不能被实例化;

2)这个方法必须在派生类(derived class)中被实现;

2.printf(“Foo::update() %p\n”, this);

?p 显示一个指针,即地址。

?// 不要这么做(Don’t do this.)

class Foo : public Observer // Observer 的定义见第 10 页
{
public:
Foo(Observable* s)
{
s->register_(this); // 错误,非线程安全
}
virtual void update();
};

?对象构造的正确方法:

// 要这么做(Do this.)

class Foo : public Observer
{
public:
Foo();
virtual void update();
// 另外定义一个函数,在构造之后执行回调函数的注册工作
void observe(Observable* s)
{
s->register_(this);
}
};
Foo* pFoo = new Foo;
Observable* s = getSubject();
pFoo->observe(s); // 二段式构造,或者直接写 s->register_(pFoo);

?二段式构造(构造函数+ initialize())有时是好办法,这虽然不符合C++教条,但多线程下别无选择。既然允许二段式构造,那么构造函数不必主动抛异常,调用方靠initialize()的返回值来判断对象是否构造成功,这能简化错误处理。

?即使构造函数的最后一行也不要泄露 this,因为 Foo 有可能是个基类,基类先于派生类构造,执行完 Foo::Foo() 的最后一行代码还会继续执行派生类的构造函数,这时 most-derived class 的对象还处于构造中,仍然不安全。

1.3 销毁太难

?对象析构在单线程里不构成问题,最多需要注意避免空悬指针和野指针。空悬指针(dangling pointer)指向已经销毁的对象或已经回收的地址,野指针(wild pointer)指的是未经初始化的指针(http://en.wikipedia.org/wiki/Dangling_pointer)。

?在多线程程序中,对一般成员函数而言,做到线程安全的办法是让它们顺次执行,而不要并发执行(关键是不要同时读写共享状态),也就是让每个成员函数的临界区不重叠。隐含条件:成员函数用来保护临界区的互斥器本身必须是有效的。而析构函数破坏了这一假设,它会把 mutex 成员变量销毁掉。

1.3.1 mutex 不是办法

?mutex只能保证函数一个接一个地执行,下面的代码试图用互斥锁来保护析构函数:(注意(1)和(2)两处标记)

Foo::~Foo()
{
MutexLockGuard lock(mutex_);
// free internal state (1)
}   void Foo::update()
{
MutexLockGuard lock(mutex_); // (2)
// make use of internal state
}

?此时,有 A、B 两个线程都能看到 Foo 对象 x,线程 A 即将销毁 x,而线程 B 正准备调用 x->update()。

extern Foo* x; // visible by all threads
// thread A
delete x;
x = NULL; // helpless   // thread B
if (x)
{
x->update();
}

?尽管线程 A 在销毁对象之后把指针置为了 NULL,线程 B 在调用 x 的成员函数之前检查了指针 x 的值,但还是无法避免一种 race condition:

1.线程 A 执行到了析构函数的 (1) 处,已经持有了互斥锁,即将继续往下执行。

2.线程 B 通过了 if (x) 检测,阻塞在 (2) 处。

?接下来会发生什么未知。因为析构函数会把 mutex_ 销毁,那么 (2) 处有可能永远阻塞下去,有可能进入“临界区”,然后 core dump,或者发生其他更糟糕的情况。



?core:内存、核心;

dump:抛出,扔出;

core dump:当某程序崩溃的一瞬间,内核会抛出当时该程序进程的内存详细情况,存储在一个名叫core.xxx(xxx为一个数字,比如core.699)的文件中。

?core dump是指对应程序由于各种异常或者bug导致在运行过程中异常退出或者中止,并且在满足一定条件下会产生一个叫做core的文件。

?通常情况下,core文件会包含程序运行时的内存,寄存器状态,堆栈指针,内存管理信息还有各种函数调用堆栈信息等,我们可以理解为是程序工作当前状态存储生成的文件,通过工具分析这个文件,我们可以定位到程序异常退出的时候对应的堆栈调用等信息,找出问题所在并解决。



?这个例子说明 delete 对象之后把指针置为 NULL 根本没用,如果一个程序要靠这个来防止二次释放,说明代码逻辑出了问题。

1.3.2 作为数据成员的 mutex 不能保护析构

?前面的例子说明,作为 class 数据成员的 MutexLock 只能用于同步本 class 的其他数据成员的读和写,它不能保护安全地析构。因为 MutexLock 成员的生命期最多与对象一样长,而析构动作可说是发生在对象身亡之后(或者身亡之时)。

?对于基类对象,调用到基类析构函数的时候,派生类对象的那部分已经析构了,那么基类对象拥有的 MutexLock 不能保护整个析构过程。

?析构过程本来也不需要保护,因为只有别的线程都访问不到这个对象时,析构才是安全的,否则会有§1.1谈到的竞态条件发生。

?如果要同时读写一个 class 的两个对象, 有潜在的死锁可能。比方说有swap()这个函数:

void swap(Counter& a, Counter& b)
{
MutexLockGuard aLock(a.mutex_); // potential dead lock
MutexLockGuard bLock(b.mutex_);
int64_t value = a.value_;
a.value_ = b.value_;
b.value_ = value;
}

?如果线程 A 执行 swap(a, b); 而同时线程 B 执行 swap(b, a);,就有可能死锁。operator=() 也是类似的道理。

Counter& Counter::operator=(const Counter& rhs)
{
if (this == &rhs)
return *this;
MutexLockGuard myLock(mutex_);  // potential dead lock
MutexLockGuard itsLock(rhs.mutex_);
value_ = rhs.value_; // 改成 value_ = rhs.value() 会死锁
return *this;
}

?一个函数如果要锁住相同类型的多个对象,为了保证始终按相同的顺序加锁,我们可以比较 mutex 对象的地址,始终先加锁地址较小的 mutex。

1.4 线程安全的 Observer 有多难

?一个动态创建的对象是否还活着,光看指针是看不出来的,引用也一样看不出来。指针就是指向了一块内存,这块内存上的对象如果已经销毁,那么就根本不能访问 [CCS,条款 99] (就像 free(3) 之后的地址不能访问一样),既然不能访问又如何知道对象的状态呢?

?判断一个指针是不是合法指针没有高效的办法,这是C/C++指针问题的根源。万一原址又创建了一个新的对象呢?再万一这个新的对象的类型异于老的对象呢?

?在面向对象程序设计中,对象的关系主要有三种:composition、aggregation、association。composition(组合/复合)关系在多线程里不会遇到什么麻烦,因为对象 x 的生命期由其唯一的拥有者 owner 控制,owner 析构的时候会把 x 也析构掉。从形式上看,x 是 owner 的直接数据成员,或者 scoped_ptr 成员,抑或 owner 持有的容器的元素。后两种关系在C++里比较难办,处理不好就会造成内存泄漏或重复释放。

?association(关联/联系)表示一个对象 a 用到了另一个对象 b,调用了后者的成员函数。从代码形式上看,a 持有 b 的指针(或引用),但是 b的生命期不由 a 单独控制。

?aggregation(聚合)关系从形式上看与 association 相同,除了 a 和 b 有逻辑上的整体与部分关系。如果 b 是动态创建的并在整个程序结束前有可能被释放,那么就会出现 § 1.1 谈到的竞态条件。

?一个简单的办法是:只创建不销毁。程序使用一个对象池来暂存用过的对象,下次申请新对象时,如果对象池里有存货,就重复利用现有的对象,否则就新建一个。对象用完了,不是直接释放掉,而是放回池子里。这个办法有缺点,但至少能避免访问失效对象的情况发生。缺点有:

1.对象池的线程安全,如何安全地、完整地把对象放回池子里,防止出现“部分放回”的竞态?(线程 A 认为对象 x 已经放回了,线程 B 认为对象 x 还活着。)

2.全局共享数据引发的 lock contention,这个集中化的对象池会不会把多线程并发的操作串行化?

3.如果共享对象的类型不止一种,那么是重复实现对象池还是使用类模板?

4.会不会造成内存泄漏与分片?因为对象池占用的内存只增不减,而且多个对象池不能共享内存。

?如果对象 x 注册了任何非静态成员函数回调,那么必然在某处持有了指向 x 的指针,这就暴露在了 race condition 之下。

?一个典型的场景是 Observer 模式(代码见 recipes/thread/test/Observer.cc)。

?当 Observable 通知每一个 Observer 时 (L17),它从何得知 Observer 对象 x 还活着?试试在 Observer 的析构函数里调用 unregister() 来解注册?恐难奏效。

?我们试着让 Observer 的析构函数去调用 unregister(this),这里有两个 race conditions。

其一:L32 如何得知 subject_ 还活着?

其二:就算 subject_指向某个永久存在的对象,那么还是险象环生:

1.线程 A 执行到 L32 之前,还没有来得及 unregister 本对象。

2.线程 B 执行到 L17,x 正好指向是 L32 正在析构的对象。

?这时悲剧又发生了,既然 x 所指的 Observer 对象正在析构,调用它的任何非静态成员函数都是不安全的,何况是虚函数(C++标准对在构造函数和析构函数中调用虚函数的行为有明确规定,但是没有考虑并发调用的情况)。更糟糕的是, Observer 是个基类,执行到 L32 时,派生类对象已经析构掉了,这时候整个对象处于将死未死的状态,core dump 恐怕是最幸运的结果。

?这些 race condition 似乎可以通过加锁来解决,但在哪儿加锁,谁持有这些互斥锁,又不是那么显而易见的。要是有什么活着的对象能帮我们就好了,它提供一个 isAlive() 之类的程序函数,告诉我们那个对象还在不在。可惜指针和引用都不是对象,它们是内建类型。

1.5 原始指针有何不妥

?指向对象的原始指针(raw pointer)是坏的,尤其当暴露给别的线程时。Observable 应当保存的不是原始的 Observer*,而是别的什么东西,能分辨 Observer 对象是否存活。类似地,如果 Observer 要在析构函数里解注册(这虽然不能解决前面提到的 race condition,但是在析构函数里打扫战场还是应该的),那么 subject_ 的类型也不能是原始的 Observable*。

空悬指针

?有两个指针 p1 和 p2,指向堆上的同一个对象 Object,p1 和 p2 位于不同的线程中(图 1-1 的左图)。假设线程 A 通过 p1 指针将对象销毁了(尽管把 p1 置为了NULL),那p2就成了空悬指针(图 1-1 的右图) 。这是一种典型的C/C++内存错误。

?要想安全地销毁对象,最好在别人(线程)都看不到的情况下完成。这正是垃圾回收的原理,所有人都用不到的东西一定是垃圾。

一个“解决办法”

?解决空悬指针的办法是引入一层间接性,让 p1 和 p2 所指的对象永久有效。比如图 1-2 中的 proxy 对象,这个对象,持有一个指向 Object 的指针。p1 和 p2 都是二级指针。

?当销毁 Object 之后,proxy 对象继续存在,其值变为 0(见图 1-3)。而 p2 也没有变成空悬指针,它可以通过查看 proxy 的内容来判断 Object 是否还活着。

?线程安全地释放Object也不容易,race condition 依旧存在。比如 p2看第一眼的时候 proxy 不是零,正准备去调用 Object 的成员函数,期间对象已经被p1 给销毁了。问题在于,何时释放 proxy 指针呢?

一个更好的解决办法

?为了安全地释放 proxy,我们可以引入引用计数(reference counting),再把 p1和 p2 都从指针变成对象 sp1 和 sp2。proxy 现在有两个成员,指针和计数器。

1.一开始,有两个引用,计数值为 2(见图 1-4) 。

2.sp1 析构了,引用计数的值减为 1(见图 1-5)。

3.sp2 也析构了,引用计数降为 0,可以安全地销毁 proxy 和 Object了(见图 1-6)。

一个万能的解决方案

?引入另外一层间接性(another layer of indirection http://en.wikipedia.org/wiki/Abstraction_layer),用对象来管理共享资源(如果把Object看作资源的话),亦即 handle/body 惯用技法(idiom)。C++的TR1标准库里提供了一对“神兵利器”,可助我们完美解决这个头疼的问题。

1.6 神器 shared_ptr/weak_ptr

?shared_ptr 是引用计数型智能指针,在 Boost 和 std::tr1 里均提供,也被纳入C++11 标准库,现代主流的 C++ 编译器都能很好地支持。shared_ptr 是一个类模板(class template),它只有一个类型参数,使用方便。

?引用计数是自动化资源管理的常用手法,当引用计数降为 0 时,对象(资源)即被销毁。weak_ptr 也是一个引用计数型智能指针,但是它不增加对象的引用次数,即弱(weak)引用。

?几个关键点。

1.shared_ptr控制对象的生命期。shared_ptr是强引用,只要有一个指向 x 对象的 shared_ptr 存在,该 x 对象就不会析构。当指向对象 x 的最后一个 shared_ptr 析构或 reset() 的时候,x 保证会被销毁。

2.weak_ptr不控制对象的生命期,但是它知道对象是否还活着。如果对象还活着,那么它可以提升(promote)为有效的shared_ptr;如果对象已经死了,提升会失败,返回一个空的 shared_ptr。“提升/lock()”行为是线程安全的。

3.shared_ptr/weak_ptr 的“计数”在主流平台上是原子操作,没有用锁,性能不俗。

4.shared_ptr/weak_ptr的线程安全级别与std::string和STL容器一样,后面会讲。

?孟岩《垃圾收集机制批判》(http://blog.csdn.net/myan/article/details/1906)中点出智能指针的优势:“C++利用智能指针达成的效果是:一旦某对象不再被引用,系统立刻回收内存。这通常发生在关键任务完成后的清理(clean up)时期,不会影响关键任务的实时性,同时,内存里所有的对象都是有用的,绝对没有垃圾空占内存。”

1.7 系统地避免各种指针错误

?C++里可能出现的内存问题有:

1.缓冲区溢出(buffer overrun)。

2.空悬指针/野指针。

3.重复释放(double delete)。

4.内存泄漏(memory leak)。

5.不配对的 new[]/delete。

6.内存碎片(memory fragmentation)。

?正确使用智能指针能解决前面5个问题,第6个问题在§ 9.2.1和§ A.1.8探讨。

1.缓冲区溢出:用std::vector/std::string 或自己编写 Buffer class 来管理缓冲区,自动记住用缓冲区的长度,并通过成员函数而不是裸指针来修改缓冲区。

2.空悬指针/野指针:用 shared_ptr/weak_ptr,这正是本章的主题。

3.重复释放:用 scoped_ptr,只在对象析构的时候释放一次。

4.内存泄漏:用 scoped_ptr,对象析构的时候自动释放内存。

5.不配对的 new[]/delete:把 new[] 统统替换为 std::vector/scoped_array。

?在这几种错误里边,内存泄漏相对危害性较小,因为它只是借了东西不归还,程序功能在一段时间内还算正常。其他如缓冲区溢出或重复释放等致命错误可能会造成安全性(security和data safety)方面的严重后果。

?注意:scoped_ptr/shared_ptr/weak_ptr都是值语意,要么是栈上对象,或是其他对象的直接数据成员,或是标准库容器里的元素。不会有下面这种用法:

shared_ptr* pFoo = new shared_ptr(new Foo); // WRONG semantic

?还要注意,如果这几种智能指针是对象 x 的数据成员,而它的模板参数 T 是个incomplete 类型,那么 x 的析构函数不能是默认的或内联的,必须在 .cpp 文件里边显式定义,否则会有编译错或运行错(原因见 § 10.3.2) 。

Please indicate the source: http://blog.csdn.net/gaoxiangnumber1

Welcome to my github: https://github.com/gaoxiangnumber1

时间: 2024-08-08 17:54:45

Chapter 1-01的相关文章

我喜欢减肥我们来减肥吧

http://www.ebay.com/cln/honus.jyw4mvptb/cars/158313278016/2015.01.28.html http://www.ebay.com/cln/honus.jyw4mvptb/cars/158313282016/2015.01.28.html http://www.ebay.com/cln/honus.jyw4mvptb/cars/158313289016/2015.01.28.html http://www.ebay.com/cln/usli

百度回家看沙发沙发是减肥了卡斯加积分卡拉是减肥

http://www.ebay.com/cln/hpryu-caw8ke/cars/158056866019/2015.01.31 http://www.ebay.com/cln/xub.50x2l7cj/cars/158445650015/2015.01.31 http://www.ebay.com/cln/xub.50x2l7cj/cars/158445674015/2015.01.31 http://www.ebay.com/cln/xub.50x2l7cj/cars/1584456790

巢哑偕倥乇椭煞谙暗逞帕俸

IEEE Spectrum 杂志发布了一年一度的编程语言排行榜,这也是他们发布的第四届编程语言 Top 榜. 据介绍,IEEE Spectrum 的排序是来自 10 个重要线上数据源的综合,例如 Stack Overflow.Twitter.Reddit.IEEE Xplore.GitHub.CareerBuilder 等,对 48 种语言进行排行. 与其他排行榜不同的是,IEEE Spectrum 可以让读者自己选择参数组合时的权重,得到不同的排序结果.考虑到典型的 Spectrum 读者需求

我国第三代移动通信研究开发进展-尤肖虎200106

众所周知,数据科学是这几年才火起来的概念,而应运而生的数据科学家(data scientist)明显缺乏清晰的录取标准和工作内容.此次课程以<星际争霸II>回放文件分析为例,集中在IBM Cloud相关数据分析服务的应用.面对星际游戏爱好者希望提升技能的要求,我们使用IBM Data Science Experience中的jJupyter Notebooks来实现数据的可视化以及对数据进行深度分析,并最终存储到IBM Cloudant中.这是个介绍+动手实践的教程,参会者不仅将和讲师一起在线

pl/sql学习1——标量变量psahnh6S

为类型.不能用于表列的数据类型.范围为的子类型.自然数.为的子类型.具有约束为单精度浮点数.为变量赋值时.后面要加为双精度浮点数.为变量赋值时.后面要加.为数字总位数.为小数位数是的子类型.最大精度位是的子类型.最大精度位单精度浮点型是的子类型.最大精度位双精度浮点型定义精度为位的实数..定义为位的整数.变长字符串.最长测试变量数据!.定长字符串.最长测试变长二进制字符串物理存储的为类型...固定长度.个字节使用定义数据类型那个最小值:最大值:最小值:最大值:最小值:最大值:最小值:最大值:最小

chapter 01

hibernate.cfg.xml <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE hibernate-configuration PUBLIC "-//Hibernate/Hibernate Configuration DTD 3.0//EN" "http://hibernate.sourceforge.net/hibernate-configuration-3.0.

[Learning You a Haskell for Great Goods!] chapter 01 starting out

Installation under CentOS/Fedora # yum install ghc Version [[email protected] haskell]# ghc -v Glasgow Haskell Compiler, Version 7.0.4, for Haskell 98, stage 2 booted by GHC version 7.0.4 Change prompt echo :set prompt "ghci> " > ~/.ghci C

Chapter 01:创建和销毁对象

<一>考虑用静态工厂方法代替构造器 下面是Boolean类的一个简单示例: public final class Boolean implements java.io.Serializable, Comparable<Boolean> { public static final Boolean TRUE = new Boolean(true); public static final Boolean FALSE = new Boolean(false); public static

Notes : &lt;Hands-on ML with Sklearn &amp; TF&gt; Chapter 7

.caret, .dropup > .btn > .caret { border-top-color: #000 !important; } .label { border: 1px solid #000; } .table { border-collapse: collapse !important; } .table td, .table th { background-color: #fff !important; } .table-bordered th, .table-bordere

[CSS Mastery]Chapter 1: Setting the Foundations

Chapter 1: Setting the Foundations The human race is a naturally inquisitive species. We just love tinkering with things. When I recently bought a new iMac, I had it to bits within seconds, before I’d even read the instruction manual. We enjoy workin