ExecutorService का नामकरण थ्रेड्स और थ्रेड-पूल


228

मान लीजिए कि मेरे पास एक एप्लिकेशन है जो Executorफ्रेमवर्क का उपयोग करता है

Executors.newSingleThreadExecutor().submit(new Runnable(){
    @Override
    public void run(){
        // do stuff
    }
}

जब मैं इस एप्लिकेशन को डीबगर में चलाता हूं, तो निम्न (डिफ़ॉल्ट) नाम के साथ एक थ्रेड बनाया जाता है Thread[pool-1-thread-1]:। जैसा कि आप देख सकते हैं, यह बहुत उपयोगी नहीं है और जहां तक ​​मैं बता सकता हूं, Executorफ्रेमवर्क थ्रेडेड या थ्रेड-पूलों को नाम देने का एक आसान तरीका प्रदान नहीं करता है।

तो, थ्रेड्स / थ्रेड-पूलों के लिए नाम प्रदान करने के बारे में कैसे जाना जाता है? उदाहरण के लिए, Thread[FooPool-FooThread]

जवाबों:


118

आप एक आपूर्ति कर सकता ThreadFactoryहै newSingleThreadScheduledExecutor(ThreadFactory threadFactory)। कारखाने धागे बनाने के लिए जिम्मेदार होंगे, और उन्हें नाम देने में सक्षम होंगे।

जावदोक को उद्धृत करने के लिए :

नए सूत्र रच रहे हैं

एक का उपयोग करके नए धागे बनाए जाते हैं ThreadFactory। यदि अन्यथा निर्दिष्ट नहीं है, तो Executors.defaultThreadFactory()इसका उपयोग किया जाता है, जो सभी को एक ThreadGroupही NORM_PRIORITYप्राथमिकता और गैर-डेमॉन स्थिति के साथ समान होने के लिए थ्रेड बनाता है । एक अलग आपूर्ति करके ThreadFactory, आप थ्रेड का नाम, थ्रेड समूह, प्राथमिकता, डेमॉन स्थिति, आदि बदल सकते हैं। यदि ThreadFactoryनल से वापस लौटने पर पूछे जाने पर एक थ्रेड बनाने में विफल रहता है newThread, तो निष्पादक जारी रहेगा, लेकिन किसी भी कार्य को निष्पादित करने में सक्षम नहीं हो सकता है


283

अमरूद में लगभग हमेशा वही होता है जिसकी आपको आवश्यकता होती है

ThreadFactory namedThreadFactory = 
  new ThreadFactoryBuilder().setNameFormat("my-sad-thread-%d").build()

और इसे अपने पास कर दें ExecutorService


3
यह बढ़िया है!
मार्टिन विक्ट्सिका

25
यह दुख की बात है! :-(
विदेशी

मुझे यकीन नहीं है कि "अमरूद" कहां मिलेगा। Google के अमरूद के कई हिस्से हैं और एक ही नाम के दर्जनों पुस्तकालय हैं। मेरा मानना ​​है कि आप search.maven.org/artifact/com.google.guava/guava/29.0-jre/… । क्या वह सही है? आपके द्वारा प्रदान किया गया लिंक यह सुझाव देता है कि यह Google से है, लेकिन Google में "अमरूद" नाम के मावेन / सोनटाइप पर लगभग आधा दर्जन कलाकृतियां हैं।
जेसन

@ जेसन - यदि आप एक गैर तुच्छ जावा परियोजना लिख ​​रहे हैं, तो आपको सबसे अधिक संभावना पहले से ही एक निर्भरता के रूप में अमरूद की होनी चाहिए। और यहाँ यह है: github.com/google/guava
pathikrit

@pathikrit, धन्यवाद! मुझे लगता है कि मुझे अमरूद पर अधिक अध्ययन करने की आवश्यकता है :-)
जेसन

95

आप अपने स्वयं के धागा कारखाने प्रदान करने का प्रयास कर सकते हैं, जो उपयुक्त नामों के साथ धागा बनाएंगे। यहाँ एक उदाहरण है:

class YourThreadFactory implements ThreadFactory {
   public Thread newThread(Runnable r) {
     return new Thread(r, "Your name");
   }
 }

Executors.newSingleThreadExecutor(new YourThreadFactory()).submit(someRunnable);

58

आप अपने थ्रेड का नाम बाद में बदल सकते हैं, जबकि थ्रेड निष्पादित होता है:

Thread.currentThread().setName("FooName");

उदाहरण के लिए, यदि आप विभिन्न प्रकार के कार्यों के लिए एक ही थ्रेडफैक्टरी का उपयोग कर रहे हैं, तो यह ब्याज की हो सकती है।


7
यह अच्छी तरह से काम किया क्योंकि जैसा कि फ्लोरियन ने वर्णित किया है, मेरे पास कई अलग-अलग प्रकार के धागे हैं और केवल नाम के लिए कई थ्रेडफैक्टरी ऑब्जेक्ट्स बनाना नहीं चाहते थे। मैंने Thread.currentThread ()। SetName ("FooName") कहा; प्रत्येक रन () विधि में पहली पंक्ति के रूप में।
रॉबिन ज़िम्मरमैन

5
इसके साथ एक मामूली समस्या यह है कि जब डॉक्स में वर्णित विफलता व्यवहार होता है (Note however that if this single thread terminates due to a failure during execution prior to shutdown, a new one will take its place if needed to execute subsequent tasks.):। यदि ExecutorService धागे की जगह लेता है, तो इसे थ्रेडफैक्ट्री द्वारा नाम दिया जाएगा। फिर, डिबगिंग के दौरान नाम गायब होते देखना एक उपयोगी संकेतक हो सकता है।
सेठ्रो

बस शानदार! धन्यवाद।
asgs

1
जैसा कि अन्य उत्तर कहता है, यह नाम सेट करने के लिए एक त्वरित और गंदी विधि है, और यदि आप कई थ्रेड्स के साथ ऐसा करते हैं, तो सभी का नाम एक ही होगा !!
टानो

बाहर निकलने पर थ्रेड नाम को वापस मूल पर सेट करना चाह सकते हैं, क्योंकि यह नाम को अलग-अलग असंबंधित कार्यों पर काम करने पर भी बनाए रख सकता है।
डस्टिन के

51

BasicThreadFactoryApache Commons-लैंग से भी नामकरण व्यवहार प्रदान करने के लिए उपयोगी है। एक अनाम आंतरिक वर्ग लिखने के बजाय, आप बिल्डर को थ्रेड्स का नाम देने के लिए उपयोग कर सकते हैं जैसा आप चाहते हैं। यहाँ javadocs से उदाहरण है:

 // Create a factory that produces daemon threads with a naming pattern and
 // a priority
 BasicThreadFactory factory = new BasicThreadFactory.Builder()
     .namingPattern("workerthread-%d")
     .daemon(true)
     .priority(Thread.MAX_PRIORITY)
     .build();
 // Create an executor service for single-threaded execution
 ExecutorService exec = Executors.newSingleThreadExecutor(factory);

30

यदि आप स्प्रिंग का उपयोग कर रहे हैं, तो ऐसा CustomizableThreadFactoryहै जिसके लिए आप थ्रेड नाम उपसर्ग सेट कर सकते हैं।

उदाहरण:

ExecutorService alphaExecutor =
    Executors.newFixedThreadPool(10, new CustomizableThreadFactory("alpha-"));

वैकल्पिक रूप से, आप ExecutorServiceउपयोग करके स्प्रिंग बीन के रूप में अपना बना सकते हैं ThreadPoolExecutorFactoryBean- फिर थ्रेड्स को सभी beanName-उपसर्ग के साथ नाम दिया जाएगा ।

@Bean
public ThreadPoolExecutorFactoryBean myExecutor() {
    ThreadPoolExecutorFactoryBean executorFactoryBean = new ThreadPoolExecutorFactoryBean();
    // configuration of your choice
    return executorFactoryBean;
}

ऊपर के उदाहरण में, थ्रेड्स का नाम myExecutor-उपसर्ग के साथ दिया जाएगा । फ़ैक्टरी बीन पर "myPool-"सेटिंग करके आप उपसर्ग को स्पष्ट रूप से एक अलग मूल्य (जैसे ) executorFactoryBean.setThreadNamePrefix("myPool-")पर सेट कर सकते हैं ।


अनुकूलन योग्य नहीं मिल सकता है? मैं jdk 1.7 का उपयोग कर रहा हूं। किसी भी विचार मैं यहाँ क्या याद आ रही है?
कामरान शाहिद

@KamranShahid यह एक स्प्रिंग फ्रेमवर्क क्लास है, आपको इसे करने के लिए स्प्रिंग का उपयोग करना होगा
एडम मिशालिक

20

ओरेकल के साथ इसके लिए एक खुला RFE है । ओरेकल कर्मचारी की टिप्पणियों से ऐसा लगता है कि वे इस मुद्दे को नहीं समझते हैं और ठीक नहीं करेंगे। यह इन चीजों में से एक है जो जेडीके (बैकवर्ड संगतता को तोड़ने के बिना) में समर्थन के लिए सरल मृत है, इसलिए यह एक शर्म की तरह है कि आरएफई को गलत समझा जाता है।

जैसा कि आपको बताया गया है कि आपको अपने स्वयं के थ्रेडफैक्ट को लागू करने की आवश्यकता है । यदि आप केवल इस उद्देश्य के लिए अमरूद या अपाचे कॉमन्स में नहीं खींचना चाहते हैं तो मैं यहां एक ThreadFactoryकार्यान्वयन प्रदान करता हूं जिसे आप उपयोग कर सकते हैं। यह बिल्कुल वैसा ही है जैसा कि आप "पूल" के अलावा किसी और चीज़ के लिए थ्रेड नाम को उपसर्ग सेट करने की क्षमता के अलावा जेडीके से प्राप्त करते हैं।

package org.demo.concurrency;

import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * ThreadFactory with the ability to set the thread name prefix. 
 * This class is exactly similar to 
 * {@link java.util.concurrent.Executors#defaultThreadFactory()}
 * from JDK8, except for the thread naming feature.
 *
 * <p>
 * The factory creates threads that have names on the form
 * <i>prefix-N-thread-M</i>, where <i>prefix</i>
 * is a string provided in the constructor, <i>N</i> is the sequence number of
 * this factory, and <i>M</i> is the sequence number of the thread created 
 * by this factory.
 */
public class ThreadFactoryWithNamePrefix implements ThreadFactory {

    // Note:  The source code for this class was based entirely on 
    // Executors.DefaultThreadFactory class from the JDK8 source.
    // The only change made is the ability to configure the thread
    // name prefix.


    private static final AtomicInteger poolNumber = new AtomicInteger(1);
    private final ThreadGroup group;
    private final AtomicInteger threadNumber = new AtomicInteger(1);
    private final String namePrefix;

    /**
     * Creates a new ThreadFactory where threads are created with a name prefix
     * of <code>prefix</code>.
     *
     * @param prefix Thread name prefix. Never use a value of "pool" as in that
     *      case you might as well have used
     *      {@link java.util.concurrent.Executors#defaultThreadFactory()}.
     */
    public ThreadFactoryWithNamePrefix(String prefix) {
        SecurityManager s = System.getSecurityManager();
        group = (s != null) ? s.getThreadGroup()
                : Thread.currentThread().getThreadGroup();
        namePrefix = prefix + "-"
                + poolNumber.getAndIncrement()
                + "-thread-";
    }


    @Override
    public Thread newThread(Runnable r) {
        Thread t = new Thread(group, r,
                namePrefix + threadNumber.getAndIncrement(),
                0);
        if (t.isDaemon()) {
            t.setDaemon(false);
        }
        if (t.getPriority() != Thread.NORM_PRIORITY) {
            t.setPriority(Thread.NORM_PRIORITY);
        }
        return t;
    }
}

जब आप इसका उपयोग करना चाहते हैं तो आप बस इस तथ्य का लाभ उठाते हैं कि सभी Executorsविधियां आपको अपना स्वयं का प्रदान करने की अनुमति देती हैं ThreadFactory

यह

    Executors.newSingleThreadExecutor();

एक ExecutorService देगा जहां थ्रेड्स का नाम दिया गया है pool-N-thread-Mलेकिन उपयोग करके

    Executors.newSingleThreadExecutor(new ThreadFactoryWithNamePrefix("primecalc"));

आपको एक ExecutorService मिलेगा जहां थ्रेड्स का नाम दिया गया है primecalc-N-thread-M। देखा!


आपने अपने आखिरी स्निपेट में एक बंद कोष्ठक को याद किया है
k.liakos

बस एक त्वरित नोट है कि सोनारलिंट / क्यूब के ThreadGroupपक्ष में उपयोग नहीं करना पसंद करते हैं ThreadPoolExecutor
Drakes

8
private class TaskThreadFactory implements ThreadFactory
{

    @Override
    public Thread newThread(Runnable r) {
        Thread t = new Thread(r, "TASK_EXECUTION_THREAD");

        return t;
    }

}

एक निष्पादनकर्ता को थ्रेडफैक्ट्री पास करें और आप जाने के लिए अच्छे हैं


8

एक त्वरित और गंदा तरीका विधि Thread.currentThread().setName(myName);में उपयोग करना run()है।


7

थ्रेडफैक्टरी बढ़ाएँ

public interface ThreadFactory

एक वस्तु जो मांग पर नए धागे बनाती है। थ्रेड फैक्ट्रियों के उपयोग से नए थ्रेड को कॉल की हार्डवेअरिंग को हटा दिया जाता है, जिससे एप्लिकेशन विशेष थ्रेड उपवर्गों, प्राथमिकताओं आदि का उपयोग करने में सक्षम हो जाता है।

Thread newThread(Runnable r)

एक नया सूत्र बनाता है। कार्यान्वयन प्राथमिकता, नाम, डेमॉन स्थिति, थ्रेडग्रुप आदि को भी प्रारंभ कर सकता है।

नमूना कोड:

import java.util.concurrent.*;
import java.util.concurrent.atomic.*;

import java.util.concurrent.ThreadPoolExecutor.DiscardPolicy;

class SimpleThreadFactory implements ThreadFactory {
   String name;
   AtomicInteger threadNo = new AtomicInteger(0);

   public SimpleThreadFactory (String name){
       this.name = name;
   }
   public Thread newThread(Runnable r) {
     String threadName = name+":"+threadNo.incrementAndGet();
     System.out.println("threadName:"+threadName);
     return new Thread(r,threadName );
   }
   public static void main(String args[]){
        SimpleThreadFactory factory = new SimpleThreadFactory("Factory Thread");
        ThreadPoolExecutor executor= new ThreadPoolExecutor(1,1,60,
                    TimeUnit.SECONDS,new ArrayBlockingQueue<Runnable>(1),new ThreadPoolExecutor.DiscardPolicy());


        final ExecutorService executorService = Executors.newFixedThreadPool(5,factory);

        for ( int i=0; i < 100; i++){
            executorService.submit(new Runnable(){
                 public void run(){
                    System.out.println("Thread Name in Runnable:"+Thread.currentThread().getName());
                 }
            });
        }
        executorService.shutdown();
    }
 }

उत्पादन:

java SimpleThreadFactory

thread no:1
thread no:2
Thread Name in Runnable:Factory Thread:1
Thread Name in Runnable:Factory Thread:2
thread no:3
thread no:4
Thread Name in Runnable:Factory Thread:3
Thread Name in Runnable:Factory Thread:4
thread no:5
Thread Name in Runnable:Factory Thread:5

....आदि


1
आप थ्रेड काउंटर थ्रेड-सुरक्षित नहीं हैं: आपको एक एटोमिकइंटर का उपयोग करना चाहिए।
पीनो

सुझाव के लिए धन्यवाद। मैंने आपके सुझाव को शामिल कर लिया है।
रविंद्र बाबू 12

5

जैसा कि अन्य उत्तर पहले ही कह चुके हैं, आप java.util.concurrent.ThreadFactoryइंटरफ़ेस के अपने स्वयं के कार्यान्वयन को बना सकते हैं और उपयोग कर सकते हैं (कोई बाहरी पुस्तकालयों की आवश्यकता नहीं)। मैं नीचे अपना कोड चिपका रहा हूं क्योंकि यह पिछले उत्तरों से अलग है क्योंकि यह String.formatविधि का उपयोग करता है और निर्माणकर्ता के रूप में थ्रेड्स के लिए एक आधार नाम लेता है:

import java.util.concurrent.ThreadFactory;

public class NameableThreadFactory implements ThreadFactory{
    private int threadsNum;
    private final String namePattern;

    public NameableThreadFactory(String baseName){
        namePattern = baseName + "-%d";
    }

    @Override
    public Thread newThread(Runnable runnable){
        threadsNum++;
        return new Thread(runnable, String.format(namePattern, threadsNum));
    }    
}

और यह उपयोग का एक उदाहरण है:

ThreadFactory  threadFactory = new NameableThreadFactory("listenerThread");        
final ExecutorService executorService = Executors.newFixedThreadPool(5, threadFactory);

संपादित करें : इसे ThreadFactoryलागू करने के लिए मेरे कार्यान्वयन थ्रेड-सुरक्षित, @mchernyakov को धन्यवाद । दस्तावेज
में कहीं भी यह नहीं ThreadFactoryकहा गया है कि इसका कार्यान्वयन थ्रेड-सुरक्षित होना चाहिए, यह तथ्य कि DefaultThreadFactoryथ्रेड-सेफ़ है एक बड़ा संकेत है:

import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;

public class NameableThreadFactory implements ThreadFactory{
    private final AtomicInteger threadsNum = new AtomicInteger();

    private final String namePattern;

    public NameableThreadFactory(String baseName){
        namePattern = baseName + "-%d";
    }

    @Override
    public Thread newThread(Runnable runnable){
        return new Thread(runnable, String.format(namePattern, threadsNum.addAndGet(1)));
    }    
}

1
आप थ्रेड काउंटर (थ्रेड्सनम) थ्रेडसेफ़ नहीं है, आपको एटोमिकइंटर का उपयोग करना चाहिए।
मचर्न्याकोव

इसे इंगित करने के लिए धन्यवाद, @mchernyakov मैंने अभी-अभी अपने उत्तर को संपादित किया है।
विक्टर गिल

4

मौजूदा कारखानों को सजाने के लिए घर में इस्तेमाल होने वाला मुख्य जावा समाधान:

public class ThreadFactoryNameDecorator implements ThreadFactory {
    private final ThreadFactory defaultThreadFactory;
    private final String suffix;

    public ThreadFactoryNameDecorator(String suffix) {
        this(Executors.defaultThreadFactory(), suffix);
    }

    public ThreadFactoryNameDecorator(ThreadFactory threadFactory, String suffix) {
        this.defaultThreadFactory = threadFactory;
        this.suffix = suffix;
    }

    @Override
    public Thread newThread(Runnable task) {
        Thread thread = defaultThreadFactory.newThread(task);
        thread.setName(thread.getName() + "-" + suffix);
        return thread;
    }
}

कार्रवाई में:

Executors.newSingleThreadExecutor(new ThreadFactoryNameDecorator("foo"));


3

आप थ्रेडफैक्टरी का अपना कार्यान्वयन लिख सकते हैं, उदाहरण के लिए कुछ मौजूदा कार्यान्वयन (जैसे defaultThreadFactory) और अंत में नाम बदल सकते हैं।

थ्रेडफैक्टरी लागू करने का उदाहरण:

class ThreadFactoryWithCustomName implements ThreadFactory {
    private final ThreadFactory threadFactory;
    private final String name;

    public ThreadFactoryWithCustomName(final ThreadFactory threadFactory, final String name) {
        this.threadFactory = threadFactory;
        this.name = name;
    }

    @Override
    public Thread newThread(final Runnable r) {
        final Thread thread = threadFactory.newThread(r);
        thread.setName(name);
        return thread;
    }
}

और उपयोग:

Executors.newSingleThreadExecutor(new ThreadFactoryWithCustomName(
        Executors.defaultThreadFactory(),
        "customName")
    );

3

मैं नीचे की तरह ही ( guavaपुस्तकालय की आवश्यकता है) का उपयोग करता हूं :

ThreadFactory namedThreadFactory = new ThreadFactoryBuilder().setNameFormat("SO-POOL-%d").build();
ExecutorService executorService = Executors.newFixedThreadPool(5,namedThreadFactory);

1
वर्थ नोटिंग जो कि ThreadFactoryBuilderGoogle Guava लाइब्रेरी से है।
क्रेग ओटिस

3

मुझे एक थ्रेड फैक्ट्री के रूप में एक लंबोदा का उपयोग करना सबसे आसान लगता है अगर आप सिर्फ एक थ्रेड निष्पादक के लिए नाम बदलना चाहते हैं।

Executors.newSingleThreadExecutor(runnable -> new Thread(runnable, "Your name"));

यह दो धागे बनाता है। एक का नाम "आपका नाम" और दूसरा "पूल-एन-थ्रेड-एम"
Systemsplanet

@Systemsplanet नहीं, यह नहीं है। एक न्यूनतम उदाहरण से एक थ्रेड डंप लेना जो एक थ्रेड को चलाने के लिए निष्पादक का उपयोग करता है जो सोता है निम्नलिखित धागे दिखाता है:main@1, Finalizer@667, Reference Handler@668, Your name@665, Signal Dispatcher@666
CamW

हुम, यह तब हुआ जब मैंने इसे आजमाया। यह समझ में आता है कि जब से आप इसे एक नया Runnable () पास करते हैं, तो यह आपके लिए एक धागा बनाता है, और आप स्वयं एक धागा बना रहे हैं।
सिस्टम्सप्लेनेट

मुझे उम्मीद है कि आपने इसके बजाय एक थ्रेडपूल का उपयोग किया है या किसी अन्य उद्देश्य के लिए चल रहा है। यह कोड "पूल-एन-थ्रेड-एम" थ्रेड नहीं बनाएगा। इसके अलावा, मुझे विश्वास नहीं है कि यह समझ में आता है कि यह होगा। आपका कथन "यदि आप इसे एक नया रननेबल () बनाते हैं तो यह आपके लिए एक धागा बनाता है" सही नहीं है। यह एक थ्रेड बनाने के लिए उस रन करने योग्य का उपयोग करता है और एक बार ऐसा करता है क्योंकि यह एक सिंगल-थ्रेड एक्ज़ीक्यूटर है। केवल 1 धागा बनाया जाता है।
कैमव्यू

2

यह मेरा अनुकूलित कारखाना है जो थ्रेड डंप विश्लेषक के लिए एक अनुकूलित नाम प्रदान करता है। आमतौर पर मैं सिर्फ tf=nullJVM डिफॉल्ट थ्रेड फैक्ट्री का पुन: उपयोग करने के लिए देता हूं । इस वेबसाइट में अधिक उन्नत धागा कारखाना है।

public class SimpleThreadFactory implements ThreadFactory {
    private ThreadFactory tf;
    private String nameSuffix;

    public SimpleThreadFactory (ThreadFactory tf, String nameSuffix) {
        this.tf = tf!=null ? tf : Executors.defaultThreadFactory();
        this.nameSuffix = nameSuffix; 
    }

    @Override public Thread newThread(Runnable task) {
        // default "pool-1-thread-1" to "pool-1-thread-1-myapp-MagicTask"
        Thread thread=tf.newThread(task);
        thread.setName(thread.getName()+"-"+nameSuffix);
        return thread;
    }
}

- - - - - 

ExecutorService es = Executors.newFixedThreadPool(4, new SimpleThreadFactory(null, "myapp-MagicTask") );

आपकी सुविधा के लिए यह डिबग उद्देश्य के लिए थ्रेड डंप लूप है।

    ThreadMXBean mxBean=ManagementFactory.getThreadMXBean();
    long[] tids = mxBean.getAllThreadIds();
    System.out.println("------------");
    System.out.println("ThreadCount="+tids.length);
    for(long tid : tids) {
        ThreadInfo mxInfo=mxBean.getThreadInfo(tid);
        if (mxInfo==null) {
            System.out.printf("%d %s\n", tid, "Thread not found");
        } else {
            System.out.printf("%d %s, state=%s, suspended=%d, lockowner=%d %s\n"
                    , mxInfo.getThreadId(), mxInfo.getThreadName()
                    , mxInfo.getThreadState().toString()
                    , mxInfo.isSuspended()?1:0
                    , mxInfo.getLockOwnerId(), mxInfo.getLockOwnerName()
            );
        }
    }

यह मेरे लिए वास्तव में अच्छी तरह से काम करता है, कुछ हद तक आश्चर्यचकित था कि यह बहुत ज्यादा था। किसी भी तरह से चीयर्स।
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.