CharlesXiao‘s Blog

  • 首页

  • 归档

  • 分类

  • 标签

  • 关于

  • 搜索

初探Goroutine调度机制

发表于 2018-03-26 | 更新于 2018-09-22 | 字数统计1.8k字 | 阅读时长7分钟

何为Goroutine调度

Goroutine是Go语言原生支持并发的具体实现,所有代码都跑在goroutine里,我们可以启动许多goroutine,多个goroutine对应一个os thread,不过最多只能允许创建10000个os线程, 如果超过10000个会抛异常。

Goroutine调度就是决定何时哪个goroutine将获得资源开始执行、哪个goroutine应该停止执行让出资源、哪个goroutine应该被唤醒恢复执行等。

Goroutine调度器

传统语言中往往是代码负责创建线程,然后交给OS来进行线程调度。操作系统调度器会将系统中的多个线程or进程按照一定算法调度到物理CPU上去运行。这样会带来一些问题,例如

  • 线程创建容易退出难,因为退出时要判断线程状态
  • 并发单元间通信困难,易错,一旦涉及到shared memory,就会用到各种lock,死锁便成为家常便饭
  • 相对于协程而言,线程开销依然很大,OS切换线程上下文的代价不小

在Golang中,goroutine可以认为是一种”轻量级线程” —- 协程,占用资源少。一个Go程序中可以创建成千上万个并发的goroutine,包括golang runtime在内的所有代码都跑在goroutine中,不涉及OS内核态。OS完全不知道goroutine的存在,go代码运行在一个或多个操作系统线程上。因此golang需要有调度器来负责调度这些goroutine,go scheduler负责将程序内的goroutines按照一定算法调度到不同的线程中去执行。

Go调度模型

G-M模型

Go 1.0版本的简单调度模型,G指代goroutine,对应于runtime中的一个抽象结构;M指machine,代表一个操作系统线程。该模型存在一些问题。例如单一全局互斥锁(Sched.Lock),意味着每个协程创建和调度都要加锁;M会做内存缓存,而且goroutine在M之间传递,造成调度延迟、、内存消耗、性能损耗。

G-P-M模型

  • 1.1版本之后沿用至今的调度模型,内部采用work stealing算法。模型图如下:

  • M represents an OS thread. It’s the thread of execution managed by the OS and works pretty much like your standard POSIX thread. In the runtime code, it’s called M for machine.

    G represents a goroutine. It includes the stack, the instruction pointer and other information important for scheduling goroutines, like any channel it might be blocked on. In the runtime code, it’s called a G.

    P represents a context for scheduling. You can look at it as a localized version of the scheduler which runs Go code on a single thread. It’s the important part that lets us go from a N:1 scheduler to a M:N scheduler. In the runtime code, it’s called P for processor.

  • 可以看到G和M之间加了个中间层P—-逻辑Processor,对于G而言,它只与P打交道,多个G会被平均分配到多个P上执行,一旦一部分G被分派到某个P上边执行,它就会存在于P的本地队列中等待被执行;而P与M的关系,可以认为是每个P绑定唯一一个M的关系,启动时的P默认数目为系统物理处理器个数。

G-P-M调度机制

  • 抢占式调度:
    • 当一个G中出现死循环或永久循环的代码逻辑,那么G将永久占用分配给它的P和M,位于同一个P中的其他G将得不到调度,出现“饿死”的情况;因此Go 1.2实现了抢占式调度机制,在每个函数或方法的入口,加上一段额外的代码,让runtime有机会检查是否需要执行抢占调;然而对于没有函数调用,纯循环计算的G,无法抢占调度。Go的具体实现是启动一个sysmon的监控线程,不绑定P,每20us~10ms启动一次,向长时间运行的G任务发出抢占调度。
    • 如果一个G任务运行10ms,sysmon就会认为其运行时间太久而发出抢占式调度的请求。一旦G的抢占标志位被设为true,那么待这个G下一次调用函数或方法时,runtime便可以将G抢占,并移出运行状态,放入P的local runq中,等待下一次被调度。
  • 网络IO/channel阻塞调度:
    • Go runtime通过实现netpoller机制来实现当goroutine发起网络I/O操作也不会导致M被阻塞(仅阻塞G),从而不会导致大量M被创建出来。
    • 对于G被网络IO/channel阻塞,调度器会将当前G放入等待队列,等到阻塞操作完成之后再次被分配给某个P执行,而此时的M会去执行下一个runnable的G;如果此时没有runnable的G供M运行,那么M将解绑P,并进入sleep状态。
  • 系统调用阻塞:

    • 当G去执行一个文件IO类似的系统调用时会导致系统G和线程M都被阻塞,P就会与M分离,去寻找其他的idle的M,没有就去创建,从而可能导致大量创建新的M的问题
    • Go 1.9增加了Poller for os package来实现在G操作支持pollable的fd时,仅阻塞G,而不阻塞M,不过对regular file无效
  • Tips:Go提供了调度器当前状态的查看方法, 使用Go运行时环境变量GODEBUG,例如GODEBUG=schedtrace=1000 godoc -http=:6060

