Linux 高性能服务器编程——多线程编程
1 创建线程和结束线程;
2 读取和设置线程属性;
3 线程同步方式:POSIX信号量,互斥锁和条件变量。
- 创建和调度线程都无须内核的干预,因此速度相当快。
- 不占用额外的内核资源,很多线程不会对系统性能造成明显影响。
- (缺点)一个进程的多个线程无法运行在不同的CPU上
- 内核调度M个内核线程,线程库调度N个用户线程
- 不会过度消耗内核资源,又可以充分利用多处理器的优势
#include <pthread.h>
int pthread_create ( pthread_t* thread, const pthread_attr_t* attr,
void * (*start_routine)(void*) , void* arg);
#include <pthread.h>
void pthread_exit ( void* retval );
#include <pthread.h>
int pthread_join( pthread_t thread, void** retval );
#include <pthread.h>
int pthread_cancel ( pthread_t thread );
接收到取消请求的目标线程可以决定是否允许被取消以及如何取消。这分别由如下两个函数完成:
#include <pthread.h>
int pthread_setcancelstate( int state, int *oldstate );
int pthread_setcanceltype ( int type, int *oldtype );
- PTHREAD_CANCEL_ENABLE:允许线程被取消。它是线程被创建时默认取消状态。
- PTHREAD_CANCEL_DISABLE:禁止线程被取消。这种情况下,如果一个线程收到取消请求,则它会将请求挂起,直到该线程允许被取消。
- PTHREAD_CANCEL_ASYNCHRONOUS:线程随时都可以被取消。它将使得接收到取消请求的目标线程立即采取行动。
- PTHREAD_CANCEL_DEFERRED:允许目标线程推迟行动,直到它调用了下面几个所谓的取消点函数中的一个:pthread_join、pthread_testcancel、pthread_cond_wait、pthread_cond_timewait、sem_wait和sigwait。
#include <bits/pthreadtypes.h>
#define __SIZEOF_PTHREAD_ATTR_T 36
typedef union
{
char __size[__SIZEOF_PTHREAD_ATTR_T];
long int __align;
} pthread_attr_t;
#include <pthread.h>
/*初始化线程属性对象*/
int pthread_attr_init( pthread_attr_t* attr );
/*销毁线程属性对象,被销毁的线程属性对象只有再次初始化之后才能继续使用*/
int pthread_attr_destroy( pthread_attr_t* attr);
/*下面这些函数用于获取和设置线程属性对象的某个属性*/
int pthread_attr_getdetachstate( const pthread_attr_t* attr ,int* detachstate );
int pthread_attr_setdetachstate( pthread_attr_t* attr,int detachstate );
int pthread_attr_getstackaddr( const pthread_attr_t* attr,void **stackaddr );
int pthread_attr_setstackaddr( pthread_attr_t* attr,void* stackaddr );
int pthread_attr_getstacksize( const pthread_attr_t* attr,size_t* stacksize );
int pthread_attr_setstacksize( pthread_attr_t* attr,size_t stackszie );
int pthread_attr_getstack( const pthread_attr_t* attr,void** stackaddr,size_t* stacksize );
int pthread_attr_setstack( pthread_attr_t* attr,void* stackaddr,size_t stacksize );
int pthread_attr_getguardsize( const pthread_attr_t* attr,size_t* guarsize );
int pthread_attr_setguardsize( pthread_attr_t* attr,size_t guarsize );
int pthread_attr_getschedparam( const pthread_attr_t* attr,struct sched_param* param );
int pthread_attr_setschedparam( pthread_attr_t* attr,const struct sched_param* param );
int pthread_attr_getschedpolicy( const pthread_attr_t* attr,int* policy );
int pthread_attr_setschedpolicy( pthread_attr_t* attr,int policy );
int pthread_attr_getinheritsched( const pthread_attr_t* attr,int* inherit );
int pthread_attr_setinheritsched( pthread_attr_t* attr,int inherit );
int pthread_attr_getscope( const pthread_attr_t* attr,int* scope );
int pthread_attr_setscope( pthread_attr_t* attr,int scope );
- detachstate:线程的脱离状态。它有PTHREAD_CREATE_JOINABLE和PTHREAD_CREATE_DETACH两个可选值。前者指定线程是可以被回收的,后者使调用线程脱离与进程中其他线程的同步。脱离了与其他线程同步的线程称为“脱离线程”。脱离线程在退出时将自行释放其占用的系统资源。线程创建时该属性的默认值是PTHREAD_CREATE_JOINABLE。
- stackaddr和stacksize:线程堆栈的起始地址和大小。一般来说,我们不需要自己来管理线程堆栈,因为Linux默认为每个线程分配了足够的堆栈空间(一般是8 MB)。我们可以使用ulimt -s 命令查看或修改这个默认值。
- guardsize:保护区域大小。如果guardsize大于0,则系统创建线程的时候会在其堆栈的尾部额外分配guardsize字节的空间,作为保护堆栈不被错误的覆盖的区域。如果guardsize等于0,则系统不为新创建的线程设置堆栈保护区。如果使用者通过pthread_attr_setstackaddr或pthread_attr_setstack函数手动设置线程的堆栈,则guardsize属性将被忽略。
- schedparam:线程调度参数。其类型时sched_param结构体。该结构体面前还只有一个整型类型的成员——sched_priority,该成员表示线程的运行优先级。
- schedpolicy:线程调度策略。该属性有SCHED_FIFO、SCHED_RR和SCHED_OTHER三个可选值,其中SCHED_OTHER是默认值。SCHED_RR表示采用轮转算法调度,SCHED_FIFO表示使用先进先出的方法调度,这两种调度方法都具备实时调度功能,但只能用于以超级用户身份运行的进程。
- inheritsched:是否继承调用线程的调度属性。该属性有PTHREAD_INHERIT_SCHED和PTHREAD_EXPLICIT_SCHED 两个可选值。前者表示新线程沿用其创建者的线程调度参数,这种情况下再设置新线程的调度参数将没有任何效果。后者表示调用者要明确的指定新线程的调度参数。
- scope:线程间竞争CPU的范围,即线程优先级的有效范围。POSIX标准定义了该属性的PTHREAD_SCOPE_SYSTEM和PTHREAD_SCOPE_PROCESS两个可选值,前者表示目标线程与系统中所有线程一起竞争CPU,后者表示目标线程仅与其他隶属于同一进程的线程竞争CPU的使用。面前Linux 只支持PTHREAD_SCOPE_SYSTEM这一种取值。
#include <semaphore.h>
int sem_init ( sem_t* sem, int pshared, unsigned int value ); // 初始化一个未命名的信号量
int sem_destroy ( sem_t* sem ); // 用于销毁信号量,以释放其占用的内核资源
int sem_wait ( sem_t*sem ); // 以原子操作的方式将信号量减1,如果信号量的值为0,则阻塞,直到该值不为0.
int sem_trywait ( sem_t* sem ); // sem_wait的非阻塞版本
int sem_post ( sem_t* sem ); // 以原子操作的方式将信号量的值加1
- sem_init函数用于初始化一个未命名的信号量。pshared参数指定信号量的类型。如果其值是0,就表示这个信号量是当前进程的局部信号量,否则该信号量就可以在多个进程之间共享。value参数指定信号量的初始值。此外,初始化一个已经被初始化的信号量将导致不可预期的结果。
- sem_destroy函数用于销毁信号量,以释放其占用的内核资源。如果销毁一个正被其他线程等待的信号量,则将导致不可预期的结果。
- sem_wait函数以原子操作的方式将信号量的值减1.如果信号量的值为0,则sem_wait将被阻塞,直到这个信号量具有非0值。
- sem_trywait与sem_wait函数相似,不过它始终立即返回,而不论被操作的信号量是否具有非0值,相当于sem_wait的非阻塞版本。当信号量的值大于0值时,sem_trywait对信号量执行减1操作。当信号量的值为0时,它将返回-1,并设置errno为EAGAIN。
- sem_post函数以原子操作的方式将信号量的值加1.当信号量的值大于0时,其他正在调用sem_wait等待信号量的线程将被唤醒。
#include <pthread.h>
int pthread_mutex_init ( pthread_mutex_t* mutex, const pthread_mutexattr_t* mutexattr );
int pthread_mutex_destroy ( pthread_mutex_t* mutex );
int pthread_mutex_lock ( pthread_mutex_t* mutex );
int pthread_mutex_trylock ( pthread_mutex_t* mutex );
int pthread_mutex_unlock ( pthread_mutex_t* mutex );
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
宏PTHREAD_MUTEX_INITIALIZER实际上只是把互斥锁的各个字段都初始化为0.
#include <pthread.h>
/* 初始化互斥锁属性对象 */
int pthread_mutexattr_init ( pthread_mutexattr_t* attr );
/* 销毁互斥锁属性对象 */
int pthread_mutexattr_destroy ( pthread_mutexattr_t* attr );
/* 获取和设置互斥锁的pshared属性 */
int pthread_mutexattr_getpshared ( const pthread_mutexattr_t* attr, int * pshared );
int pthread_mutexattr_setpshared ( pthread_mutexattr_t* attr, int pthread );
/* 获取和设置互斥锁的type属性 */
int pthread_mutexattr_gettype ( const pthread_mutexattr_t* atr, int * type );
int pthread_mutexattr_settype ( pthread_mutexattr_t* attr, int type );
- PTHREAD_PROCESS_SHARED:互斥锁可以被跨进程共享。
- PTHREAD_PROCESS_PRIVATE:互斥锁只能被和锁的初始化线程隶属于同一个进程的线程共享。
- PTHREAD_MUTEX_DEFAULT:默认锁(缺省的互斥锁类型属性)。如果一个线程试图对一个默认锁重复锁定或者试图解锁一个由别的线程锁定的默认锁或者试图解锁已经被解锁的默认锁会引发不可预料的结果。
- PTHREAD_MUTEX_NORMAL:普通锁。当一个线程对一个普通锁加锁以后,其余请求该锁的线程将形成一个等待队列,并在该锁解锁后按优先级获得锁。这种锁类型保证了资源分配的公平性。但这种锁也很容易引发问题:(1)一个线程如果对一个已经加锁的普通锁再次加锁,将引发死锁。(2)对一个已经被其他线程加锁的普通锁解锁,或者对一个已经解锁的普通锁再次解锁,将导致不可预期的后果。
- PTHREAD_MUTEX_ERRORCHECK:检错锁。 如果一个线程试图对一个互斥锁重复锁定,将会返回一个错误码EDEADLK。 如果试图解锁一个由别的线程锁定的互斥锁或者试图解锁已经被解锁的互斥锁,则解锁操作返回EPERM。
- PTHREAD_MUTEX_RECURSIVE:嵌套锁。如果一个线程对这种类型的互斥锁重复上锁,不会引起死锁,不过一个线程对这类互斥锁的多次重复上锁必须由这个线程来重复相同数量的解锁,这样才能解开这个互斥锁,别的线程才能得到这个互斥锁。如果对一个已经被其他线程加锁的嵌套锁解锁,或者对一个已经解锁的嵌套锁再次解锁,则解锁操作返回EPERM。这种类型的互斥锁只能是进程私有的(作用域属性为PTHREAD_PROCESS_PRIVATE)。
#include <pthread.h>
#include <unistd.h>
#include <stdio.h> int a = 0;
int b = 0;
pthread_mutex_t mutex_a;
pthread_mutex_t mutex_b; void* another( void* arg )
{
pthread_mutex_lock( &mutex_b );
printf( "in child thread, got mutex b, waiting for mutex a\n" );
sleep( 5 );
++b;
pthread_mutex_lock( &mutex_a );
b += a++;
pthread_mutex_unlock( &mutex_a );
pthread_mutex_unlock( &mutex_b );
pthread_exit( NULL );
} int main()
{
pthread_t id; pthread_mutex_init( &mutex_a, NULL );
pthread_mutex_init( &mutex_b, NULL );
pthread_create( &id, NULL, another, NULL ); pthread_mutex_lock( &mutex_a );
printf( "in parent thread, got mutex a, waiting for mutex b\n" );
sleep( 5 );
++a;
pthread_mutex_lock( &mutex_b );
a += b++;
pthread_mutex_unlock( &mutex_b );
pthread_mutex_unlock( &mutex_a ); pthread_join( id, NULL );
pthread_mutex_destroy( &mutex_a );
pthread_mutex_destroy( &mutex_b );
return 0;
}
#include <pthread.h>
int pthread_cond_init (pthread_cond_t* cond, const pthread_condattr_t* cond_attr);
int pthread_cond_destroy ( pthread_cond_t* cond );
int pthread_cond_broadcast ( pthread_cond_t* cond ); //以广播的形式唤醒一个等待目标条件变量的线程
int pthread_cond_signal ( pthread_cond_t* cond ); //唤醒一个等待目标条件变量的线程
int pthread_cond_wait ( pthread_cond_t* cond, pthread_mutex_t* mutex ); // 等待目标条件变量,mutex参数保证对条件变量及其等待队列的操作原子性。
- pthread_cond_init函数用于初始化条件变量。cond_attr参数指定条件变量的属性。如果将它设置为NULL,则表示使用默认属性。除了pthread_cond_init函数外,还可以如下初始化一个条件变量:
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
宏PTHREAD_COND_INITIALIZER实际上只是把条件变量的各个字段都初始化为0.
- pthread_cond_destroy 用于销毁条件变量,以释放其占用的内核资源。销毁一个正在被等待的条件变量将失败并返回EBUSY。
- pthread_cond_broadcast 函数以广播的方式唤醒所有等待目标条件变量的线程。
- pthread_cond_signal 函数用于唤醒一个等待目标条件变量的线程。至于哪个线程将被唤醒,则取决于线程的优先级和调度策略。有时候我们可能想唤醒一个指定线程,但pthread没有对该需求提供解决方案。不过我们可以间接实现该需求:定义一个能够唯一表示目标线程的全局变量,在唤醒等待条件变量的线程前先设置该变量为目标线程,然后采用广播方式唤醒所有等待条件变量的线程,这些线程被唤醒后都检查该变量以判断被唤醒的是否是自己,如果是就开始执行后续代码,如果不是则返回继续等待。
#ifndef LOCKER_H
#define LOCKER_H #include <exception>
#include <pthread.h>
#include <semaphore.h> class sem
{
public:
sem()
{
if( sem_init( &m_sem, 0, 0 ) != 0 )
{
throw std::exception();
}
}
~sem()
{
sem_destroy( &m_sem );
}
bool wait()
{
return sem_wait( &m_sem ) == 0;
}
bool post()
{
return sem_post( &m_sem ) == 0;
} private:
sem_t m_sem;
}; class locker
{
public:
locker()
{
if( pthread_mutex_init( &m_mutex, NULL ) != 0 )
{
throw std::exception();
}
}
~locker()
{
pthread_mutex_destroy( &m_mutex );
}
bool lock()
{
return pthread_mutex_lock( &m_mutex ) == 0;
}
bool unlock()
{
return pthread_mutex_unlock( &m_mutex ) == 0;
} private:
pthread_mutex_t m_mutex;
}; class cond
{
public:
cond()
{
if( pthread_mutex_init( &m_mutex, NULL ) != 0 )
{
throw std::exception();
}
if ( pthread_cond_init( &m_cond, NULL ) != 0 )
{
pthread_mutex_destroy( &m_mutex );
throw std::exception();
}
}
~cond()
{
pthread_mutex_destroy( &m_mutex );
pthread_cond_destroy( &m_cond );
}
bool wait()
{
int ret = 0;
pthread_mutex_lock( &m_mutex );
ret = pthread_cond_wait( &m_cond, &m_mutex );
pthread_mutex_unlock( &m_mutex );
return ret == 0;
}
bool signal()
{
return pthread_cond_signal( &m_cond ) == 0;
} private:
pthread_mutex_t m_mutex;
pthread_cond_t m_cond;
}; #endif
#include <pthread.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <wait.h> pthread_mutex_t mutex; /*子线程运行的函数。它首先获得互斥锁mutex,然后暂停5s ,再释放该互斥锁*/
void* another( void* arg )
{
printf( "in child thread, lock the mutex\n" );
pthread_mutex_lock( &mutex );
sleep( 5 );
pthread_mutex_unlock( &mutex );
} void prepare()
{
pthread_mutex_lock( &mutex );
} void infork()
{
pthread_mutex_unlock( &mutex );
} int main()
{
pthread_mutex_init( &mutex, NULL );
pthread_t id;
pthread_create( &id, NULL, another, NULL );
//pthread_atfork( prepare, infork, infork );
/*父进程中的主线程暂停1s,以确保在执行fork操作之前,子线程已经开始运行并获得了互斥变量mutex*/
sleep( 1 );
int pid = fork();
if( pid < 0 )
{
pthread_join( id, NULL );
pthread_mutex_destroy( &mutex );
return 1;
}
else if( pid == 0 )
{
printf( "I anm in the child, want to get the lock\n" );
/*子进程从父进程继承了互斥锁mutex的状态,该互斥锁处于锁住的状态,这是由父进程中的子线程执行pthread_mutex_lock引起的,因此
,下面这句加锁操作会一直阻塞,尽管从逻辑上来说它是不应该阻塞的*/
pthread_mutex_lock( &mutex );
printf( "I can not run to here, oop...\n" );
pthread_mutex_unlock( &mutex );
exit( 0 );
}
else
{
pthread_mutex_unlock( &mutex );
wait( NULL );
}
pthread_join( id, NULL );
pthread_mutex_destroy( &mutex );
return 0;
}
#include <pthread.h>
int pthread_atfork( void (*prepare)(void), void (*parent)(void),void (*child)(void) );
该函数将建立3个fork句柄来帮助我们清理互斥锁的状态。该函数成功时返回0,失败则返回错误码。
- prepare 句柄将在fork调用创建出子进程之前被执行。它可以用来锁住父进程中的互斥锁。
- parent 句柄则是fork调用创建出子进程之后,而fork返回之前,在父进程中被执行。它的作用是释放所有在prepare 句柄中被锁住的互斥锁。
- child 句柄是在fork 返回之前,在子进程中被执行。和parent句柄一样,child 句柄也是用于释放所有在prepare 句柄中被锁住的互斥锁。
void prepare()
{
pthread_mutex_lock( &mutex );
} void infork()
{
pthread_mutex_unlock( &mutex );
} pthread_atfork( prepare, infork, infork );
#include <pthread.h>
include <signal.h>
int pthread_sigmask( int how, const sigset_t* newmask, sigset_t* oldmask );
由于进程中的所有线程共享该进程的信号,所以线程库将根据线程掩码决定把信号发送给哪个具体的线程。因此,如果我们在每个子线程中都单独设置信号掩码,就很容易导致逻辑错误。此外,所有线程共享信号处理函数。也就是说,当我们在一个线程中设置了某个信号的信号处理函数后,它将覆盖其他线程为同一个信号设置的信号处理函数。这两点都说明,我们应该定义一个专门的线程来处理所有的信号。这可以通过如下两个步骤来实现:
- 在主线程创建出其他子线程之前就调用pthread_sigmask来设置好信号掩码,所有新创建的子线程都将自动继承这个信号掩码。这样做之后,实际上所有线程都不会响应被屏蔽的信号了。
- 在某个线程中调用如下函数来等待信号并处理之:
#include <signal.h>
int sigwait( const sigset_t* set, int* sig );set 参数指定需要等待的信号的集合。我们可以简单的将其指定为在第一步中创建的信号掩码,表示在该线程中等待所有被屏蔽的信号。参数sig 指向的整数用于存储该函数返回的信号值。sigwait成功时返回0,失败则返回错误码。一旦sigwait正确返回,我们就可以对接收到的信号做处理了。很显然,如果我们使用了sigwait,就不应该再为信号设置信号处理函数了。这是因为当程序接收到了信号时,二者中只能有一个起作用。
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h> /* Simple error handling functions */ #define handle_error_en(en, msg) \
do { errno = en; perror(msg); exit(EXIT_FAILURE); } while (0) static void *sig_thread(void *arg)
{
printf( "yyyyy, thread id is: %ld\n", pthread_self() );
sigset_t *set = (sigset_t *) arg;
int s, sig;
for (;;)
{
/*第二步骤,调用sigwait等待信号*/
s = sigwait(set, &sig);
if (s != 0)
handle_error_en(s, "sigwait");
printf("Signal handling thread got signal %d\n", sig);
}
} int main(int argc, char *argv[])
{
pthread_t thread;
sigset_t set;
int s; /*第一步骤,在主线程中设置信号掩码*/
sigemptyset(&set);
sigaddset(&set, SIGQUIT);
sigaddset(&set, SIGUSR1);
s = pthread_sigmask(SIG_BLOCK, &set, NULL);
if (s != 0)
handle_error_en(s, "pthread_sigmask");
s = pthread_create(&thread, NULL, &sig_thread, (void *) &set);
if (s != 0)
handle_error_en(s, "pthread_create");
printf( "sub thread with id: %ld\n", thread ); pause(); /* Dummy pause so we can test program */
}
pthread 还提供了下面的方法,使得我们可以明确的将一个信号发送给指定的线程:
#include <signal.h>
int pthread_kill( pthread_t thread, int sig );
thread参数指定目标线程。
Linux 高性能服务器编程——多线程编程的更多相关文章
- Linux高性能server规划——多线程编程(在)
多线程编程 Linux主题概述 线程模型 线程是程序中完毕一个独立任务的完整执行序列.即一个可调度的实体. 依据执行环境和调度者的身份.线程可分为内核线程和用户线程.内核线程,在有的系统上也称为LWP ...
- Linux 高性能服务器编程——高性能服务器程序框架
问题聚焦: 核心章节. 服务器一般分为如下三个主要模块:I/O处理单元(四种I/O模型,两种高效事件处理模块),逻辑单元(两种高效并发模式,有效状态机)和存储单元(不讨论). 服务器模 ...
- linux高性能服务器编程
<Linux高性能服务器编程>:当当网.亚马逊 目录: 第一章:tcp/ip协议族 第二章:ip协议族 第三章:tcp协议详解 第四章:tcp/ip通信案例:访问Internet 第五章: ...
- linux高性能服务器编程 (一) --Tcp/Ip协议族
前言: 在学习swoole入门基础的过程中,遇到了很多知识瓶颈,比方说多进程.多线程.以及进程池和线程池等都有诸多的疑惑.之前也有学习相关知识,但只是单纯的知识面了解.而没有真正的学习他们的来龙去脉. ...
- Linux 高性能服务器编程——多进程编程
问题聚焦: 进程是Linux操作系统环境的基础. 本篇讨论以下几个内容,同时也是面试经常被问到的一些问题: 1 复制进程映像的fork系统调用和替换进程映像的exec系列系统调 ...
- Linux 高性能服务器编程——I/O复用
问题聚焦: 前篇提到了I/O处理单元的四种I/O模型. 本篇详细介绍实现这些I/O模型所用到的相关技术. 核心思想:I/O复用 使用情景: 客户端程序要同时处理多个socket ...
- Linux 高性能服务器编程——Linux服务器程序规范
问题聚焦: 除了网络通信外,服务器程序通常还必须考虑许多其他细节问题,这些细节问题涉及面逛且零碎,而且基本上是模板式的,所以称之为服务器程序规范. 工欲善其事,必先利其器,这篇主要来探 ...
- Linux 高性能服务器编程——Linux网络编程基础API
问题聚焦: 这节介绍的不仅是网络编程的几个API 更重要的是,探讨了Linux网络编程基础API与内核中TCP/IP协议族之间的关系. 这节主要介绍三个方面的内容:套接字(so ...
- Linux 高性能服务器编程——TCP协议详解
问题聚焦: 本节从如下四个方面讨论TCP协议: TCP头部信息:指定通信的源端端口号.目的端端口号.管理TCP连接,控制两个方向的数据流 TCP状态转移过程:TCP连接的任意一 ...
随机推荐
- 阿里云在RSAC 2018上宣布 将在西雅图建立安全实验室
日前,2018年度的RSA Conference全球信息安全大会在美国加州旧金山市召开.作为全球三大云计算服务商之一,阿里云携3款全新安全产品亮相,并宣布今年将在西雅图设立全新的安全实验室,整合全球安 ...
- .NET CORE 2.0 踩坑记录之ConfigurationManager
在之前.net framework 中使用的ConfigurationManager还是很方便的,不过在.NET CORE 2.0中SDK默认已经不存在ConfigurationManager. 那么 ...
- slf4j 与各个 logging框架的适配器说明
在java领域里,日志框架纷杂繁多,项目中必然要使用很多的第三方库,而这些第三方库所使用的log框架又不尽相同.想要打出合理有效的日志,就必须在你的项目中将这些日志框架统一才行.幸好,slf4j, c ...
- vi/vim下tab的长度修改
默认下的长度是8,如果要想修改可以在根目录下新建'.vimrc'文件 里面的内容是: [root@localhost 09:06 ~]# cat .vimrc set tabstop=4 set sh ...
- EventBus InMemory 的实践基于eShopOnContainers (二)
前言 最近在工作中遇到了一个需求,会用到EventBus,正好看到eShopOnContainers上有相关的实例,去研究了研究.下面来分享一下用EventBus 来改造一下我们上篇Event发布与实 ...
- [HNOI 2014]江南乐
Description 题库链接 给你指定一个数 \(f\) ,并给你 \(T\) 组游戏,每组有 \(n\) 堆石子, \(A,B\) 两人轮流对石子进行操作,每次你可以选择其中任意一堆数量不小于 ...
- 【BZOJ3506】【Cqoi2014】排序机械臂
传送门(因为BZOJ上没有题面...所以放的是luogu的) 题意:你需要维护一个序列,支持区间翻转与查询区间最小. 解题思路:由于区间最小实际上每一次就是对应的整个数列的第k小,因此可以直接预处理解 ...
- [Russian Code Cup 2017 - Finals [Unofficial Mirror]]简要题解
来自FallDream的博客,未经允许,请勿转载,谢谢. Div1难度+ACM赛制 和几个大佬组队逛了逛 A.给一个大小为n的集合ai(1<=ai<=1000000),要求你构造一个大小 ...
- Qone 正式开源,使 javascript 支持 .NET LINQ
Qone 下一代 Web 查询语言,使 javascript 支持 LINQ Github: https://github.com/dntzhang/qone 缘由 最近刚好修改了腾讯文档 Excel ...
- Springboot整合log4j2【详细步骤】
1.去除logback中的依赖包 <dependency> <groupId>org.springframework.boot</groupId> <arti ...