Go programs run with multiple threads, even when GOMAXPROCS is 1. The runtime uses goroutines that call syscalls, leaving threads behind.

In order to run goroutines, a thread must hold a context.

There are 3 usual models for threading. One is N:1 where several userspace threads are run on one OS thread. This has the advantage of being very quick to context switch but cannot take advantage of multi-core systems. Another is 1:1 where one thread of execution matches one OS thread. It takes advantage of all of the cores on the machine, but context switching is slow because it has to trap through the OS.

Go tries to get the best of both worlds by using a M:N scheduler. It schedules an arbitrary number of goroutines onto an arbitrary number of OS threads. You get quick context switches and you take advantage of all the cores in your system. The main disadvantage of this approach is the complexity it adds to the scheduler.

netpooler

当系统出现高并发的IO访问时,如一个网络服务器通常要并发处理成百上千的链接,每个链接可能都是由一个用户任务执行的,那么将会出现大量阻塞的IO操作,如果为每个阻塞操作都单独分配一个OS线程,那么将会增加系统的负载。因此在Golang中针对网络IO实现了netpooler来做特别的优化,只阻塞G,不阻塞M。

当goroutine读或写阻塞时会被放到等待队列,goroutine失去运行权,而M继续执行其它的G。后台的poller不停地poll,所有的文件描述符都被添加到了这个poller中,当某个时刻一个文件描述符准备好了,poller就会唤醒之前因它而阻塞的goroutine,于是goroutine重新被分配给某个P执行。

和使用Unix系统中的select或是poll方法不同地是,Golang的netpoller查询的是能被调度的goroutine而不是那些函数指针、包含了各种状态变量的struct等,这样你就不用管理这些状态,也不用重新检查函数指针等,这些都是你在传统Unix网络I/O需要操心的问题。

参考链接

  1. The Go scheduler
  2. The Go netpoller

Effective C++ 札记

发表于 2018-03-22 | 更新于 2018-10-23 | 字数统计606字 | 阅读时长2分钟

变量常量

  1. 避免使用#define,而使用常量、枚举和内联函数,enum比const好用
  2. 出于效率原因,C++不保证内置类型(int等基本类型)数据成员的初始化。对于成员变量(class A)的内置类型, 会在构造函数进入之前进行初始化; 在构造函数前给出初始化列表来确保初始化,例如:C():b(), i(){}
  3. 静态变量的生命周期不同于栈或者堆中的对象,从它被构造开始一直存在,直到程序结束。 包括全局变量、命名空间下的变量、类中和函数中定义的static对象。 其中,定义在函数中的称为 local static,其他的称为 non-local static。

函数

  1. 编译器默认定义函数调用时机:

    • 构造函数:对象定义;使用其他兼容的类型初始化对象时(可使用 explicit 来避免这种情况)
    • 复制构造函数:用一个对象来初始化另一对象时;传入对象参数时;返回对象时;
    • 析构函数:作用域结束(包括函数返回)时;delete
    • =运算符:一个对象赋值给另一对象
    • 示例:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      Empty e1;               // 默认构造函数
      Empty e2(e1); // 拷贝构造函数
      Empty e3 = e1; // 拷贝构造函数
      e2 = e1; // = 运算符

      void func(Empty e){ // 拷贝构造函数,拷贝一份参数对象
      return e; // 拷贝构造函数,拷贝一份返回对象
      // 析构函数,拷贝得到的参数对象被析构
      }

      e2 = func(e1); // = 运算符
      // 析构函数,返回值被析构
  2. 通过把默认定义函数声明成private而且不实现它,可以禁用这些函数被调用,可以用于实现单例;也可以用base class的方式来禁用

  3. 析构函数声明为虚函数的目的在于以基类指针调用析构函数时能够正确地析构子类部分的内存。 否则子类部分的内存将会泄漏。示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 基类
    class TimeKeeper{
    public:
    virtual ~TimeKeeper();
    ...
    };
    TimeKeeper *ptk = getTimeKeeper(): // 可能返回任何一种子类
    ...
    delete ptk;
  4. 由于析构函数常常被自动调用,在析构函数中抛出的异常往往会难以捕获,引发程序非正常退出或未定义行为,所以析构函数不要抛出异常

  5. 使用对象来管理资源,类似智能指针

C++手稿集锦

发表于 2018-03-22 | 更新于 2018-10-21 | 字数统计3.6k字 | 阅读时长13分钟

哪些变量会自动初始化?

在C语言中的全局变量和静态变量都是会自动初始化为0,堆和栈中的局部变量不会初始化而拥有不可预测的值(C++默认初始化策略也是如此)。 C++保证了所有对象与对象成员都会初始化,无论是否写了圆括号或者是否写了参数列表,但其中基本数据类型的初始化还得依赖于构造函数中手动初始化。

成员变量分为成员对象和内置类型成员,其中成员对象总是会被初始化的。我们通常会在构造函数中手动初始化所有内置类型的成员。例如:

1
2
3
4
5
class A{
public:
int v;
A(): v(0);
};

参考链接

静态变量实现单例模式

1
2
3
4
5
6
7
8
9
10
11
12
13
// 将构造函数私有化,并提供获取单例的方法。 此后还需禁止复制构造函数、禁止赋值运算符。
class CPerson{
private:
static CPerson* p;
CPerson(){};
CPerson(CPerson&);
const CPerson& operator= (const CPerson&);
public:
static Person* instance(){
return p ? p : (p = new P());
}
};
CPerson* CPerson::p = NULL; // 静态成员必须在声明类的文件中进行声明(通常会初始化),否则链接错。

对象的生命周期,构造与析构

对象实例化

可以直接定义对象变量,在栈中分配并初始化对象;也可以定义对象指针,从堆中分配空间并初始化对象。

1
2
3
CPerson p1;
CPerson p2(2);
CPerson* p3 = new CPerson(3);

析构函数

类的声明中,签名为~CPerson()的方法称为析构函数。析构函数没有参数和返回值。当对象生命周期结束时被调用,通常用来释放资源。一个类只能由一个析构函数。析构函数与构造函数类似,用户不指定时编译器会生成一个缺省的析构(构造)函数, 缺省的析构(构造)函数是空函数。

构造析构顺序

对象的构造过程中,首先完成父类的构造函数,再完成成员对象的构造,最后调用当前类的构造函数:

  1. 构造父类的对象。在此过程中对象的动态类型是仍然是父类。
  2. 构造对象属性。它们实例化的顺序只取决于在类中声明的顺序,与初始化列表中的顺序无关。
  3. 调用构造函数。在这里完成当前类指定的构造过程。

对象的析构过程恰好相反,首先调用当前类的析构函数,然后析构对象属性,最后析构父类对象。

对象指针数组

1
CPerson *p[3] = {new CPerson(1), new CPerson(2)};

复制构造函数

类的声明中,签名为CPerson(CPerson&)的方法称为复制构造函数,用来从一个已存在的对象复制生成一个新的对象。 在如下三种情况下会被调用:

  1. 用一个对象初始化另一个对象时。例如:

    1
    2
    CPerson p2(p1);
    CPerson p2 = p1;
  2. 对象作为参数传递时。例如:

    1
    void func(A a){}
  3. 对象作为返回值时。例如:

    1
    A func(){ A a; return a;}

只有一个参数的复制构造函数可以被称为转换构造函数。当需要类型转换时,会被调用:

1
CPerson = 2;    // CPerson(int) called

注意区分赋值和初始化:对象变量间赋值不会调用复制构造函数。赋值只会按位拷贝对象所在的内存。当然你也可以重载operator=来改变它的行为。

对象生命周期

如下程序解释了对象的声明周期何时开始,以及何时结束。涉及到了:全局对象、静态对象、栈中的对象、堆中的对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
CPerson p1;             // main执行前,构造函数被调用
void func(){
static CPerson p2; // func第一次执行前,构造函数被调用
CPerson p3; // p3的构造函数被调用
// func结束时,p3的析构函数被调用
}
int main(){
CPerson p4, *p5; // 调用p4的构造函数
func();
p5 = new CPerson; // 调用p5的构造函数
delete p5; // 调用p5的析构函数
// main结束时,p4的析构函数被调用
}
// 程序结束前,p1, p2的析构函数被调用

栈对象和堆对象的创建方式

C++中,内存划分为三个逻辑区域:堆、栈和静态存储区,对象分为堆对象,栈对象以及静态对象(构造之后直到main程序结束才调用析构函数将其销毁,不管是local还是非local static对象)。而类的对象建立分为两种,一种是静态建立,如A a;另一种是动态建立,如A* ptr=new A;两者都要执行构造函数。

  1. 静态建立类对象:是由编译器为对象在栈空间中分配内存,是通过直接移动栈顶指针,挪出适当的空间,然后在这片内存空间上调用构造函数形成一个栈对象。使用这种方法,直接调用类的构造函数,内存空间自动释放

    。

  2. 动态建立类对象:是使用new运算符将对象建立在堆空间中。这个过程分为两步,第一步是执行operator new()函数,在堆空间中搜索合适的内存并进行分配;第二步是调用构造函数构造对象,初始化这片内存空间。这种方法,间接调用类的构造函数,内存空间需要手动释放。

  3. 注意点:

  1. 栈对象和堆对象区别和优劣 : 栈对象的创建速度一般较堆对象快,它仅仅需要移动栈顶指针就可以被创建,递归函数中最好不要使用栈对象以避免栈溢出;程序员对堆对象的生命具有完全的控制权。比如,我们需要创建一个对象,能够被多个函数所访问,但是又不想使其成为全局的,那么这个时候创建一个堆对象无疑是良好的选择,然后在各个函数之间传递这个堆对象的指针,便可以实现对该对象的共享。

  2. 如何限制类对象只能在堆上建立? 编译器在为类对象分配栈空间时,会先检查类的析构函数的访问性,其实不光是析构函数,只要是非静态的函数,编译器都会进行检查。如果类的析构函数是私有的,则编译器不会在栈空间上为类对象分配内存(如果我们将构造函数设置为私有,那么我们也就不能用new来直接产生堆对象了,因为new在为对象分配空间后也会调用它的构造函数)。为了统一,可以将构造函数设为protected,然后提供一个public的static函数来完成构造,这样不使用new,而是使用一个函数来构造,使用一个函数来析构。

  3. 构造函数设置为私有会限制继承。如果一个类不打算作为基类,可以将其析构函数声明为private。为了限制栈对象,却不限制继承,我们可以将析构函数声明为protected,这样就两全其美。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    >    class NoStackObject { 
    > protected:
    > NoStackObject() { }
    > ~NoStackObject() { }
    > public:
    > static NoStackObject* creatInstance() {
    > return new NoStackObject() ;//调用保护的构造函数
    > }
    > void destroy() {
    > delete this ;//调用保护的析构函数
    > }
    > };
    > NoStackObject* hash_ptr = NoStackObject::creatInstance() ;
    > hash_ptr->destroy() ;
    > hash_ptr = NULL ; //防止使用悬挂指针
    >

>

  1. 只要禁用new运算符就可以实现类对象只能建立在栈上: 你不能影响new operator的能力(因为那是C++语言内建的),但是new operator 总是先调用 operator new,而后者我们是可以自行声明重写的。因此,将operator new()设为私有即可禁止对象被new在堆上;如果也想禁止堆对象数组,可以把operator new[]和operator delete[]也声明为private。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    >    class A    
    > {
    > private:
    > void* operator new(size_t t){} // 注意函数的第一个参数和返回值都是固定的
    > void operator delete(void* ptr){} // 重载了new就需要重载delete
    > public:
    > A(){}
    > ~A(){}
    > };
    >

>

  1. CPP进程的地址空间分配-堆栈空间

引用和指针

  1. 引用定义一个变量的别名,是一种隐式的指针,对它的操作都会被解释为对它引用的对象的操作。 引用不占用栈空间,因为编译器知道它的地址。但作为参数传递引用时,会把指针放在参数栈中。为了在函数中修改传入的参数,可以把函数参数声明为引用。引用作为函数的返回值,一般是为了在函数外部修改内部变量。

常量指针与指针常量

常量指针指向地方的内容不可改变,指针常量指向的地方不可改变

1
2
3
4
5
6
7
8
9
10
11
12
13
// 定义整数
int n = 1, m=2;

// 常量指针
const int * p = &n;
*p = 5; // 编译错,内容不可变

// 指针常量
int * const p = &n;
p = &m; // 编译错,地址不可变

// 指针和指向的对象都是常量
const int * const p = &n;

指针数组vs数组指针

1
2
3
4
5
// 指针数组:数组的长度为8,数组的每一项都是类型为int*的指针
int* arr[8];

// 数组指针:一个指向长度为8的数组的指针
int (*arr)[8];

函数指针

函数指针通常用来进行传参,借此实现动态的策略。传参时可以用函数名,也可以用函数指针。而函数指针需要用函数名来初始化。

1
2
3
4
5
6
7
8
9
10
11
12
int sum(int a, int b)j{
return a+b;
}
void wrapper(int a, int b, int (*p)(int, int)){
cout<<p(a, b)<<endl;
}

int main(){
wrapper(2, 3, sum);
int (*p)(int, int) = sum;
wrapper(2, 3, p);
}

C++ 智能指针与裸指针

我们知道c++的内存管理是让很多人头疼的事,当我们写一个new语句时,一般就会立即把delete语句直接也写了,但是我们不能避免程序还未执行到delete时就跳转了或者在函数中没有执行到最后的delete语句就返回了,如果我们不在每一个可能跳转或者返回的语句前释放资源,就会造成内存泄露。智能指针就是一个类,当超出了类的作用域,类会自动调用析构函数,析构函数会自动释放资源。

为什么要用智能指针?

  1. 难以区分裸指针指向的是单个对象还是一个数组,也无法确定到底是用delete(销毁单个对象)还是delete[]销毁一个数组;
  2. 使用完指针之后无法判断是否应该销毁指针,因为无法判断指针是否“拥有”指向的对象,也无法确定销毁指针的方式;
  3. 遗漏销毁指正可能导致内存泄露,而销毁多次则会导致未定义行为。

四种智能指针

  1. std::auto_ptr和std::unique_ptr:auto_ptr是c++ 98遗留的关键字,已经不建议使用,auto_ptr的功能都可以由unique_ptr更加高效的做到。
    • auto_ptr智能指针可以像类的原始指针一样访问类的public成员,成员函数get()返回一个原始的指针,成员函数reset()重新绑定指向的对象,而原来的对象则会被释放。注意我们访问auto_ptr的成员函数时用的是“.”,访问指向对象的成员时用的是“->”。
    • auto_ptr指针当我们对智能指针进行赋值时,如ptest2 = ptest,ptest2会接管ptest原来的内存管理权,ptest会变为空指针,如果ptest2原来不为空,则它会释放原来的资源,基于这个原因,应该避免把auto_ptr放到容器中,因为算法对容器操作时,很难避免STL内部对容器实现了赋值传递操作,这样会使容器中很多元素被置为NULL。判断一个智能指针是否为空不能使用if(ptest == NULL),应该使用if(ptest.get() == NULL)。
    • unique_ptr指针永远“拥有”其指向的对象,unique_ptr是一个move-only类型,一个unique_ptr指针无法被复制,只能将“所有权”在两个unique_ptr指针之间转移,转移完成后源unique_ptr将被设为null。unique_ptr默认的销毁方式是通过对unique_ptr中的裸指针进行delete操作,它可以无缝地转换成shared_ptr。不能使用两个unique_ptr智能指针赋值操作,应该使用std::move,例如foo(std::move(ptest))
  2. std::shared_ptr和std::weak_ptr:
    • shared_ptr使用计数机制来表明资源被几个指针共享,可以自动管理对象的生命周期和GC,shared_ptr的引用计数增减是原子操作
    • 一个对象可以被多个shared_ptr指向和访问,这些shared_ptr类型的指针共同享有该对象的所有权,当最后一个指向该对象的shared_ptr生命周期结束的时候,对象被销毁
    • shared_ptr的构造将引用计数加1,销毁的时候引用计数减1,而赋值则将源指针引用计数加1,目标指针引用计数减1,例如P1=P2,P1指向对象的引用计数减1,P2指向对象的引用计数加1。当引用计数减1之后为0的时候,shared_ptr将会销毁指向的对象。
    • 由于control_block的存在,shared_ptr的size通常是2倍裸指针或unique_ptr的大小
    • weak_ptr是用来解决shared_ptr相互引用时的死锁问题,如果说两个shared_ptr相互引用,那么这两个指针的引用计数永远不可能下降为0,资源永远不会释放。它是对对象的一种弱引用,不会增加对象的引用计数,和shared_ptr之间可以相互转化,shared_ptr可以直接赋值给它,它可以通过调用lock函数来获得shared_ptr。不能通过weak_ptr直接访问对象的方法,应该先把它转化为shared_ptr

Golang学习总结

发表于 2018-02-07 | 更新于 2018-12-18 | 字数统计664字 | 阅读时长3分钟

基础语法

Channel+WaitGroup

  1. 实现发布/订阅Server:for+select循环监听发布事件、订阅事件、取消事件
  2. 工作调度器:

interface

  1. go 没有显式的关键字用来实现 interface,如果一个类型实现了一个 interface 中所有方法,我们说类型实现了该 interface

  2. go 允许不带任何方法的 interface ,这种类型的 interface 叫 empty interface。go 不会对类型是interface{} 的 slice 进行转换 。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    // 报错:cannot use names (type []string) as type []interface {} in argument to printAll
    // 原因:interface{} 会占用两个字长的存储空间,一个是自身的 methods 数据,一个是指向其存储值的指针,也就是 interface 变量存储的值;slice []interface{} 其长度是固定的N*2,但是 []T 的长度是N*sizeof(T),两种 slice 实际存储值的大小是有区别的
    func printAll(vals []interface{}) {
    for _, val := range vals {
    fmt.Println(val)
    }
    }
    func main(){
    names := []string{"stanley", "david", "oscar"}
    printAll(names)
    }

    // 手动转换成[]interface{}
    var dataSlice []int = foo()
    var interfaceSlice []interface{} = make([]interface{}, len(dataSlice))
    for i, d := range dataSlice {
    interfaceSlice[i] = d
    }
  3. interface 变量存储的是实现者的值,也就是可以申明一个interface,然后把具体的struct赋值给其进行初始化。

  4. Type assertions (类型断言),用于区分 interface 的变量究竟存储哪种类型的值

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    s := S{}
    var i I //声明 i
    i = &s //赋值 s 到 i

    // 方案一
    if t, ok := i.(*S); ok {
    fmt.Println("s implements I", t)
    }

    // 方案二
    switch t := i.(type) {
    case *S:
    fmt.Println("i store *S", t)
    case *R:
    fmt.Println("i store *R", t)
    }
  5. interface 定义时并没有严格规定实现者的方法 receiver 是个 value receiver 还是 pointer receiver。interface 的实现者 ss的方法 receiver 都是 value receiver,执行函数func(i)可以无论是 pointer 还是 value 都可以正确执行,也就是func(&ss)或者func(ss);如果ss的方法receiver存在pointer receiver,则不能调用func(ss),因为根据value找不到地址

  6. go 中函数都是按值传递即 passed by value。对于 receiver 是 value 的 method,任何在 method 内部对 value 做出的改变都不影响调用者看到的 value,这就是按值传递。

参考链接

  1. 理解 Go interface 的 5 个关键点

其它

gofmt与goimports

gofmt follows a simple rule that works for all use cases: imports may be separated by
blank lines, and each set of blank lines is sorted independently.

goimports, which does more than just formatting, applies a different rule: it groups
imports according to whether they are standard imports, appengine imports, or other
imports. It emits each group separately. Then the gofmt algorithm kicks in and sorts
each group independently.
The goimports and gofmt programs do different jobs and it is not a goal that they
produce identical results.

大数据分析相关工具

发表于 2018-02-07 | 更新于 2018-02-12 | 字数统计100字 | 阅读时长1分钟

基础组件

  1. Hadoop = HDFS + YARN + MapReduce;HDFS负责存储,YARN负责资源调度,依然发挥重要作用,MapReduce计算框架
  2. Hbase主要解决实时数据查询问题;Hive和Pig主要解决数据处理和计算问题,分别以SQL形式和脚本形式
  3. 如何用形象的比喻描述大数据的技术生态?Hadoop、Hive、Spark 之间是什么关系?

1234…18
CharlesXiao

CharlesXiao

在码农炼成之路不断挣扎……stay hungry……keep learning……

87 日志
18 分类
78 标签
github weibo Daijiale的个人站点
推荐阅读
  • RocksDB
  • Google FE
© 2015.05.16 – 2019 CharlesXiao
本站总访问量:
|
总访客数:
|
博客全站共169.6k字